Skip to content

Commit

Permalink
Add Passkeys SDK UI Recipe and Page (#161)
Browse files Browse the repository at this point in the history
* Add passkeys SDK UI recipe and page

* Add views enum logic & success view

* Add views enum logic & success view
  • Loading branch information
ollie-stytch committed Dec 11, 2023
1 parent 251925b commit 6b660e5
Show file tree
Hide file tree
Showing 8 changed files with 360 additions and 253 deletions.
33 changes: 33 additions & 0 deletions components/Passkeys/LoginWithPasskeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import {OTPMethods, Products, StytchError, StytchEvent, StytchLoginConfig} from '@stytch/vanilla-js';
import {StytchLogin, useStytchUser} from '@stytch/nextjs';
import {useRouter} from 'next/router';

const loginConfig: StytchLoginConfig = {
sessionOptions: {
sessionDurationMinutes: 60,
},
products: [Products.passkeys, Products.otp],
otpOptions: {
expirationMinutes: 10,
methods: [OTPMethods.Email],
},
};

const callbackConfig = {
onEvent: (message: StytchEvent) => console.log(message),
onError: (error: StytchError) => console.log(error),
}

const LoginWithPasskeys = () => {
const { user } = useStytchUser();
const router = useRouter();

if (user) {
router.push('/recipes/passkeys/profile');
}

return <StytchLogin config={loginConfig} callbacks={callbackConfig} />;
};

export default LoginWithPasskeys;
187 changes: 187 additions & 0 deletions components/Passkeys/PasskeyRegistration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import React, {useEffect, useState} from 'react';
import {AuthenticationFactor, Products, StytchError, StytchEvent, StytchEventType} from '@stytch/vanilla-js';
import {StytchPasskeyRegistration, useStytch, useStytchSession, useStytchUser} from '@stytch/nextjs';

const styles: Record<string, React.CSSProperties> = {
registerButton: {
margin: 'auto'
}
};

enum StepUpType {
email = "email",
webauthn = "webauthn",
}

const StepUp = ({ type }: { type: StepUpType }) => {
const [inputValue, setInputValue] = useState("");
const [methodID, setMethodID] = useState("");
const { user } = useStytchUser();
const [error, setError] = useState("");
const stytch = useStytch();

const validateOTPButtonClick = () => {
stytch.otps.authenticate(inputValue, methodID, {
session_duration_minutes: 30,
}).catch((e) => {
setError("Error occurred validating OTP: " + e);
});
};

const handleSendOTPButtonClick = () => {
stytch.otps.email
.send(user?.emails?.at(0)?.email as string, {
expiration_minutes: 5,
})
.then((resp) => {
setMethodID(resp.method_id);
})
.catch((e) => {
setError("Error occurred sending email: " + e);
});
};

const handleInputChange = (event: any) => {
setInputValue(event.target.value);
};

if (type === StepUpType.webauthn) {
return (
<>
<h3>You need to step up {type} before creating Passkeys!</h3>
<button
color="primary"
onClick={() => {
stytch.webauthn.authenticate({
session_duration_minutes: 30,
});
}}
>
Step Up WebAuthn
</button>
</>
);
}

return (
<div>
<h3>You need to step up {type} before creating Passkeys!</h3>
<button
className="mt2"
onClick={handleSendOTPButtonClick}
>
Send OTP to{" "}
{user?.emails?.at(0)?.email as string}
</button>
<br/>
<br/>
<input
placeholder="123456"
type="text"
value={inputValue}
onChange={handleInputChange}
/>
<br/>
<button
className="mt2"
color="primary"
onClick={validateOTPButtonClick}
>
Validate OTP
</button>
{error}
</div>
);
};


enum PasskeyRegViews {
Start = "START",
Register = "REGISTER",
Success = "SUCCESS",
StepUpWebAuthn = "STEP_UP_WEBAUTHN",
StepUpEmail = "STEP_UP_EMAIL",
}

const PasskeyRegistration = () => {
const [displayView, setDisplayView] = useState(PasskeyRegViews.Start);
const { session } = useStytchSession();
const { user, isInitialized } = useStytchUser();

useEffect(() => {
const sessionHasPasskeyFactor = session?.authentication_factors?.some(
(factor: AuthenticationFactor) => factor.delivery_method === "webauthn_registration",
);
const sessionHasEmailFactor = session?.authentication_factors?.some(
(factor: AuthenticationFactor) => factor.delivery_method === "email",
);
const displayPasskeyStepUp = sessionHasEmailFactor && !sessionHasPasskeyFactor && user?.webauthn_registrations?.length! > 0;
const displayEmailStepUp = !sessionHasEmailFactor && sessionHasPasskeyFactor;
if (displayEmailStepUp) {
setDisplayView(PasskeyRegViews.StepUpEmail);
} else if (displayPasskeyStepUp){
setDisplayView(PasskeyRegViews.StepUpWebAuthn);
}

// If the user authenticates succesfully on the step-up page we should navigate to the registration view
if (displayView === PasskeyRegViews.StepUpEmail || displayView === PasskeyRegViews.StepUpWebAuthn
&& !displayEmailStepUp && !displayPasskeyStepUp) {
setDisplayView(PasskeyRegViews.Register);
}

},[session, user]);

const callbackConfig = {
onEvent: (message: StytchEvent) => {
console.log(message)
if (message.type === StytchEventType.PasskeySkip) {
alert("We just return to the start here, but you can do whatever you want!");
setDisplayView(PasskeyRegViews.Start);
}
if (message.type === StytchEventType.PasskeyDone) {
setDisplayView(PasskeyRegViews.Success);
}
},
onError: (error: StytchError) => console.log(error),
}

return (
<>
{displayView === PasskeyRegViews.Start && (
<button style={styles.registerButton} onClick={() => setDisplayView(PasskeyRegViews.Register)}>
Register a Passkey
</button>
)}
{displayView === PasskeyRegViews.StepUpWebAuthn && (
<StepUp type={StepUpType.webauthn} />
)}
{displayView === PasskeyRegViews.StepUpEmail && (
<StepUp type={StepUpType.email} />
)}
{displayView === PasskeyRegViews.Register && (
<StytchPasskeyRegistration
styles={{ container: { width: "400px" } }}
config={{ products: [Products.passkeys]}}
callbacks={callbackConfig}
/>
)}
{displayView === PasskeyRegViews.Success && (
<div>
<h3>Passkey created!</h3>
<p>
You can now use your Passkey to sign in to your account.
</p>
<button
className="mt2"
onClick={() => {
setDisplayView(PasskeyRegViews.Register);
}}>
Register Another Passkey
</button>
</div>
)}
</>
);
};

export default PasskeyRegistration;
5 changes: 5 additions & 0 deletions lib/loginProduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import smsIcon from '/public/sms-icon.svg';
import webauthnIcon from '/public/webauthn-icon.svg';
import web3Icon from '/public/web3-icon.svg';
import passwordsIcon from '/public/passwords-icon.svg';
import passkeysIcon from 'public/passkeys-icon.svg';
import { LoginProduct } from './types';

const LoginProducts: Record<string, LoginProduct> = {
Expand Down Expand Up @@ -31,6 +32,10 @@ const LoginProducts: Record<string, LoginProduct> = {
icon: passwordsIcon,
name: 'Passwords',
},
PASSKEYS: {
icon: passkeysIcon,
name: 'Passkeys',
}
};

export default LoginProducts;
36 changes: 33 additions & 3 deletions lib/recipeData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import LoginWithStytchSDKUI from '../components/LoginWithStytchSDKUI';
import LoginWithPasswords from '../components/Passwords/LoginWithPasswords';
import LoginProducts from './loginProduct';
import LoginWithOneTap from '../components/LoginWithOneTapSDKUI';
import LoginWithPasskeys from "../components/Passkeys/LoginWithPasskeys";
import {OTPMethods, Products, StytchLoginConfig} from "@stytch/vanilla-js";

export const Recipes: Record<string, LoginType> = {
REACT: {
Expand Down Expand Up @@ -127,7 +129,7 @@ const trigger = useCallback(async () => {
id: 'passwords',
title: 'Passwords',
details:
'Build an email/password authentication experience including passwords resets, password strength checking, and magic links using prebuilt Stytch UI components.',
'Build an email/password authentication experience including passwords resets, password strength checking, and magic links using prebuilt Stytch UI components.',
description: ``,
instructions: `To the right you'll see the Stytch UI configured for password login. Enter a new email address and you will be prompted to create an account with a secure password.`,
component: <LoginWithPasswords />,
Expand All @@ -152,14 +154,42 @@ const LoginWithPasswords = () => {
if (user) {
router.push('/profile');
}
return <StytchLogin config={loginConfig} callbacks={callbackConfig} />;`,
},
PASSKEYS: {
id: 'passkeys',
title: 'Passkeys',
details:
'Build an email/passkey authentication experience including passkey registrations and email OTPs using prebuilt Stytch UI components.',
description: ``,
instructions: 'To the right you\'ll see the Stytch UI configured for Email OTP and Passkey login. Continue with email to create an account. Then, once logged in, use the Passkey Registration SDK to create a passkey for your account.',
component: <LoginWithPasskeys />,
products: [LoginProducts.PASSKEYS],
code: `const loginConfig: StytchLoginConfig =
sessionOptions: {
sessionDurationMinutes: 60,
},
products: [Products.passkeys, Products.otp],
otpOptions: {
expirationMinutes: 10,
methods: [OTPMethods.Email],
},
};
const LoginWithPasskeys = () => {
const { user } = useStytchUser();
const router = useRouter();
if (user) {
router.push('/recipes/passkeys/profile');
}
return <StytchLogin config={loginConfig} callbacks={callbackConfig} />;`,
},
ONETAP: {
ONETAP: {
id: 'onetap',
title: 'Floating Google One Tap',
details:
'Render Google One Tap in a floating manner on your webpages, and nudge users down the login/signup flow from anywhere in your user experience.',
'Render Google One Tap in a floating manner on your webpages, and nudge users down the login/signup flow from anywhere in your user experience.',
description: `This authentication method can be used as a standalone login/signup method, or paired with other login methods such as email magic links.`,
instructions: `Google One Tap is powered through an iframe that Google provides compared to the traditional OAuth flow of redirecting the user to a separate Google page. As a result, the user can click directly on their desired account to login or create an account - hence, a “One Tap” experience. In the top right hand corner of this page you'll see the Stytch UI configured for Google One Tap if you have any active Chrome sessions in your browser.`,
component: <LoginWithOneTap />,
Expand Down
Loading

1 comment on commit 6b660e5

@vercel
Copy link

@vercel vercel bot commented on 6b660e5 Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.