diff --git a/examples/with-account-linking/backend/config.ts b/examples/with-account-linking/backend/config.ts index 0848c681a..5f9017cad 100644 --- a/examples/with-account-linking/backend/config.ts +++ b/examples/with-account-linking/backend/config.ts @@ -20,6 +20,12 @@ export function getWebsiteDomain() { return websiteUrl; } +let otp = ""; + +export function getOtp() { + return otp; +} + export const SuperTokensConfig: TypeInput = { supertokens: { // this is the location of the SuperTokens core. @@ -44,18 +50,6 @@ export const SuperTokensConfig: TypeInput = { }; } - if (newAccountInfo.recipeUserId !== undefined && user !== undefined) { - let userId = newAccountInfo.recipeUserId.getAsString(); - let hasInfoAssociatedWithUserId = false; // TODO: add your own implementation here. - if (hasInfoAssociatedWithUserId) { - return { - // Alternatively, you can link users but then you should provide an `onAccountLinked` callback - // that implements merging the user of the two users. - shouldAutomaticallyLink: false, - }; - } - } - return { shouldAutomaticallyLink: true, shouldRequireVerification: true, @@ -93,6 +87,17 @@ export const SuperTokensConfig: TypeInput = { Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + smsDelivery: { + override: (oI) => { + return { + ...oI, + sendSms: async (input) => { + console.log("OTP is:", input.userInputCode!); + otp = input.userInputCode!; + }, + }; + }, + }, }), Session.init(), Dashboard.init(), diff --git a/examples/with-account-linking/backend/index.ts b/examples/with-account-linking/backend/index.ts index 6087d2acd..b22729286 100644 --- a/examples/with-account-linking/backend/index.ts +++ b/examples/with-account-linking/backend/index.ts @@ -7,7 +7,7 @@ import { getWebsiteDomain, SuperTokensConfig } from "./config"; import EmailVerification from "supertokens-node/recipe/emailverification"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import Session from "supertokens-node/recipe/session"; -import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; +import { getOtp } from "./config"; import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init(SuperTokensConfig); @@ -49,10 +49,9 @@ app.get("/userInfo", verifySession(), async (req: SessionRequest, res) => { }); }); -// We use a custom endpoint to add phone numbers, because otherwise we'd need to transfer the OTP to the browser in some other way -// In a normal (non-demo) implementation the phone number would need to be verified. -// This is best done through MFA, which is out of the scope of this example, plus this shows usage of the manual linking APIs. -app.post("/addPhoneNumber", verifySession(), async (req: SessionRequest, res) => { +// This is just an example api for how to do manual account linking, but is not +// called by the frontend code of this example app. +app.post("/manual-account-linking-example", verifySession(), async (req: SessionRequest, res) => { const session = req.session!; // First we check that the current session (and the user it belongs to) can have a user linked to it. const user = await getUser(session.getRecipeUserId().getAsString()); @@ -103,6 +102,10 @@ app.post("/addPhoneNumber", verifySession(), async (req: SessionRequest, res) => }); }); +app.get("/get-otp-for-testing", async (req, res) => { + return res.send(getOtp()); +}); + // In case of session related errors, this error handler // returns 401 to the client. app.use(errorHandler()); diff --git a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx index dccc9a9df..3e02bde1c 100644 --- a/examples/with-account-linking/frontend/src/LinkingPage/index.tsx +++ b/examples/with-account-linking/frontend/src/LinkingPage/index.tsx @@ -19,6 +19,9 @@ export const LinkingPage: React.FC = () => { const [phoneNumber, setPhoneNumber] = useState(); const [password, setPassword] = useState(); + const [showEnterOTPField, setShowEnterOTPField] = useState(false); + const [otp, setOtp] = useState(""); + const loadUserInfo = useCallback(async () => { const res = await fetch(`${getApiDomain()}/userInfo`); setUserInfo(await res.json()); @@ -42,24 +45,69 @@ export const LinkingPage: React.FC = () => { }, [setError, setSuccess, password]); const addPhoneNumber = useCallback(async () => { - const resp = await fetch(`${getApiDomain()}/addPhoneNumber`, { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ + if (phoneNumber === undefined) { + return; + } + let cancel = false; + try { + let response = await Passwordless.createCode({ phoneNumber, - }), - }); + }); - const respBody = await resp.json(); - if (respBody.status !== "OK") { - setError(respBody.reason ?? respBody.message ?? respBody.status); - } else { - setSuccess("Successfully added password"); + if (cancel) { + return; + } + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + setShowEnterOTPField(true); + } + } catch (err: any) { + setError(err.message); } - loadUserInfo(); - }, [setError, setSuccess, loadUserInfo, phoneNumber]); + + return () => { + cancel = true; + }; + }, [setError, phoneNumber]); + + const verifyOtp = useCallback(async () => { + if (phoneNumber === undefined) { + return; + } + let cancel = false; + try { + let response = await Passwordless.consumeCode({ + userInputCode: otp, + }); + + if (cancel) { + return; + } + if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert("Incorrect OTP. Please try again"); + } else if (response.status !== "OK") { + window.alert("OTP expired. Please try again"); + setShowEnterOTPField(false); + } else { + setSuccess("Successfully added phone number"); + setShowEnterOTPField(false); + loadUserInfo(); + } + } catch (err: any) { + setError(err.message); + } + + return () => { + cancel = true; + }; + }, [setError, setSuccess, otp, loadUserInfo]); useEffect(() => { loadUserInfo(); @@ -123,12 +171,28 @@ export const LinkingPage: React.FC = () => { {phoneLoginMethod?.length === 0 && (
{ - addPhoneNumber(); + if (showEnterOTPField) { + verifyOtp(); + } else { + addPhoneNumber(); + } ev.preventDefault(); return false; }}> - setPhoneNumber(ev.currentTarget.value)}> - + {showEnterOTPField ? ( +
+ setOtp(ev.currentTarget.value)}> + +
+ ) : ( +
+ setPhoneNumber(ev.currentTarget.value)}> + +
+ )}
)} {thirdPartyLoginMethod?.length === 0 && ( diff --git a/examples/with-account-linking/package.json b/examples/with-account-linking/package.json index 55d8979fe..471346144 100644 --- a/examples/with-account-linking/package.json +++ b/examples/with-account-linking/package.json @@ -15,6 +15,7 @@ "author": "", "license": "ISC", "dependencies": { + "axios": "^1.6.8", "npm-run-all": "^4.1.5" } } diff --git a/examples/with-account-linking/test/basic.test.js b/examples/with-account-linking/test/basic.test.js index b7e05fbd2..f8fce01c9 100644 --- a/examples/with-account-linking/test/basic.test.js +++ b/examples/with-account-linking/test/basic.test.js @@ -33,6 +33,7 @@ const Session = require("../backend/node_modules/supertokens-node/recipe/session const EmailVerification = require("../backend/node_modules/supertokens-node/recipe/emailverification"); const EmailPassword = require("../backend/node_modules/supertokens-node/recipe/emailpassword"); const Passwordless = require("../backend/node_modules/supertokens-node/recipe/passwordless"); +const axios = require("axios"); // Run the tests in a DOM environment. require("jsdom-global")(); @@ -118,10 +119,20 @@ describe("SuperTokens Example Basic tests", function () { await page.waitForSelector(".emailpassword.login-method"); await checkLoginMethods(page, [{ loginMethod: "emailpassword", email }]); - const input = await page.waitForSelector("[type=tel]"); - await input.type(phoneNumber); + { + const input = await page.waitForSelector("[type=tel]"); + await input.type(phoneNumber); + await page.click("[type=tel]+button"); + } + + { + const otpInput = await page.waitForSelector("[type=otp]"); + let otp = await axios("http://localhost:3001/get-otp-for-testing"); + otp = otp.data; + await otpInput.type(otp + ""); + await page.click("[type=otp]+button"); + } - await page.click("[type=tel]+button"); await page.waitForSelector(".passwordless.login-method"); await checkLoginMethods(page, [