Skip to content

Commit

Permalink
Remove challengeOptions and require a token instead (#15)
Browse files Browse the repository at this point in the history
* wip

* wip

* Update README.md
  • Loading branch information
hwhmeikle authored Oct 16, 2024
1 parent 15f3e96 commit 992aec9
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 94 deletions.
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,16 @@ yarn add @authsignal/react
```

## Usage
Add the `Authsignal` component to your app. Generally, this should be placed at the root of your app.
Render the `AuthsignalProvider` component at the root of your app.

```jsx
import { Authsignal } from '@authsignal/react';

function App() {
return (
<div>
<AuthsignalProvider tenantId="YOUR_TENANT_ID" baseUrl="YOUR_BASE_URL">
<Checkout />
<Authsignal tenantId="YOUR_TENANT_ID" baseUrl="YOUR_BASE_URL" />
</div>
</AuthsignalProvider>
);
}
```
Expand All @@ -52,9 +51,9 @@ export function Checkout() {

const data = await response.json();

if (data.challengeOptions) {
if (data.token) {
startChallenge({
challengeOptions: data.challengeOptions,
token: data.token,
onChallengeSuccess: ({ token }) => {
// Challenge was successful
},
Expand Down Expand Up @@ -95,10 +94,10 @@ export function Checkout() {

const data = await response.json();

if (data.challengeOptions) {
if (data.token) {
try {
const { token } = await startChallengeAsync({
challengeOptions: data.challengeOptions,
token: data.token,
});

// Challenge was successful
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@authsignal/react",
"version": "0.0.15",
"version": "0.1.0",
"description": "",
"keywords": [
"authsignal",
Expand Down
23 changes: 12 additions & 11 deletions src/authsignal-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import React from "react";

import { AuthsignalProps } from "./types";

type AuthsignalProviderProps = {
children: React.ReactNode;
} & AuthsignalProps;

export const AuthsignalContext = React.createContext<
Pick<AuthsignalProps, "baseUrl" | "tenantId" | "appearance"> | undefined
>(undefined);
import { Challenge } from "./components/challenge/challenge";
import { AuthsignalProviderProps, ChallengeProps } from "./types";
import { AuthsignalContext } from "./hooks/use-authsignal-context";

export function AuthsignalProvider({
children,
tenantId,
baseUrl,
baseUrl = "https://api.authsignal.com/v1",
appearance,
}: AuthsignalProviderProps) {
const [challenge, setChallenge] = React.useState<ChallengeProps | undefined>(
undefined,
);

return (
<AuthsignalContext.Provider value={{ tenantId, baseUrl, appearance }}>
<AuthsignalContext.Provider
value={{ tenantId, baseUrl, appearance, setChallenge, challenge }}
>
{children}
{challenge && <Challenge {...challenge} />}
</AuthsignalContext.Provider>
);
}
16 changes: 0 additions & 16 deletions src/authsignal.tsx

This file was deleted.

11 changes: 10 additions & 1 deletion src/hooks/use-authsignal-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import React from "react";
import { AuthsignalProviderProps, ChallengeProps } from "../types";

import { AuthsignalContext } from "../authsignal-provider";
export const AuthsignalContext = React.createContext<
| (Pick<AuthsignalProviderProps, "baseUrl" | "tenantId" | "appearance"> & {
challenge: ChallengeProps | undefined;
setChallenge: React.Dispatch<
React.SetStateAction<ChallengeProps | undefined>
>;
})
| undefined
>(undefined);

export function useAuthsignalContext() {
const context = React.useContext(AuthsignalContext);
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "./main.css";

export { Authsignal } from "./authsignal";
export { AuthsignalProvider } from "./authsignal-provider";
export { useAuthsignal, ChallengeError } from "./use-authsignal";
export { Appearance, AuthsignalProps, ChallengeOptions } from "./types";
export { Appearance, AuthsignalProviderProps } from "./types";
16 changes: 11 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ export type Appearance = {
variables?: AppearanceVariables;
};

export type AuthsignalProps = {
export type AuthsignalProviderProps = {
baseUrl?:
| "https://api.authsignal.com/v1"
| "https://au.api.authsignal.com/v1"
| "https://eu.api.authsignal.com/v1"
| (string & {});
tenantId: string;
appearance?: Appearance;
children: React.ReactNode;
};

export const VerificationMethod = {
Expand All @@ -47,13 +48,18 @@ export type ChallengeOptions = {
};
};

export type ChallengeProps = {
challengeOptions: ChallengeOptions;
type ChallengeCallbacks = {
onChallengeSuccess?: (params: { token: string }) => void;
onCancel?: () => void;
onTokenExpired?: () => void;
};

export type StartChallengeOptions = ChallengeProps;
export type ChallengeProps = {
challengeOptions: ChallengeOptions;
} & ChallengeCallbacks;

export type StartChallengeAsyncOptions = { challengeOptions: ChallengeOptions };
export type StartChallengeOptions = { token: string } & ChallengeCallbacks;

export type StartChallengeAsyncOptions = {
token: string;
};
110 changes: 60 additions & 50 deletions src/use-authsignal.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback } from "react";
import {
ChallengeOptions,
ChallengeProps,
StartChallengeAsyncOptions,
StartChallengeOptions,
} from "./types";
import { useAuthsignalContext } from "./hooks/use-authsignal-context";

const ANIMATION_DURATION = 500;

type Listener<T> = (payload: T) => void;

class EventEmitter<T> {
private listeners: Listener<T>[] = [];

subscribe(listener: Listener<T>): () => void {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}

emit(payload: T) {
this.listeners.forEach((listener) => listener(payload));
}
}

type ChallengeErrorCodes =
| "USER_CANCELED"
| "TOKEN_EXPIRED"
Expand All @@ -39,29 +24,54 @@ export class ChallengeError extends Error {
}
}

let memoryChallengeState: ChallengeProps | undefined;

const challengeEmitter = new EventEmitter<ChallengeProps | undefined>();
type InitResponse = {
challengeOptions: ChallengeOptions;
};

export function useAuthsignal() {
const [challenge, setChallenge] = useState<ChallengeProps | undefined>(
memoryChallengeState,
);
const { baseUrl, setChallenge, challenge } = useAuthsignalContext();

const getChallengeOptions = useCallback(
async ({ token }: { token: string }) => {
const initRequest = await fetch(`${baseUrl}/client/init`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({}),
});

if (!initRequest.ok) {
return;
}

const initResponse: InitResponse = await initRequest.json();

useEffect(() => {
const unsubscribe = challengeEmitter.subscribe(setChallenge);
return () => unsubscribe();
}, []);
return initResponse.challengeOptions;
},
[baseUrl],
);

const startChallenge = useCallback(
(options: StartChallengeOptions) => {
async (options: StartChallengeOptions) => {
if (challenge) {
throw new ChallengeError(
"EXISTING_CHALLENGE",
"An existing challenge is already in progress.",
);
}

const challengeOptions = await getChallengeOptions({
token: options.token,
});

if (!challengeOptions) {
options.onTokenExpired?.();
return;
}

const previouslyFocusedElement =
document.activeElement as HTMLElement | null;

Expand All @@ -70,16 +80,15 @@ export function useAuthsignal() {
};

const newChallenge: ChallengeProps = {
...options,
challengeOptions,

onChallengeSuccess: ({ token }) => {
setTimeout(() => {
if (options.onChallengeSuccess) {
options.onChallengeSuccess({ token });
}

memoryChallengeState = undefined;
challengeEmitter.emit(memoryChallengeState);
setChallenge(undefined);
}, ANIMATION_DURATION);
},

Expand All @@ -89,8 +98,7 @@ export function useAuthsignal() {
options.onCancel();
}

memoryChallengeState = undefined;
challengeEmitter.emit(memoryChallengeState);
setChallenge(undefined);

returnFocus();
}, ANIMATION_DURATION);
Expand All @@ -102,29 +110,35 @@ export function useAuthsignal() {
options.onTokenExpired();
}

memoryChallengeState = undefined;
challengeEmitter.emit(memoryChallengeState);
setChallenge(undefined);

returnFocus();
}, ANIMATION_DURATION);
},
};

memoryChallengeState = newChallenge;
challengeEmitter.emit(memoryChallengeState);
setChallenge(newChallenge);
},
[challenge],
[challenge, getChallengeOptions, setChallenge],
);

const startChallengeAsync = useCallback(
(options: StartChallengeAsyncOptions) => {
async (options: StartChallengeAsyncOptions) => {
if (challenge) {
throw new ChallengeError(
"EXISTING_CHALLENGE",
"An existing challenge is already in progress.",
);
}

const challengeOptions = await getChallengeOptions({
token: options.token,
});

if (!challengeOptions) {
throw new ChallengeError("TOKEN_EXPIRED", "Challenge token expired.");
}

const previouslyFocusedElement =
document.activeElement as HTMLElement | null;

Expand All @@ -134,14 +148,13 @@ export function useAuthsignal() {

return new Promise<{ token: string }>((resolve, reject) => {
const newChallenge: ChallengeProps = {
...options,
challengeOptions,

onChallengeSuccess: ({ token }) => {
setTimeout(() => {
resolve({ token });

memoryChallengeState = undefined;
challengeEmitter.emit(memoryChallengeState);
setChallenge(undefined);
}, ANIMATION_DURATION);
},

Expand All @@ -154,8 +167,7 @@ export function useAuthsignal() {
),
);

memoryChallengeState = undefined;
challengeEmitter.emit(memoryChallengeState);
setChallenge(undefined);

returnFocus();
}, ANIMATION_DURATION);
Expand All @@ -167,19 +179,17 @@ export function useAuthsignal() {
new ChallengeError("TOKEN_EXPIRED", "Challenge token expired."),
);

memoryChallengeState = undefined;
challengeEmitter.emit(memoryChallengeState);
setChallenge(undefined);

returnFocus();
}, ANIMATION_DURATION);
},
};

memoryChallengeState = newChallenge;
challengeEmitter.emit(memoryChallengeState);
setChallenge(newChallenge);
});
},
[challenge],
[challenge, getChallengeOptions, setChallenge],
);

return { challenge, startChallenge, startChallengeAsync };
Expand Down

0 comments on commit 992aec9

Please sign in to comment.