Skip to content

Commit

Permalink
Merge pull request #3 from authsignal/use-authsignal
Browse files Browse the repository at this point in the history
Refactor to hook based API
  • Loading branch information
hwhmeikle authored Oct 4, 2024
2 parents 9bde023 + 2fd9142 commit 22cd593
Show file tree
Hide file tree
Showing 18 changed files with 254 additions and 123 deletions.
49 changes: 24 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,31 @@ yarn add @authsignal/react
```

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

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

function App() {
return (
<AuthsignalProvider tenantId="<AUTHSIGNAL_TENANT_ID>" baseUrl="<AUTHSIGNAL_BASE_URL>">
<div>
<Checkout />
</AuthsignalProvider>
<Authsignal tenantId="YOUR_TENANT_ID" baseUrl="YOUR_BASE_URL" />
</div>
);
}
```
Import the `useAuthsignal` hook in your component.

Then use the `AuthChallenge` component to start the authentication flow.
Then pass the `challengeOptions` returned from your server to the `startChallenge` function.

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

function Checkout() {
const [authsignalToken, setAuthsignalToken] = React.useState(null);
export function Checkout() {
const { startChallenge } = useAuthsignal();

const handlePayment = async () => {
// Return the Authsignal token from your server's track action call
const response = await fetch('/api/payment', {
method: 'POST',
headers: {
Expand All @@ -47,27 +48,25 @@ function Checkout() {

const data = await response.json();

setAuthsignalToken(data.authsignalToken);
if (data.challengeOptions) {
startChallenge({
...challengeOptions,
onChallengeSuccess: () => {
// Challenge was successful
},
onCancel: () => {
// User cancelled the challenge
},
onTokenExpired: () => {
// Token expired
},
});
}
};

return (
<div>
<button type="button" onClick={handlePayment}>Pay</button>

{authsignalToken && (
<AuthChallenge
token={authsignalToken}
onChallengeSuccess={() => {
console.log("Payment successful");
}}
onCancel={() => {
console.log("Payment cancelled");
}}
onTokenExpired={() => {
console.log("Token expired");
}}
/>
)}
</div>
);
}
Expand Down
17 changes: 9 additions & 8 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import { fixupPluginRules } from "@eslint/compat";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import prettier from "eslint-plugin-prettier";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";

import { fixupPluginRules } from "@eslint/compat";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import prettier from "eslint-plugin-prettier";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -30,7 +31,7 @@ export default [
{
plugins: {
react,
// @ts-ignore
// @ts-expect-error - this works
"react-hooks": fixupPluginRules(reactHooks),
"@typescript-eslint": typescriptEslint,
prettier,
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.3",
"version": "0.0.4",
"description": "",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.mjs",
Expand Down
36 changes: 36 additions & 0 deletions src/authsignal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";

import { AuthsignalProvider } from "./authsignal-provider";
import { Challenge } from "./components/challenge/challenge";
import { useAuthsignal } from "./use-authsignal";

type AuthsignalProps = {
baseUrl?:
| "https://api.authsignal.com/v1"
| "https://au.api.authsignal.com/v1"
| "https://eu.api.authsignal.com/v1"
| (string & {});
tenantId: string;
appearance?: {
theme?: "authsignal";
variables?: {
colorPrimary?: string;
colorBackground?: string;
colorText?: string;
colorDanger?: string;
fontFamily?: string;
spacingUnit?: string;
borderRadius?: string;
};
};
};

export function Authsignal(props: AuthsignalProps) {
const { challenge } = useAuthsignal();

return (
<AuthsignalProvider {...props}>
{challenge && <Challenge {...challenge} />}
</AuthsignalProvider>
);
}
Original file line number Diff line number Diff line change
@@ -1,81 +1,56 @@
import { Authsignal } from "@authsignal/browser";
import React from "react";
import { Drawer } from "vaul";
import { EmailOtpChallenge } from "./screens/email-otp-challenge";
import { useAuthsignal } from "../../hooks/use-authsignal";
import { VerificationMethods } from "./screens/verification-methods";
import { PasskeyChallenge } from "./screens/passkey-challenge";
import { EmailOtpIcon } from "../icons/email-otp-icon";

import { useAuthsignalContext } from "../../hooks/use-authsignal-context";
import { useMediaQuery } from "../../hooks/use-media-query";
import { Dialog, DialogContent } from "../../ui/dialog";
import { AuthenticatorAppIcon } from "../icons/authenticator-app-icon";
import { EmailOtpIcon } from "../icons/email-otp-icon";
import { PasskeyIcon } from "../icons/passkey-icon";
import { AuthenticatorAppChallenge } from "./screens/authenticator-app-challenge";
import { SmsOtpIcon } from "../icons/sms-otp-icon";

import { AuthenticatorAppChallenge } from "./screens/authenticator-app-challenge";
import { EmailOtpChallenge } from "./screens/email-otp-challenge";
import { PasskeyChallenge } from "./screens/passkey-challenge";
import { SmsOtpChallenge } from "./screens/sms-otp-challenge";
import { Authsignal } from "@authsignal/browser";
import { useMediaQuery } from "../../hooks/use-media-query";
import { Dialog, DialogContent } from "../../ui/dialog";
import { VerificationMethods } from "./screens/verification-methods";
import {
AuthChallengeContext,
TVerificationMethod,
useChallengeContext,
VerificationMethod,
} from "./use-challenge-context";

type AuthChallengeProps = {
export type ChallengeProps = {
token: string;
onChallengeSuccess: () => void;
onCancel?: () => void;
onTokenExpired?: () => void;
defaultVerificationMethod?: TVerificationMethod;
verificationMethods?: TVerificationMethod[];
userDetails?: {
user?: {
email?: string;
phoneNumber?: string;
};
};

export const VerificationMethod = {
PASSKEY: "PASSKEY",
EMAIL_OTP: "EMAIL_OTP",
AUTHENTICATOR_APP: "AUTHENTICATOR_APP",
SMS: "SMS",
} as const;

export type TVerificationMethod =
(typeof VerificationMethod)[keyof typeof VerificationMethod];

type AuthChallengeState = {
verificationMethod?: TVerificationMethod;
setVerificationMethod: React.Dispatch<
React.SetStateAction<TVerificationMethod | undefined>
>;
handleChallengeSuccess: () => void;
authsignal: Authsignal;
} & Pick<AuthChallengeProps, "userDetails" | "verificationMethods">;

const AuthChallengeContext = React.createContext<
AuthChallengeState | undefined
>(undefined);

export function useAuthChallenge() {
const context = React.useContext(AuthChallengeContext);

if (!context) {
throw new Error("useAuthChallenge must be used within a AuthChallenge");
}

return context;
}

export function AuthChallenge({
export function Challenge({
defaultVerificationMethod,
onChallengeSuccess,
onCancel,
token,
userDetails,
user,
verificationMethods,
onTokenExpired,
}: AuthChallengeProps) {
}: ChallengeProps) {
const [open, setOpen] = React.useState(false);

const [verificationMethod, setVerificationMethod] = React.useState<
TVerificationMethod | undefined
>(defaultVerificationMethod);

const { tenantId, baseUrl } = useAuthsignal();
const { tenantId, baseUrl } = useAuthsignalContext();

const onTokenExpiredRef = React.useRef(onTokenExpired);

Expand Down Expand Up @@ -156,7 +131,7 @@ export function AuthChallenge({
verificationMethods,
setVerificationMethod,
handleChallengeSuccess,
userDetails,
user,
authsignal,
}}
>
Expand Down Expand Up @@ -211,7 +186,7 @@ const verificationMethodConfig: Record<

function AuthChallengeFooter() {
const { verificationMethod, verificationMethods, setVerificationMethod } =
useAuthChallenge();
useChallengeContext();

if (
(verificationMethods && verificationMethods.length > 2) ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Drawer } from "vaul";
import { z } from "zod";

import { cn } from "../../../lib/utils";
import {
FormField,
Expand All @@ -12,8 +14,7 @@ import {
Form,
} from "../../../ui/form";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "../../../ui/input-otp";
import { Drawer } from "vaul";
import { useAuthChallenge } from "../auth-challenge";
import { useChallengeContext } from "../use-challenge-context";

const formSchema = z.object({
code: z.string().min(6, { message: "Enter a valid code" }),
Expand All @@ -30,7 +31,7 @@ export function AuthenticatorAppChallenge() {
OtpInputState.IDLE,
);

const { handleChallengeSuccess, authsignal } = useAuthChallenge();
const { handleChallengeSuccess, authsignal } = useChallengeContext();

const submitButtonRef = React.useRef<HTMLButtonElement>(null);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Drawer } from "vaul";
import { z } from "zod";

import { cn } from "../../../lib/utils";
import {
FormField,
Expand All @@ -12,8 +14,7 @@ import {
Form,
} from "../../../ui/form";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "../../../ui/input-otp";
import { Drawer } from "vaul";
import { useAuthChallenge } from "../auth-challenge";
import { useChallengeContext } from "../use-challenge-context";

const formSchema = z.object({
code: z.string().min(6, { message: "Enter a valid code" }),
Expand All @@ -30,8 +31,7 @@ export function EmailOtpChallenge() {
OtpInputState.IDLE,
);

const { handleChallengeSuccess, userDetails, authsignal } =
useAuthChallenge();
const { handleChallengeSuccess, user, authsignal } = useChallengeContext();

const submitButtonRef = React.useRef<HTMLButtonElement>(null);

Expand Down Expand Up @@ -89,8 +89,7 @@ export function EmailOtpChallenge() {
Confirm it&apos;s you
</Drawer.Title>
<p className="as-text-center as-text-sm">
Enter the code sent to {userDetails?.email ?? "<insert-email>"} to
proceed.
Enter the code sent to {user?.email ?? ""} to proceed.
</p>
</div>
<Form {...form}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReloadIcon } from "@radix-ui/react-icons";
import React, { useCallback } from "react";
import { Drawer } from "vaul";
import { ReloadIcon } from "@radix-ui/react-icons";
import { useAuthChallenge } from "../auth-challenge";
import { useChallengeContext } from "../use-challenge-context";

type PasskeyChallengeProps = {
token: string; // TODO: This should be set in the web sdk
Expand All @@ -15,7 +15,7 @@ enum State {
export function PasskeyChallenge({ token }: PasskeyChallengeProps) {
const [state, setState] = React.useState<State>(State.AUTHENTICATING);

const { handleChallengeSuccess, authsignal } = useAuthChallenge();
const { handleChallengeSuccess, authsignal } = useChallengeContext();

const handlePasskeyAuthentication = useCallback(async () => {
const handleError = () => {
Expand Down
Loading

0 comments on commit 22cd593

Please sign in to comment.