From d2843782bd143852b1bbf4fe492a934d715a0242 Mon Sep 17 00:00:00 2001 From: Jake Whelan Date: Fri, 1 Mar 2024 19:37:15 +0000 Subject: [PATCH 1/5] passkey-detect --- packages/react/package.json | 6 +- .../src/components/form/initial/controls.tsx | 21 +++++- packages/react/src/dev.tsx | 74 +++++++++---------- packages/react/src/main.ts | 1 + packages/react/src/passkey.ts | 5 ++ pnpm-lock.yaml | 31 +++++++- 6 files changed, 94 insertions(+), 44 deletions(-) create mode 100644 packages/react/src/passkey.ts diff --git a/packages/react/package.json b/packages/react/package.json index 58946ada..004a983a 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@slashid/react", - "version": "1.19.0", + "version": "1.20.0-next.0", "private": false, "publishConfig": { "access": "public" @@ -65,7 +65,7 @@ }, "devDependencies": { "@faker-js/faker": "^8.0.2", - "@slashid/slashid": "3.18.2", + "@slashid/slashid": "3.19.1-next.10", "@storybook/addon-essentials": "7.0.0-rc.2", "@storybook/addon-interactions": "7.4.0", "@storybook/addon-links": "7.4.0", @@ -100,7 +100,7 @@ "yalc": "1.0.0-pre.53" }, "peerDependencies": { - "@slashid/slashid": ">= 3.18.2", + "@slashid/slashid": "3.19.1-next.10", "react": ">=16", "react-dom": ">=16" } diff --git a/packages/react/src/components/form/initial/controls.tsx b/packages/react/src/components/form/initial/controls.tsx index 01bd9857..5628c87b 100644 --- a/packages/react/src/components/form/initial/controls.tsx +++ b/packages/react/src/components/form/initial/controls.tsx @@ -24,6 +24,7 @@ import { useForm } from "../../../hooks/use-form"; import { TextConfig, TextConfigKey } from "../../text/constants"; import { ErrorMessage } from "../error-message"; import { isValidEmail, isValidPhoneNumber } from "../authenticating/validation"; +import { passkeysSupported } from '../../../passkey' import * as styles from "./initial.css"; import { useInternalFormContext } from "../internal-context"; @@ -172,16 +173,32 @@ const FormInput = ({ children }: FormInputProps) => { const { lastHandle } = useInternalFormContext(); const { factors, text } = useConfiguration(); + const [showPasskeys, setShowPasskeys] = useState(null) + useEffect(() => { + (async () => { + const isSupported = await passkeysSupported + setShowPasskeys(isSupported) + console.log('passkey support', isSupported) + })() + }, []) + // @ts-expect-error TODO fix inference const nonOidcFactors: FactorNonOIDC[] = useMemo( - () => factors.filter((f) => isFactorNonOidc(f)), - [factors] + () => factors.filter((f) => { + if (f.method === "webauthn" && showPasskeys === false) return false + return isFactorNonOidc(f) + }), + [factors, showPasskeys] ); const handleTypes = useMemo(() => { return getHandleTypes(factors); }, [factors]); + if (showPasskeys === null) { + return null + } + if (typeof children === "function") { return <>{children({ factors: nonOidcFactors, handleTypes })}; } diff --git a/packages/react/src/dev.tsx b/packages/react/src/dev.tsx index 84f012c1..81c4afa0 100644 --- a/packages/react/src/dev.tsx +++ b/packages/react/src/dev.tsx @@ -1,10 +1,10 @@ import { type Factor } from "@slashid/slashid"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import ReactDOM from "react-dom/client"; import { GDPRConsentDialog } from "./components/gdpr-consent-dialog"; import "./dev.css"; -import { FactorConfiguration, Handle } from "./domain/types"; +import { Handle } from "./domain/types"; import { ConfigurationProvider, DynamicFlow, @@ -34,46 +34,46 @@ const initialFactors: Factor[] = [ }, ]; -const withWan: FactorConfiguration[] = [ - { method: "webauthn", options: { attachment: "platform" } }, - { method: "email_link" }, - { method: "otp_via_sms" }, - { - method: "oidc", - options: { - provider: "google", - client_id: import.meta.env.VITE_GOOGLE_SSO_CLIENT_ID ?? "test_oidc_1", - }, - }, - { - method: "oidc", - label: "Google SSO - label test", - options: { - provider: "google", - client_id: "test_oidc_2", - }, - }, -]; +// const withWan: FactorConfiguration[] = [ +// { method: "webauthn", options: { attachment: "platform" } }, +// { method: "email_link" }, +// { method: "otp_via_sms" }, +// { +// method: "oidc", +// options: { +// provider: "google", +// client_id: import.meta.env.VITE_GOOGLE_SSO_CLIENT_ID ?? "test_oidc_1", +// }, +// }, +// { +// method: "oidc", +// label: "Google SSO - label test", +// options: { +// provider: "google", +// client_id: "test_oidc_2", +// }, +// }, +// ]; function Config() { const { currentOrganization } = useOrganizations(); - const [factors, setFactors] = useState(initialFactors); + const [factors] = useState(initialFactors); - useEffect(() => { - const checkPlatformAuthenticator = async () => { - try { - const hasPlatformAuthenticator = - await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); - if (hasPlatformAuthenticator) { - setFactors(withWan); - } - } catch (e) { - console.log(e); - } - }; + // useEffect(() => { + // const checkPlatformAuthenticator = async () => { + // try { + // const hasPlatformAuthenticator = + // await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); + // if (hasPlatformAuthenticator) { + // setFactors(withWan); + // } + // } catch (e) { + // console.log(e); + // } + // }; - checkPlatformAuthenticator(); - }, []); + // checkPlatformAuthenticator(); + // }, []); return ( diff --git a/packages/react/src/main.ts b/packages/react/src/main.ts index b3bbaa7b..767cb726 100644 --- a/packages/react/src/main.ts +++ b/packages/react/src/main.ts @@ -1,3 +1,4 @@ +import "./passkey" // start this loading ASAP import { DynamicFlow } from "./components/dynamic-flow"; import { Form } from "./components/form"; import { Slot } from "./components/slot"; diff --git a/packages/react/src/passkey.ts b/packages/react/src/passkey.ts new file mode 100644 index 00000000..6f03b452 --- /dev/null +++ b/packages/react/src/passkey.ts @@ -0,0 +1,5 @@ +import { Utils } from '@slashid/slashid' + +export const passkeysSupported = Utils.runtimeSupportsBestPasskeyUX() + +console.log('loading!') \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d8cad21..c0db9905 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -370,8 +370,8 @@ importers: specifier: ^8.0.2 version: 8.3.1 '@slashid/slashid': - specifier: 3.18.2 - version: 3.18.2 + specifier: 3.19.1-next.10 + version: 3.19.1-next.10 '@storybook/addon-essentials': specifier: 7.0.0-rc.2 version: 7.0.0-rc.2(react-dom@18.2.0)(react@18.2.0) @@ -8659,6 +8659,24 @@ packages: regenerator-runtime: 0.13.11 url: 0.11.3 uuid: 8.3.2 + dev: false + + /@slashid/slashid@3.19.1-next.10: + resolution: {integrity: sha512-wp+F7qdZi1UJZINX27WQwJ/DGleWzmb7KEcM4jOG7itDmi3UqEtvTpbVRKF8EtMzDuTFcsvZ/N53RdWwwbarCA==} + dependencies: + '@changesets/cli': 2.26.2 + '@types/uuid': 8.3.4 + changeset: 0.2.6 + compare-versions: 6.1.0 + docdash: 1.2.0 + jwt-decode: 3.1.2 + qrcode: 1.5.3 + querystring-es3: 0.2.1 + regenerator-runtime: 0.13.11 + ua-parser-js: 1.0.37 + url: 0.11.3 + uuid: 8.3.2 + dev: true /@storybook/addon-actions@7.0.0-rc.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-e4VXOoeejRnz+yTtPGtenAvrMtwRWO4VRxbGBHDPyMo+FOlQPC6plOrQMZ+lSRaE20J3KfTo6wSLZgnHQUQtRw==} @@ -12280,6 +12298,10 @@ packages: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true + /compare-versions@6.1.0: + resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} + dev: true + /compress.js@1.2.2: resolution: {integrity: sha512-eIsMmzyBtPVDdPtpmPXTRB6uiLjgTXygYl0KCVVp0I/U/++YzZtEkhVTuWH97dzTxKvzAcZMFwYUHGC09BSUqg==} dependencies: @@ -18347,6 +18369,7 @@ packages: /qrcode@1.5.3: resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} engines: {node: '>=10.13.0'} + hasBin: true dependencies: dijkstrajs: 1.0.3 encode-utf8: 1.0.3 @@ -20420,6 +20443,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + /ua-parser-js@1.0.37: + resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + dev: true + /udc@1.0.1: resolution: {integrity: sha512-jv+D9de1flsum5QkFtBdjyppCQAdz9kTck/0xST5Vx48T9LL2BYnw0Iw77dSKDQ9KZ/PS3qPO1vfXHDpLZlxcQ==} From b83d6756b4df5e171f76db751affc59855d551da Mon Sep 17 00:00:00 2001 From: Jake Whelan Date: Mon, 4 Mar 2024 08:08:16 +0000 Subject: [PATCH 2/5] remove debug --- packages/react/package.json | 2 +- .../src/components/form/initial/controls.tsx | 8 +- packages/react/src/dev.tsx | 74 +++++++++---------- packages/react/src/passkey.ts | 4 +- 4 files changed, 43 insertions(+), 45 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 004a983a..64331eb4 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@slashid/react", - "version": "1.20.0-next.0", + "version": "1.19.0", "private": false, "publishConfig": { "access": "public" diff --git a/packages/react/src/components/form/initial/controls.tsx b/packages/react/src/components/form/initial/controls.tsx index 5628c87b..5f03cd21 100644 --- a/packages/react/src/components/form/initial/controls.tsx +++ b/packages/react/src/components/form/initial/controls.tsx @@ -24,7 +24,7 @@ import { useForm } from "../../../hooks/use-form"; import { TextConfig, TextConfigKey } from "../../text/constants"; import { ErrorMessage } from "../error-message"; import { isValidEmail, isValidPhoneNumber } from "../authenticating/validation"; -import { passkeysSupported } from '../../../passkey' +import { runtimeSupportsPasskeys } from '../../../passkey' import * as styles from "./initial.css"; import { useInternalFormContext } from "../internal-context"; @@ -176,9 +176,8 @@ const FormInput = ({ children }: FormInputProps) => { const [showPasskeys, setShowPasskeys] = useState(null) useEffect(() => { (async () => { - const isSupported = await passkeysSupported + const isSupported = await runtimeSupportsPasskeys setShowPasskeys(isSupported) - console.log('passkey support', isSupported) })() }, []) @@ -195,7 +194,8 @@ const FormInput = ({ children }: FormInputProps) => { return getHandleTypes(factors); }, [factors]); - if (showPasskeys === null) { + const passkeySupportDetectionLoading = showPasskeys === null + if (passkeySupportDetectionLoading) { return null } diff --git a/packages/react/src/dev.tsx b/packages/react/src/dev.tsx index 81c4afa0..84f012c1 100644 --- a/packages/react/src/dev.tsx +++ b/packages/react/src/dev.tsx @@ -1,10 +1,10 @@ import { type Factor } from "@slashid/slashid"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; import { GDPRConsentDialog } from "./components/gdpr-consent-dialog"; import "./dev.css"; -import { Handle } from "./domain/types"; +import { FactorConfiguration, Handle } from "./domain/types"; import { ConfigurationProvider, DynamicFlow, @@ -34,46 +34,46 @@ const initialFactors: Factor[] = [ }, ]; -// const withWan: FactorConfiguration[] = [ -// { method: "webauthn", options: { attachment: "platform" } }, -// { method: "email_link" }, -// { method: "otp_via_sms" }, -// { -// method: "oidc", -// options: { -// provider: "google", -// client_id: import.meta.env.VITE_GOOGLE_SSO_CLIENT_ID ?? "test_oidc_1", -// }, -// }, -// { -// method: "oidc", -// label: "Google SSO - label test", -// options: { -// provider: "google", -// client_id: "test_oidc_2", -// }, -// }, -// ]; +const withWan: FactorConfiguration[] = [ + { method: "webauthn", options: { attachment: "platform" } }, + { method: "email_link" }, + { method: "otp_via_sms" }, + { + method: "oidc", + options: { + provider: "google", + client_id: import.meta.env.VITE_GOOGLE_SSO_CLIENT_ID ?? "test_oidc_1", + }, + }, + { + method: "oidc", + label: "Google SSO - label test", + options: { + provider: "google", + client_id: "test_oidc_2", + }, + }, +]; function Config() { const { currentOrganization } = useOrganizations(); - const [factors] = useState(initialFactors); + const [factors, setFactors] = useState(initialFactors); - // useEffect(() => { - // const checkPlatformAuthenticator = async () => { - // try { - // const hasPlatformAuthenticator = - // await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); - // if (hasPlatformAuthenticator) { - // setFactors(withWan); - // } - // } catch (e) { - // console.log(e); - // } - // }; + useEffect(() => { + const checkPlatformAuthenticator = async () => { + try { + const hasPlatformAuthenticator = + await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); + if (hasPlatformAuthenticator) { + setFactors(withWan); + } + } catch (e) { + console.log(e); + } + }; - // checkPlatformAuthenticator(); - // }, []); + checkPlatformAuthenticator(); + }, []); return ( diff --git a/packages/react/src/passkey.ts b/packages/react/src/passkey.ts index 6f03b452..253e1dd4 100644 --- a/packages/react/src/passkey.ts +++ b/packages/react/src/passkey.ts @@ -1,5 +1,3 @@ import { Utils } from '@slashid/slashid' -export const passkeysSupported = Utils.runtimeSupportsBestPasskeyUX() - -console.log('loading!') \ No newline at end of file +export const runtimeSupportsPasskeys = Utils.runtimeSupportsBestPasskeyUX() From 192ea3f1cf9a222ab5b14c0411d35ce9eeb5f0b6 Mon Sep 17 00:00:00 2001 From: Jake Whelan Date: Mon, 4 Mar 2024 08:09:15 +0000 Subject: [PATCH 3/5] changeset --- .changeset/funny-snails-double.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/funny-snails-double.md diff --git a/.changeset/funny-snails-double.md b/.changeset/funny-snails-double.md new file mode 100644 index 00000000..89ecb118 --- /dev/null +++ b/.changeset/funny-snails-double.md @@ -0,0 +1,5 @@ +--- +"@slashid/react": minor +--- + +Automatically hide passkey option if platform or browser does not support good passkey UX From 711fa42e19828fcae3191e5bd93ca34a1fa230a0 Mon Sep 17 00:00:00 2001 From: Jake Whelan Date: Mon, 4 Mar 2024 08:09:46 +0000 Subject: [PATCH 4/5] fmt --- .../src/components/form/initial/controls.tsx | 25 ++++++++++--------- packages/react/src/main.ts | 2 +- packages/react/src/passkey.ts | 4 +-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/react/src/components/form/initial/controls.tsx b/packages/react/src/components/form/initial/controls.tsx index 5f03cd21..68bd60bf 100644 --- a/packages/react/src/components/form/initial/controls.tsx +++ b/packages/react/src/components/form/initial/controls.tsx @@ -24,7 +24,7 @@ import { useForm } from "../../../hooks/use-form"; import { TextConfig, TextConfigKey } from "../../text/constants"; import { ErrorMessage } from "../error-message"; import { isValidEmail, isValidPhoneNumber } from "../authenticating/validation"; -import { runtimeSupportsPasskeys } from '../../../passkey' +import { runtimeSupportsPasskeys } from "../../../passkey"; import * as styles from "./initial.css"; import { useInternalFormContext } from "../internal-context"; @@ -173,20 +173,21 @@ const FormInput = ({ children }: FormInputProps) => { const { lastHandle } = useInternalFormContext(); const { factors, text } = useConfiguration(); - const [showPasskeys, setShowPasskeys] = useState(null) + const [showPasskeys, setShowPasskeys] = useState(null); useEffect(() => { (async () => { - const isSupported = await runtimeSupportsPasskeys - setShowPasskeys(isSupported) - })() - }, []) + const isSupported = await runtimeSupportsPasskeys; + setShowPasskeys(isSupported); + })(); + }, []); // @ts-expect-error TODO fix inference const nonOidcFactors: FactorNonOIDC[] = useMemo( - () => factors.filter((f) => { - if (f.method === "webauthn" && showPasskeys === false) return false - return isFactorNonOidc(f) - }), + () => + factors.filter((f) => { + if (f.method === "webauthn" && showPasskeys === false) return false; + return isFactorNonOidc(f); + }), [factors, showPasskeys] ); @@ -194,9 +195,9 @@ const FormInput = ({ children }: FormInputProps) => { return getHandleTypes(factors); }, [factors]); - const passkeySupportDetectionLoading = showPasskeys === null + const passkeySupportDetectionLoading = showPasskeys === null; if (passkeySupportDetectionLoading) { - return null + return null; } if (typeof children === "function") { diff --git a/packages/react/src/main.ts b/packages/react/src/main.ts index 767cb726..bcdbf122 100644 --- a/packages/react/src/main.ts +++ b/packages/react/src/main.ts @@ -1,4 +1,4 @@ -import "./passkey" // start this loading ASAP +import "./passkey"; // start this loading ASAP import { DynamicFlow } from "./components/dynamic-flow"; import { Form } from "./components/form"; import { Slot } from "./components/slot"; diff --git a/packages/react/src/passkey.ts b/packages/react/src/passkey.ts index 253e1dd4..ff9a3db4 100644 --- a/packages/react/src/passkey.ts +++ b/packages/react/src/passkey.ts @@ -1,3 +1,3 @@ -import { Utils } from '@slashid/slashid' +import { Utils } from "@slashid/slashid"; -export const runtimeSupportsPasskeys = Utils.runtimeSupportsBestPasskeyUX() +export const runtimeSupportsPasskeys = Utils.runtimeSupportsBestPasskeyUX(); From c92fbb14c54b98ef6a6ec9c0c30a76af5d30c539 Mon Sep 17 00:00:00 2001 From: Jake Whelan Date: Mon, 4 Mar 2024 15:45:59 +0000 Subject: [PATCH 5/5] update core sdk --- packages/react/package.json | 4 ++-- pnpm-lock.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 64331eb4..e993d4d0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@faker-js/faker": "^8.0.2", - "@slashid/slashid": "3.19.1-next.10", + "@slashid/slashid": "3.19.0", "@storybook/addon-essentials": "7.0.0-rc.2", "@storybook/addon-interactions": "7.4.0", "@storybook/addon-links": "7.4.0", @@ -100,7 +100,7 @@ "yalc": "1.0.0-pre.53" }, "peerDependencies": { - "@slashid/slashid": "3.19.1-next.10", + "@slashid/slashid": ">=3.19.0", "react": ">=16", "react-dom": ">=16" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0db9905..bcc6dea9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -370,8 +370,8 @@ importers: specifier: ^8.0.2 version: 8.3.1 '@slashid/slashid': - specifier: 3.19.1-next.10 - version: 3.19.1-next.10 + specifier: 3.19.0 + version: 3.19.0 '@storybook/addon-essentials': specifier: 7.0.0-rc.2 version: 7.0.0-rc.2(react-dom@18.2.0)(react@18.2.0) @@ -8661,8 +8661,8 @@ packages: uuid: 8.3.2 dev: false - /@slashid/slashid@3.19.1-next.10: - resolution: {integrity: sha512-wp+F7qdZi1UJZINX27WQwJ/DGleWzmb7KEcM4jOG7itDmi3UqEtvTpbVRKF8EtMzDuTFcsvZ/N53RdWwwbarCA==} + /@slashid/slashid@3.19.0: + resolution: {integrity: sha512-MG2vGh+q7vKxqJbs9gA4CrWVh2aXjw9mKk4A8k9gewts/L+AcNZHWTe73Y0QvQ1L7GJhgLB3+tHgGgCLC133Lg==} dependencies: '@changesets/cli': 2.26.2 '@types/uuid': 8.3.4