Skip to content

Commit

Permalink
Updates with account linking example app (#812)
Browse files Browse the repository at this point in the history
* updates example app

* more changes

* test fixes
  • Loading branch information
rishabhpoddar authored Apr 11, 2024
1 parent 81c7a6a commit 0dd6150
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 38 deletions.
29 changes: 17 additions & 12 deletions examples/with-account-linking/backend/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
13 changes: 8 additions & 5 deletions examples/with-account-linking/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
100 changes: 82 additions & 18 deletions examples/with-account-linking/frontend/src/LinkingPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export const LinkingPage: React.FC = () => {
const [phoneNumber, setPhoneNumber] = useState<string>();
const [password, setPassword] = useState<string>();

const [showEnterOTPField, setShowEnterOTPField] = useState<boolean>(false);
const [otp, setOtp] = useState<string>("");

const loadUserInfo = useCallback(async () => {
const res = await fetch(`${getApiDomain()}/userInfo`);
setUserInfo(await res.json());
Expand All @@ -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();
Expand Down Expand Up @@ -123,12 +171,28 @@ export const LinkingPage: React.FC = () => {
{phoneLoginMethod?.length === 0 && (
<form
onSubmit={(ev) => {
addPhoneNumber();
if (showEnterOTPField) {
verifyOtp();
} else {
addPhoneNumber();
}
ev.preventDefault();
return false;
}}>
<input type="tel" onChange={(ev) => setPhoneNumber(ev.currentTarget.value)}></input>
<button type="submit"> Add phone number </button>
{showEnterOTPField ? (
<div>
<input type="otp" value={otp} onChange={(ev) => setOtp(ev.currentTarget.value)}></input>
<button type="submit"> Submit OTP </button>
</div>
) : (
<div>
<input
type="tel"
value={phoneNumber}
onChange={(ev) => setPhoneNumber(ev.currentTarget.value)}></input>
<button type="submit"> Add phone number </button>
</div>
)}
</form>
)}
{thirdPartyLoginMethod?.length === 0 && (
Expand Down
1 change: 1 addition & 0 deletions examples/with-account-linking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.6.8",
"npm-run-all": "^4.1.5"
}
}
17 changes: 14 additions & 3 deletions examples/with-account-linking/test/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")();
Expand Down Expand Up @@ -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, [
Expand Down

0 comments on commit 0dd6150

Please sign in to comment.