diff --git a/README.md b/README.md
index 7505d1c..24765c5 100644
--- a/README.md
+++ b/README.md
@@ -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 (
-
+
);
}
```
@@ -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
},
@@ -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
diff --git a/package.json b/package.json
index 4f4f1f7..24965db 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@authsignal/react",
- "version": "0.0.15",
+ "version": "0.1.0",
"description": "",
"keywords": [
"authsignal",
diff --git a/src/authsignal-provider.tsx b/src/authsignal-provider.tsx
index 7122f6e..21a2d70 100644
--- a/src/authsignal-provider.tsx
+++ b/src/authsignal-provider.tsx
@@ -1,24 +1,25 @@
import React from "react";
-import { AuthsignalProps } from "./types";
-
-type AuthsignalProviderProps = {
- children: React.ReactNode;
-} & AuthsignalProps;
-
-export const AuthsignalContext = React.createContext<
- Pick | 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(
+ undefined,
+ );
+
return (
-
+
{children}
+ {challenge && }
);
}
diff --git a/src/authsignal.tsx b/src/authsignal.tsx
deleted file mode 100644
index 78724bd..0000000
--- a/src/authsignal.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-
-import { AuthsignalProvider } from "./authsignal-provider";
-import { Challenge } from "./components/challenge/challenge";
-import { useAuthsignal } from "./use-authsignal";
-import { AuthsignalProps } from "./types";
-
-export function Authsignal(props: AuthsignalProps) {
- const { challenge } = useAuthsignal();
-
- return (
-
- {challenge && }
-
- );
-}
diff --git a/src/hooks/use-authsignal-context.ts b/src/hooks/use-authsignal-context.ts
index cfc4664..70ce6a2 100644
--- a/src/hooks/use-authsignal-context.ts
+++ b/src/hooks/use-authsignal-context.ts
@@ -1,6 +1,15 @@
import React from "react";
+import { AuthsignalProviderProps, ChallengeProps } from "../types";
-import { AuthsignalContext } from "../authsignal-provider";
+export const AuthsignalContext = React.createContext<
+ | (Pick & {
+ challenge: ChallengeProps | undefined;
+ setChallenge: React.Dispatch<
+ React.SetStateAction
+ >;
+ })
+ | undefined
+>(undefined);
export function useAuthsignalContext() {
const context = React.useContext(AuthsignalContext);
diff --git a/src/index.ts b/src/index.ts
index dfc1d94..61d0f03 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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";
diff --git a/src/types.ts b/src/types.ts
index 02730e6..067a829 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -17,7 +17,7 @@ export type Appearance = {
variables?: AppearanceVariables;
};
-export type AuthsignalProps = {
+export type AuthsignalProviderProps = {
baseUrl?:
| "https://api.authsignal.com/v1"
| "https://au.api.authsignal.com/v1"
@@ -25,6 +25,7 @@ export type AuthsignalProps = {
| (string & {});
tenantId: string;
appearance?: Appearance;
+ children: React.ReactNode;
};
export const VerificationMethod = {
@@ -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;
+};
diff --git a/src/use-authsignal.tsx b/src/use-authsignal.tsx
index ffd4279..7632753 100644
--- a/src/use-authsignal.tsx
+++ b/src/use-authsignal.tsx
@@ -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 = (payload: T) => void;
-
-class EventEmitter {
- private listeners: Listener[] = [];
-
- subscribe(listener: Listener): () => 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"
@@ -39,22 +24,38 @@ export class ChallengeError extends Error {
}
}
-let memoryChallengeState: ChallengeProps | undefined;
-
-const challengeEmitter = new EventEmitter();
+type InitResponse = {
+ challengeOptions: ChallengeOptions;
+};
export function useAuthsignal() {
- const [challenge, setChallenge] = useState(
- 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",
@@ -62,6 +63,15 @@ export function useAuthsignal() {
);
}
+ const challengeOptions = await getChallengeOptions({
+ token: options.token,
+ });
+
+ if (!challengeOptions) {
+ options.onTokenExpired?.();
+ return;
+ }
+
const previouslyFocusedElement =
document.activeElement as HTMLElement | null;
@@ -70,7 +80,7 @@ export function useAuthsignal() {
};
const newChallenge: ChallengeProps = {
- ...options,
+ challengeOptions,
onChallengeSuccess: ({ token }) => {
setTimeout(() => {
@@ -78,8 +88,7 @@ export function useAuthsignal() {
options.onChallengeSuccess({ token });
}
- memoryChallengeState = undefined;
- challengeEmitter.emit(memoryChallengeState);
+ setChallenge(undefined);
}, ANIMATION_DURATION);
},
@@ -89,8 +98,7 @@ export function useAuthsignal() {
options.onCancel();
}
- memoryChallengeState = undefined;
- challengeEmitter.emit(memoryChallengeState);
+ setChallenge(undefined);
returnFocus();
}, ANIMATION_DURATION);
@@ -102,22 +110,20 @@ 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",
@@ -125,6 +131,14 @@ export function useAuthsignal() {
);
}
+ const challengeOptions = await getChallengeOptions({
+ token: options.token,
+ });
+
+ if (!challengeOptions) {
+ throw new ChallengeError("TOKEN_EXPIRED", "Challenge token expired.");
+ }
+
const previouslyFocusedElement =
document.activeElement as HTMLElement | null;
@@ -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);
},
@@ -154,8 +167,7 @@ export function useAuthsignal() {
),
);
- memoryChallengeState = undefined;
- challengeEmitter.emit(memoryChallengeState);
+ setChallenge(undefined);
returnFocus();
}, ANIMATION_DURATION);
@@ -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 };