Skip to content

Commit 9602069

Browse files
authored
Merge pull request #12 from auth0-samples/release-v3
Added support for release-3 screens
2 parents 239dbfb + a294030 commit 9602069

File tree

21 files changed

+2859
-303
lines changed

21 files changed

+2859
-303
lines changed

package-lock.json

Lines changed: 1649 additions & 301 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
8-
"build": "tsc -b && vite build",
8+
"build": "tsc -b && vite build --watch",
99
"lint": "eslint .",
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13-
"@auth0/auth0-acul-js": "^0.1.0-beta.1",
13+
"@auth0/auth0-acul-js": "^0.1.0-beta.2",
1414
"react": "^18.3.1",
1515
"react-dom": "^18.3.1",
1616
"react-error-boundary": "^4.1.2"
@@ -20,11 +20,14 @@
2020
"@types/react": "^18.3.12",
2121
"@types/react-dom": "^18.3.1",
2222
"@vitejs/plugin-react": "^4.3.3",
23+
"autoprefixer": "^10.4.20",
2324
"eslint": "^9.13.0",
2425
"eslint-plugin-react-hooks": "^5.0.0",
2526
"eslint-plugin-react-refresh": "^0.4.14",
2627
"globals": "^15.11.0",
28+
"postcss": "^8.5.2",
2729
"sass-embedded": "1.80.0",
30+
"tailwindcss": "^3.4.17",
2831
"typescript": "~5.6.2",
2932
"typescript-eslint": "^8.11.0",
3033
"vite": "^5.4.10"

postcss.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}

