From 3ed13eeadc48e197620788e902e5bde6743d6f76 Mon Sep 17 00:00:00 2001 From: Mohammad Cheikh Date: Mon, 11 Nov 2024 16:11:54 -0500 Subject: [PATCH] working passkey separation --- package.json | 2 + .../{public => src/assets}/apple.svg | 0 packages/sdk-react/src/assets/checkbox.svg | 10 + packages/sdk-react/src/assets/clock.svg | 3 + packages/sdk-react/src/assets/email.svg | 3 + .../{public => src/assets}/facebook.svg | 0 packages/sdk-react/src/assets/faceid.svg | 3 + packages/sdk-react/src/assets/fingerprint.svg | 3 + .../{public => src/assets}/google.svg | 0 packages/sdk-react/src/assets/keyhole.svg | 10 + packages/sdk-react/src/assets/sms.svg | 3 + packages/sdk-react/src/assets/turnkey.svg | 12 + .../sdk-react/src/components/auth/Apple.tsx | 3 +- .../src/components/auth/Auth.module.css | 48 +++ .../sdk-react/src/components/auth/Auth.tsx | 287 +++++++----------- .../src/components/auth/Facebook.tsx | 5 +- .../sdk-react/src/components/auth/Google.tsx | 3 +- .../sdk-react/src/components/auth/index.ts | 1 + pnpm-lock.yaml | 38 ++- rollup.config.base.mjs | 16 +- 20 files changed, 264 insertions(+), 186 deletions(-) rename packages/sdk-react/{public => src/assets}/apple.svg (100%) create mode 100644 packages/sdk-react/src/assets/checkbox.svg create mode 100644 packages/sdk-react/src/assets/clock.svg create mode 100644 packages/sdk-react/src/assets/email.svg rename packages/sdk-react/{public => src/assets}/facebook.svg (100%) create mode 100644 packages/sdk-react/src/assets/faceid.svg create mode 100644 packages/sdk-react/src/assets/fingerprint.svg rename packages/sdk-react/{public => src/assets}/google.svg (100%) create mode 100644 packages/sdk-react/src/assets/keyhole.svg create mode 100644 packages/sdk-react/src/assets/sms.svg create mode 100644 packages/sdk-react/src/assets/turnkey.svg diff --git a/package.json b/package.json index d6af5937a..f6525f647 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "rollup-plugin-node-externals": "^6.1.2", "rollup-plugin-postcss": "^4.0.2", "rollup-preserve-directives": "^1.1.2", + "@rollup/plugin-alias": "5.1.1", + "@rollup/plugin-url":"8.0.2", "tsx": "^3.12.7", "typescript": "^5.1.4" }, diff --git a/packages/sdk-react/public/apple.svg b/packages/sdk-react/src/assets/apple.svg similarity index 100% rename from packages/sdk-react/public/apple.svg rename to packages/sdk-react/src/assets/apple.svg diff --git a/packages/sdk-react/src/assets/checkbox.svg b/packages/sdk-react/src/assets/checkbox.svg new file mode 100644 index 000000000..b8c065755 --- /dev/null +++ b/packages/sdk-react/src/assets/checkbox.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/sdk-react/src/assets/clock.svg b/packages/sdk-react/src/assets/clock.svg new file mode 100644 index 000000000..97c83c148 --- /dev/null +++ b/packages/sdk-react/src/assets/clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/sdk-react/src/assets/email.svg b/packages/sdk-react/src/assets/email.svg new file mode 100644 index 000000000..76f97cbb2 --- /dev/null +++ b/packages/sdk-react/src/assets/email.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/sdk-react/public/facebook.svg b/packages/sdk-react/src/assets/facebook.svg similarity index 100% rename from packages/sdk-react/public/facebook.svg rename to packages/sdk-react/src/assets/facebook.svg diff --git a/packages/sdk-react/src/assets/faceid.svg b/packages/sdk-react/src/assets/faceid.svg new file mode 100644 index 000000000..0b0e9cffe --- /dev/null +++ b/packages/sdk-react/src/assets/faceid.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/sdk-react/src/assets/fingerprint.svg b/packages/sdk-react/src/assets/fingerprint.svg new file mode 100644 index 000000000..567225f21 --- /dev/null +++ b/packages/sdk-react/src/assets/fingerprint.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/sdk-react/public/google.svg b/packages/sdk-react/src/assets/google.svg similarity index 100% rename from packages/sdk-react/public/google.svg rename to packages/sdk-react/src/assets/google.svg diff --git a/packages/sdk-react/src/assets/keyhole.svg b/packages/sdk-react/src/assets/keyhole.svg new file mode 100644 index 000000000..d0d179a8f --- /dev/null +++ b/packages/sdk-react/src/assets/keyhole.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/sdk-react/src/assets/sms.svg b/packages/sdk-react/src/assets/sms.svg new file mode 100644 index 000000000..6de1055f3 --- /dev/null +++ b/packages/sdk-react/src/assets/sms.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/sdk-react/src/assets/turnkey.svg b/packages/sdk-react/src/assets/turnkey.svg new file mode 100644 index 000000000..549b30a0c --- /dev/null +++ b/packages/sdk-react/src/assets/turnkey.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/sdk-react/src/components/auth/Apple.tsx b/packages/sdk-react/src/components/auth/Apple.tsx index f07e197c4..6bd38b526 100644 --- a/packages/sdk-react/src/components/auth/Apple.tsx +++ b/packages/sdk-react/src/components/auth/Apple.tsx @@ -3,6 +3,7 @@ import { sha256 } from "@noble/hashes/sha2"; import { bytesToHex } from "@noble/hashes/utils"; import AppleLogin from "react-apple-login"; import styles from "./Socials.module.css"; +import appleIcon from "assets/apple.svg"; interface AppleAuthButtonProps { iframePublicKey: string; clientId: string; @@ -51,7 +52,7 @@ const AppleAuthButton: React.FC = ({ responseMode="fragment" render={({ onClick }) => (
- + Continue with Apple
)} diff --git a/packages/sdk-react/src/components/auth/Auth.module.css b/packages/sdk-react/src/components/auth/Auth.module.css index c2c600203..c8b978bd2 100644 --- a/packages/sdk-react/src/components/auth/Auth.module.css +++ b/packages/sdk-react/src/components/auth/Auth.module.css @@ -271,3 +271,51 @@ button:disabled { color: #ff4c4c; font-size: 12px; } + +.passkeyIconContainer { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + margin: 16px 0; +} + +.rowsContainer { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + margin-top: 16px; + width: 95%; + margin-left: auto; + margin-right: auto; + margin-bottom: 32px; +} + +.row { + display: flex; + align-items: center; +} + +.rowIcon { + padding-right: 8px; +} + + +.noPasskeyLink { + color: var(--Blue-500, #4C48FF); + font-size: 0.9rem; + margin-top: 8px; + cursor: pointer; + align-items: center; + text-align: center; + display: inline-block; +} + +.passkeyContainer { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-top: 16px; +} diff --git a/packages/sdk-react/src/components/auth/Auth.tsx b/packages/sdk-react/src/components/auth/Auth.tsx index 4452ecfa2..5d8a52dec 100644 --- a/packages/sdk-react/src/components/auth/Auth.tsx +++ b/packages/sdk-react/src/components/auth/Auth.tsx @@ -15,6 +15,17 @@ import AppleAuthButton from "./Apple"; import FacebookAuthButton from "./Facebook"; import { CircularProgress } from "@mui/material"; import { parsePhoneNumberFromString } from 'libphonenumber-js'; +import turnkeyIcon from "assets/turnkey.svg" +import googleIcon from "assets/google.svg"; +import facebookIcon from "assets/facebook.svg"; +import appleIcon from "assets/apple.svg"; +import emailIcon from "assets/email.svg"; +import smsIcon from "assets/sms.svg"; +import faceidIcon from "assets/faceid.svg" +import fingerprintIcon from "assets/fingerprint.svg" +import checkboxIcon from "assets/checkbox.svg" +import clockIcon from "assets/clock.svg" +import keyholeIcon from "assets/keyhole.svg" interface AuthProps { onHandleAuthSuccess: () => Promise; @@ -39,7 +50,7 @@ const Auth: React.FC = ({ onHandleAuthSuccess, authConfig }) => { const [oauthLoading, setOauthLoading] = useState(""); const [suborgId, setSuborgId] = useState(""); const [resendText, setResendText] = useState("Re-send Code"); - const [firstTimePasskey, setFirstTimePasskey] = useState(""); + const [passkeySignupScreen, setPasskeySignupScreen] = useState(false); const otpInputRef = useRef(null); const formatPhoneNumber = (phone: string) => { @@ -97,67 +108,72 @@ const Auth: React.FC = ({ onHandleAuthSuccess, authConfig }) => { } }; - const handleLoginWithPasskey = async () => { - // Step 1: Try to retrieve the suborg by email - const getSuborgsResponse = await getSuborgs({ - filterType: "EMAIL", - filterValue: email, - }); - const existingSuborgId = getSuborgsResponse!.organizationIds[0]; + const handleSignupWithPasskey = async () => { + const siteInfo = `${window.location.href} - ${new Date().toLocaleString(undefined, { + year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' + })}`; + const { encodedChallenge, attestation } = + (await passkeyClient?.createUserPasskey({ + publicKey: { user: { name: siteInfo, displayName: siteInfo } }, + })) || {}; - if (existingSuborgId) { - // If a suborg exists, use it to create a read/write session without a new passkey - const sessionResponse = await passkeyClient?.createReadWriteSession({ - organizationId: existingSuborgId, - targetPublicKey: authIframeClient?.iframePublicKey!, - }); + if (encodedChallenge && attestation) { + // Use the generated passkey to create a new suborg + const createSuborgResponse = await createSuborg({ + email, + passkey: { + authenticatorName: "First Passkey", + challenge: encodedChallenge, + attestation, + }, + }); - if (sessionResponse?.credentialBundle) { - await handleAuthSuccess(sessionResponse.credentialBundle); - } else { - setError("Failed to complete passkey login."); - } + const suborgId = createSuborgResponse?.subOrganizationId; + console.log(suborgId) + }else { + setError("Failed to create user passkey."); + } + const sessionResponse = await passkeyClient?.createReadWriteSession({ targetPublicKey: authIframeClient?.iframePublicKey!,}) + if (sessionResponse?.credentialBundle) { + await handleAuthSuccess(sessionResponse.credentialBundle); } else { - // If no suborg exists, first create a user passkey - const { encodedChallenge, attestation } = - (await passkeyClient?.createUserPasskey({ - publicKey: { user: { name: email, displayName: email } }, - })) || {}; - - if (encodedChallenge && attestation) { - // Use the generated passkey to create a new suborg - const createSuborgResponse = await createSuborg({ - email, - passkey: { - authenticatorName: "First Passkey", - challenge: encodedChallenge, - attestation, - }, - }); + setError("Failed to complete passkey login."); + } +} + const handleLoginWithPasskey = async () => { + const sessionResponse = await passkeyClient?.createReadWriteSession({ targetPublicKey: authIframeClient?.iframePublicKey!,}) + if (sessionResponse?.credentialBundle) { + await handleAuthSuccess(sessionResponse.credentialBundle); + } else { + setError("Failed to complete passkey login."); + } + } + + // if (existingSuborgId) { + // // If a suborg exists, use it to create a read/write session without a new passkey + // const sessionResponse = await passkeyClient?.createReadWriteSession({ + // organizationId: existingSuborgId, + // targetPublicKey: authIframeClient?.iframePublicKey!, + // }); - const newSuborgId = createSuborgResponse?.subOrganizationId; + // if (sessionResponse?.credentialBundle) { + // await handleAuthSuccess(sessionResponse.credentialBundle); + // } else { + // setError("Failed to complete passkey login."); + // } + // } else { + // If no suborg exists, first create a user passkey - if (newSuborgId) { - // With the new suborg, create a read/write session - const newSessionResponse = - await passkeyClient?.createReadWriteSession({ - organizationId: newSuborgId, - targetPublicKey: authIframeClient?.iframePublicKey!, - }); - - if (newSessionResponse?.credentialBundle) { - await handleAuthSuccess(newSessionResponse.credentialBundle); - } else { - setError("Failed to complete passkey login with new suborg."); - } - } else { - setError("Failed to create suborg with passkey."); - } - } else { - setError("Failed to create user passkey."); - } - } - }; + // } else { + // setError("Failed to complete passkey login with new suborg."); + // } + // } else { + // setError("Failed to create suborg with passkey."); + // } + // } else { + // setError("Failed to create user passkey."); + // } + // } const handleOtpLogin = async ( type: "EMAIL" | "PHONE_NUMBER", @@ -252,62 +268,48 @@ const Auth: React.FC = ({ onHandleAuthSuccess, authConfig }) => { className={styles.circularProgress} /> {oauthLoading === "Google" && ( - + )} {oauthLoading === "Facebook" && ( - + )} {oauthLoading === "Apple" && ( - + )}
Powered by - - - - - - - - - - - - - - +
- ) : ( + ) : passkeySignupScreen ?
+ + +
+

Secure your account with a passkey

+ +
+
+ + Log in with Touch ID, Face ID, or a security key +
+
+ + More secure than a password +
+
+ + Takes seconds to set up and use +
+ +
+ +
: (

{otpId ? "Enter verification code" : "Log in or sign up"}

@@ -340,16 +342,14 @@ const Auth: React.FC = ({ onHandleAuthSuccess, authConfig }) => {
} {authConfig.passkeyEnabled && !otpId && ( -
+
- +
setPasskeySignupScreen(true)}>I don't have a passkey
)} {!otpId && @@ -386,35 +386,9 @@ const Auth: React.FC = ({ onHandleAuthSuccess, authConfig }) => {
{step === "otpEmail" ? ( - - - + ) : ( - - - + )}
@@ -526,48 +500,7 @@ const Auth: React.FC = ({ onHandleAuthSuccess, authConfig }) => {
window.location.href = "https://www.turnkey.com/"} className={styles.poweredBy}> Secured by - - - - - - - - - - - - - - +
)} diff --git a/packages/sdk-react/src/components/auth/Facebook.tsx b/packages/sdk-react/src/components/auth/Facebook.tsx index 5a7022480..a75d08e49 100644 --- a/packages/sdk-react/src/components/auth/Facebook.tsx +++ b/packages/sdk-react/src/components/auth/Facebook.tsx @@ -5,7 +5,7 @@ import styles from "./Socials.module.css"; import { exchangeCodeForToken, generateChallengePair } from "./facebook-utils"; import { sha256 } from "@noble/hashes/sha256"; import { bytesToHex } from "@noble/hashes/utils"; - +import facebookIcon from "assets/facebook.svg"; interface FacebookAuthButtonProps { iframePublicKey: string; clientId: string; @@ -93,8 +93,7 @@ const FacebookAuthButton: React.FC = ({ return (
- {/* */} - + Continue with Facebook
); diff --git a/packages/sdk-react/src/components/auth/Google.tsx b/packages/sdk-react/src/components/auth/Google.tsx index ff94bca50..68c9361b7 100644 --- a/packages/sdk-react/src/components/auth/Google.tsx +++ b/packages/sdk-react/src/components/auth/Google.tsx @@ -2,6 +2,7 @@ import { GoogleOAuthProvider } from "@react-oauth/google"; import { sha256 } from "@noble/hashes/sha2"; import { bytesToHex } from "@noble/hashes/utils"; import styles from "./Socials.module.css"; +import googleIcon from "assets/google.svg"; interface GoogleAuthButtonProps { iframePublicKey: string; @@ -34,7 +35,7 @@ const GoogleAuthButton: React.FC = ({ return (
- + Continue with Google
diff --git a/packages/sdk-react/src/components/auth/index.ts b/packages/sdk-react/src/components/auth/index.ts index 4adba6e6c..91a0e7ed3 100644 --- a/packages/sdk-react/src/components/auth/index.ts +++ b/packages/sdk-react/src/components/auth/index.ts @@ -1 +1,2 @@ export { default as Auth } from "./Auth"; +export * from './OtpInput' \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adb747536..6772d5e87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,9 +28,15 @@ importers: '@jest/types': specifier: ^29.3.1 version: 29.4.3 + '@rollup/plugin-alias': + specifier: 5.1.1 + version: 5.1.1(rollup@4.22.4) '@rollup/plugin-typescript': specifier: ^11.1.5 version: 11.1.5(rollup@4.22.4)(typescript@5.1.5) + '@rollup/plugin-url': + specifier: 8.0.2 + version: 8.0.2(rollup@4.22.4) '@tsconfig/node16-strictest': specifier: ^1.0.4 version: 1.0.4 @@ -6924,10 +6930,10 @@ packages: dependencies: '@babel/runtime': 7.26.0 '@emotion/cache': 11.13.1 - '@emotion/react': 11.13.3(@types/react@18.2.75)(react@18.2.0) + '@emotion/react': 11.13.3(@types/react@18.2.14)(react@18.2.0) '@emotion/serialize': 1.3.2 '@emotion/sheet': 1.4.0 - '@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.2.75)(react@18.2.0) + '@emotion/styled': 11.13.0(@emotion/react@11.13.3)(@types/react@18.2.14)(react@18.2.0) csstype: 3.1.3 prop-types: 15.8.1 react: 18.2.0 @@ -10215,6 +10221,18 @@ packages: - supports-color dev: false + /@rollup/plugin-alias@5.1.1(rollup@4.22.4): + resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + rollup: 4.22.4 + dev: true + /@rollup/plugin-typescript@11.1.5(rollup@4.22.4)(typescript@5.1.5): resolution: {integrity: sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==} engines: {node: '>=14.0.0'} @@ -10234,6 +10252,21 @@ packages: typescript: 5.1.5 dev: true + /@rollup/plugin-url@8.0.2(rollup@4.22.4): + resolution: {integrity: sha512-5yW2LP5NBEgkvIRSSEdJkmxe5cUNZKG3eenKtfJvSkxVm/xTTu7w+ayBtNwhozl1ZnTUCU0xFaRQR+cBl2H7TQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.5(rollup@4.22.4) + make-dir: 3.1.0 + mime: 3.0.0 + rollup: 4.22.4 + dev: true + /@rollup/pluginutils@5.0.5(rollup@4.22.4): resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} @@ -19704,7 +19737,6 @@ packages: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true - dev: false /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} diff --git a/rollup.config.base.mjs b/rollup.config.base.mjs index ad9c7be67..794c7899c 100644 --- a/rollup.config.base.mjs +++ b/rollup.config.base.mjs @@ -2,10 +2,13 @@ import typescript from "@rollup/plugin-typescript"; import nodeExternals from "rollup-plugin-node-externals"; import path from "node:path"; import postcss from 'rollup-plugin-postcss'; -import preserveDirectives from 'rollup-preserve-directives' +import preserveDirectives from 'rollup-preserve-directives'; +import url from '@rollup/plugin-url'; +import alias from '@rollup/plugin-alias'; const getFormatConfig = (format) => { const pkgPath = path.join(process.cwd(), "package.json"); + const __dirname = path.dirname(new URL(import.meta.url).pathname); return { input: 'src/index.ts', @@ -17,6 +20,11 @@ const getFormatConfig = (format) => { sourcemap: true, }, plugins: [ + alias({ + entries: [ + { find: 'assets', replacement: path.resolve(__dirname, 'packages/sdk-react/src/assets') } + ] + }), postcss({ modules: true, extensions: ['.css', '.scss'], @@ -40,6 +48,12 @@ const getFormatConfig = (format) => { packagePath: pkgPath, builtinsPrefix: 'ignore', }), + url({ + include: ['**/*.svg', '**/*.png', '**/*.jpg', '**/*.gif'], + limit: 8192, + emitFiles: true, + fileName: '[name].[hash][extname]', + }), ], }; };