src/App.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ const ResetPasswordEmail = React.lazy(() => import("./screens/ResetPasswordEmail
1010
const ResetPassword = React.lazy(() => import("./screens/ResetPassword"));
1111
const ResetPasswordError = React.lazy(() => import("./screens/ResetPasswordError"));
1212
const ResetPasswordSuccess = React.lazy(() => import("./screens/ResetPasswordSuccess"));
13+
const MfaSmsChallengeScreen = React.lazy(() => import("./screens/mfa-sms-challenge"));
14+
const MfaBeginEnrollOptionsScreen = React.lazy(() => import("./screens/mfa-begin-enroll-options"));
15+
const MfaPushWelcomeScreen = React.lazy(() => import("./screens/mfa-push-welcome"));
16+
const MFASmsEnrollmentScreen = React.lazy(() => import("./screens/mfa-sms-enrollment"));
17+
const MfaCountryCodesScreen = React.lazy(() => import("./screens/mfa-country-codes"));
18+
const MfaPushEnrollmentQrScreen = React.lazy(() => import("./screens/mfa-push-enrollment-qr"));
19+
const MFASmsListScreen = React.lazy(() => import("./screens/mfa-sms-list"));
20+
const MfaEmailChallengeScreen = React.lazy(() => import("./screens/mfa-email-challenge"));
21+
const MfaDetectBrowserCapabilitiesScreen = React.lazy(() => import("./screens/mfa-detect-browser-capabilities"));
22+
const MfaLoginOptionsScreen = React.lazy(() => import("./screens/mfa-login-options"));
23+
const MfaEmailListScreen = React.lazy(() => import("./screens/mfa-email-list"));
24+
const MfaPushChallengePushScreen = React.lazy(() => import("./screens/mfa-push-challenge-push"));
25+
const MfaEnrollResultScreen = React.lazy(() => import("./screens/mfa-enroll-result"));
26+
const MfaPushListScreen = React.lazy(() => import("./screens/mfa-push-list"));
27+
1328

1429
const App: React.FC = () => {
1530
const [screen, setScreen] = React.useState("login-id");
@@ -38,6 +53,34 @@ const App: React.FC = () => {
3853
return <ResetPasswordError />;
3954
case "reset-password-success":
4055
return <ResetPasswordSuccess />;
56+
case "mfa-begin-enroll-options":
57+
return <MfaBeginEnrollOptionsScreen />
58+
case "mfa-sms-challenge":
59+
return <MfaSmsChallengeScreen />;
60+
case "mfa-sms-enrollment":
61+
return <MFASmsEnrollmentScreen />;
62+
case "mfa-push-welcome":
63+
return <MfaPushWelcomeScreen />;
64+
case "mfa-country-codes":
65+
return <MfaCountryCodesScreen />;
66+
case "mfa-push-enrollment-qr":
67+
return <MfaPushEnrollmentQrScreen />;
68+
case "mfa-sms-list":
69+
return <MFASmsListScreen />;
70+
case "mfa-email-challenge":
71+
return <MfaEmailChallengeScreen />;
72+
case "mfa-detect-browser-capabilities":
73+
return <MfaDetectBrowserCapabilitiesScreen />;
74+
case "mfa-login-options":
75+
return <MfaLoginOptionsScreen />;
76+
case "mfa-email-list":
77+
return <MfaEmailListScreen />;
78+
case "mfa-push-challenge-push":
79+
return <MfaPushChallengePushScreen />;
80+
case "mfa-enroll-result":
81+
return <MfaEnrollResultScreen />;
82+
case "mfa-push-list":
83+
return <MfaPushListScreen />;
4184
default:
4285
return <>No screen rendered</>;
4386
}

src/index.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;

src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { StrictMode } from "react";
22
import { createRoot } from "react-dom/client";
33
import App from "./App.tsx";
44
import { ErrorBoundary } from "react-error-boundary";
5+
import './index.css';
56

67
const rootElement = document.createElement("div");
78
rootElement.id = "root";
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useCallback } from 'react';
2+
import MfaBeginEnrollOptions, { type FactorType } from '@auth0/auth0-acul-js/mfa-begin-enroll-options';
3+
4+
/** Enum for Factor Types */
5+
enum FactorTypeEnum {
6+
PUSH_NOTIFICATION = 'push-notification',
7+
OTP = 'otp',
8+
SMS = 'sms',
9+
PHONE = 'phone',
10+
VOICE = 'voice',
11+
WEBAUTHN_ROAMING = 'webauthn-roaming'
12+
}
13+
14+
const MfaBeginEnrollOptionsScreen: React.FC = () => {
15+
const mfaBeginEnrollOptions = new MfaBeginEnrollOptions();
16+
const { tenant, screen: { texts } } = mfaBeginEnrollOptions;
17+
18+
/** Dynamically map factor IDs to display names */
19+
const factorDisplayNames: Record<FactorTypeEnum, string> = {
20+
[FactorTypeEnum.PUSH_NOTIFICATION]: texts?.authenticatorNamesPushNotification ?? 'Push Notification (Auth0 Guardian)',
21+
[FactorTypeEnum.OTP]: texts?.authenticatorNamesOTP ?? 'One-Time Password (Google Authenticator)',
22+
[FactorTypeEnum.SMS]: texts?.authenticatorNamesSMS ?? 'SMS',
23+
[FactorTypeEnum.PHONE]: texts?.authenticatorNamesPhone ?? 'Phone Call',
24+
[FactorTypeEnum.VOICE]: texts?.authenticatorNamesVoice ?? 'Voice Call',
25+
[FactorTypeEnum.WEBAUTHN_ROAMING]: texts?.authenticatorNamesWebauthnRoaming ?? 'Security Key'
26+
};
27+
28+
/** Handles user selection of an MFA factor */
29+
const handleFactorSelection = useCallback(async (factor: FactorTypeEnum) => {
30+
try {
31+
await mfaBeginEnrollOptions.enroll({
32+
action: factor as FactorType
33+
});
34+
} catch (error) {
35+
console.error(`Error enrolling factor [${factor}]:`, error);
36+
}
37+
}, []);
38+
39+
return (
40+
<div className="min-h-screen bg-gray-100 flex flex-col flex-start py-12 sm:px-6 lg:px-8">
41+
<div className="sm:mx-auto sm:w-full sm:max-w-md">
42+
<h2 className="text-center text-3xl font-extrabold text-gray-900">
43+
{texts?.title ?? 'Multi-factor Authentication'}
44+
</h2>
45+
<p className="mt-2 text-center text-sm text-gray-600">
46+
{texts?.description ?? 'Choose a Multi-factor Authentication Method'}
47+
</p>
48+
</div>
49+
50+
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
51+
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
52+
<div className="space-y-4">
53+
{tenant.enabledFactors?.map((factor) => {
54+
const factorEnum = factor as FactorTypeEnum;
55+
return (
56+
<button
57+
key={factor}
58+
onClick={() => handleFactorSelection(factorEnum)}
59+
className="w-full flex justify-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-white hover:bg-gray-50 text-sm font-medium text-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
60+
>
61+
{factorDisplayNames[factorEnum]}
62+
</button>
63+
);
64+
})}
65+
</div>
66+
</div>
67+
</div>
68+
</div>
69+
);
70+
};
71+
72+
export default MfaBeginEnrollOptionsScreen;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import MfaCountryCodes from '@auth0/auth0-acul-js/mfa-country-codes';
3+
4+
const MfaCountryCodesScreen: React.FC = () => {
5+
const mfaCountryCodes = new MfaCountryCodes();
6+
const { screen } = mfaCountryCodes;
7+
const { phone_prefixes } = screen.data || {};
8+
const handleCountrySelect = async (countryCode: string, phonePrefix: string) => {
9+
try {
10+
await mfaCountryCodes.selectCountryCode({
11+
country_code: countryCode,
12+
phone_prefix: phonePrefix
13+
});
14+
} catch (error) {
15+
console.error('Failed to select country code:', error);
16+
}
17+
};
18+
19+
const handleGoBack = async () => {
20+
try {
21+
await mfaCountryCodes.goBack();
22+
} catch (error) {
23+
console.error('Failed to go back:', error);
24+
}
25+
};
26+
27+
return (
28+
<div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
29+
<div className="sm:mx-auto sm:w-full sm:max-w-md">
30+
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
31+
Select Your Country Code
32+
</h2>
33+
</div>
34+
35+
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
36+
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
37+
<div className="space-y-4">
38+
{phone_prefixes?.map((prefix, index) => (
39+
<button
40+
key={`${prefix.country_code}${index}`}
41+
onClick={() => handleCountrySelect(prefix.country_code, prefix.phone_prefix)}
42+
className="w-full flex justify-between items-center py-3 px-4 border border-gray-300 rounded-md shadow-sm bg-white hover:bg-gray-50 text-sm font-medium text-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
43+
>
44+
<span>{prefix.country}</span>
45+
<span className="text-gray-500">{prefix.phone_prefix}</span>
46+
</button>
47+
))}
48+
</div>
49+
50+
<div className="mt-6">
51+
<button
52+
onClick={handleGoBack}
53+
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
54+
>
55+
Go Back
56+
</button>
57+
</div>
58+
</div>
59+
</div>
60+
</div>
61+
);
62+
};
63+
64+
export default MfaCountryCodesScreen;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useEffect, useState } from 'react';
2+
import MfaDetectBrowserCapabilities from '@auth0/auth0-acul-js/mfa-detect-browser-capabilities';
3+
4+
const MfaDetectBrowserCapabilitiesScreen: React.FC = () => {
5+
const [isLoading, setIsLoading] = useState(true);
6+
const [error, setError] = useState<string | null>(null);
7+
const mfaDetectBrowserCapabilities = new MfaDetectBrowserCapabilities();
8+
9+
useEffect(() => {
10+
const detectCapabilities = async () => {
11+
try {
12+
setIsLoading(true);
13+
setError(null);
14+
15+
await mfaDetectBrowserCapabilities.detectCapabilities();
16+
} catch (err) {
17+
setError('Failed to detect browser capabilities. Please try again.');
18+
console.error('Error:', err);
19+
} finally {
20+
setIsLoading(false);
21+
}
22+
};
23+
24+
detectCapabilities();
25+
}, []);
26+
27+
if (isLoading) {
28+
return (
29+
<div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
30+
<div className="sm:mx-auto sm:w-full sm:max-w-md">
31+
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
32+
<div className="flex justify-center">
33+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" />
34+
</div>
35+
<p className="mt-4 text-center text-sm text-gray-600">
36+
Detecting browser capabilities...
37+
</p>
38+
</div>
39+
</div>
40+
</div>
41+
);
42+
}
43+
44+
if (error) {
45+
return (
46+
<div className="min-h-screen bg-gray-100 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
47+
<div className="sm:mx-auto sm:w-full sm:max-w-md">
48+
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
49+
<div className="rounded-md bg-red-50 p-4">
50+
<div className="flex">
51+
<div className="flex-shrink-0">
52+
<svg
53+
className="h-5 w-5 text-red-400"
54+
xmlns="http://www.w3.org/2000/svg"
55+
viewBox="0 0 20 20"
56+
fill="currentColor"
57+
>
58+
<path
59+
fillRule="evenodd"
60+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
61+
clipRule="evenodd"
62+
/>
63+
</svg>
64+
</div>
65+
<div className="ml-3">
66+
<h3 className="text-sm font-medium text-red-800">{error}</h3>
67+
</div>
68+
</div>
69+
</div>
70+
</div>
71+
</div>
72+
</div>
73+
);
74+
}
75+
76+
return null; // The screen will automatically redirect after successful capability detection
77+
};
78+
79+
export default MfaDetectBrowserCapabilitiesScreen;

0 commit comments

Comments
 (0)