UNPKG

@circle-fin/w3s-pw-web-sdk

Version:

Javascript/Typescript SDK for Circle Programmable Wallets

668 lines (667 loc) 30.3 kB
"use strict"; // Copyright (c) 2023, Circle Technologies, LLC. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.W3SSdk = void 0; const app_1 = require("firebase/app"); const auth_1 = require("firebase/auth"); const jsonwebtoken_1 = require("jsonwebtoken"); const uuid_1 = require("uuid"); const package_json_1 = __importDefault(require("../package.json")); const types_1 = require("./types"); class W3SSdk { constructor(configs, onLoginComplete) { this.serviceUrl = 'https://pw-auth.circle.com'; this.window = window; this.securityQuestionsRequiredCount = 2; this.shouldCloseModalOnForgotPin = false; this.receivedResponseFromService = false; /** * Handles the postMessage event. * @param event - PostMessageEvent. */ this.messageHandler = (event) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8; if (event.origin !== this.serviceUrl) { return; } if ((_a = event.data) === null || _a === void 0 ? void 0 : _a.onFrameReady) { this.receivedResponseFromService = true; const iframe = this.window.document.getElementById('sdkIframe'); (_b = iframe === null || iframe === void 0 ? void 0 : iframe.contentWindow) === null || _b === void 0 ? void 0 : _b.postMessage({ w3s: { appSettings: (_c = this.configs) === null || _c === void 0 ? void 0 : _c.appSettings, auth: (_d = this.configs) === null || _d === void 0 ? void 0 : _d.authentication, challenge: this.challenge, customizations: { securityQuestions: { questions: this.securityQuestions, requiredCount: this.securityQuestionsRequiredCount, securityConfirmItems: this.securityConfirmItems, }, themeColor: this.themeColor, localizations: this.localizations, resources: this.resources, customLinks: this.customLinks, }, deviceInfo: this.deviceInfo, socialVerification: { token: this.socialLoginToken, deviceToken: (_f = (_e = this.configs) === null || _e === void 0 ? void 0 : _e.loginConfigs) === null || _f === void 0 ? void 0 : _f.deviceToken, deviceEncryptionKey: (_h = (_g = this.configs) === null || _g === void 0 ? void 0 : _g.loginConfigs) === null || _h === void 0 ? void 0 : _h.deviceEncryptionKey, socialLoginProvider: this.socialLoginProvider, }, emailVerification: { deviceToken: (_k = (_j = this.configs) === null || _j === void 0 ? void 0 : _j.loginConfigs) === null || _k === void 0 ? void 0 : _k.deviceToken, deviceEncryptionKey: (_m = (_l = this.configs) === null || _l === void 0 ? void 0 : _l.loginConfigs) === null || _m === void 0 ? void 0 : _m.deviceEncryptionKey, otpToken: (_p = (_o = this.configs) === null || _o === void 0 ? void 0 : _o.loginConfigs) === null || _p === void 0 ? void 0 : _p.otpToken, }, }, }, this.serviceUrl); } else if ((_q = event.data) === null || _q === void 0 ? void 0 : _q.onForgotPin) { (_r = this.onForgotPin) === null || _r === void 0 ? void 0 : _r.call(this); } else if ((_s = event.data) === null || _s === void 0 ? void 0 : _s.onComplete) { const iframe = this.window.document.getElementById('sdkIframe'); (_t = iframe === null || iframe === void 0 ? void 0 : iframe.parentNode) === null || _t === void 0 ? void 0 : _t.removeChild(iframe); void ((_u = this.onComplete) === null || _u === void 0 ? void 0 : _u.call(this, undefined, (_v = event.data) === null || _v === void 0 ? void 0 : _v.result)); } else if ((_w = event.data) === null || _w === void 0 ? void 0 : _w.deviceId) { (_x = this.resolveDeviceIdPromise) === null || _x === void 0 ? void 0 : _x.call(this, event.data.deviceId); this.closeModal(); this.unSubscribeMessage(); } else if ((_y = event.data) === null || _y === void 0 ? void 0 : _y.showUi) { this.iframe.width = '100%'; this.iframe.height = '100%'; this.iframe.style.zIndex = '2147483647'; this.iframe.style.position = 'fixed'; this.iframe.style.top = '50%'; this.iframe.style.left = '50%'; this.iframe.style.transform = 'translate(-50%, -50%)'; this.iframe.style.display = ''; } else if ((_z = event.data) === null || _z === void 0 ? void 0 : _z.onSocialLoginVerified) { void ((_0 = this.onLoginComplete) === null || _0 === void 0 ? void 0 : _0.call(this, event.data.onSocialLoginVerified.error, event.data.onSocialLoginVerified.result)); this.closeModal(); this.unSubscribeMessage(); } else if ((_1 = event.data) === null || _1 === void 0 ? void 0 : _1.onEmailLoginVerified) { void ((_2 = this.onLoginComplete) === null || _2 === void 0 ? void 0 : _2.call(this, event.data.onEmailLoginVerified.error, event.data.onEmailLoginVerified.result)); if (event.data.onEmailLoginVerified.result && !event.data.onEmailLoginVerified.error) { this.unSubscribeMessage(); this.closeModal(); } } else if ((_3 = event.data) === null || _3 === void 0 ? void 0 : _3.onResendOtpEmail) { (_4 = this.onResendOtpEmail) === null || _4 === void 0 ? void 0 : _4.call(this); } else if ((_5 = event.data) === null || _5 === void 0 ? void 0 : _5.onError) { void ((_6 = this.onComplete) === null || _6 === void 0 ? void 0 : _6.call(this, (_7 = event.data) === null || _7 === void 0 ? void 0 : _7.error, undefined)); } else if ((_8 = event.data) === null || _8 === void 0 ? void 0 : _8.onClose) { this.closeModal(); this.unSubscribeMessage(); } }; if (W3SSdk.instance != null) { this.setupInstance(configs, onLoginComplete); return W3SSdk.instance; } this.iframe = document.createElement('iframe'); this.setupInstance(configs, onLoginComplete); W3SSdk.instance = this; } /** * Sets the application settings. * This method will be deprecated in the future. Please use the constructor to set the application settings. * @param appSettings - Application settings. */ setAppSettings(appSettings) { if (this.configs) { this.configs.appSettings = appSettings; } else { this.configs = { appSettings }; } } /** * Sets the authentication information. * @param auth - Authentication information. */ setAuthentication(auth) { if (this.configs) { this.configs.authentication = auth; } else { this.configs = { appSettings: { appId: '', }, authentication: auth, }; } } /** * Updates the configurations. * @param configs - Configurations. * @param onLoginComplete - Callback function that is called when the page is redirected back from the social login provider and receives the verification result. */ updateConfigs(configs, onLoginComplete) { this.setupInstance(configs, onLoginComplete !== null && onLoginComplete !== void 0 ? onLoginComplete : this.onLoginComplete); } /** * Gets the device ID. * @returns Promise<string> - Device ID. */ getDeviceId() { return new Promise((resolve, reject) => { this.resolveDeviceIdPromise = resolve; this.rejectDeviceIdPromise = reject; this.subscribeMessage(); this.appendIframe(false, 'device-id'); setTimeout(() => { var _a; if (!this.receivedResponseFromService) { (_a = this.rejectDeviceIdPromise) === null || _a === void 0 ? void 0 : _a.call(this, 'Failed to receive deviceId'); this.closeModal(); this.unSubscribeMessage(); } }, 1000 * 10); }); } /** * Performs social login. * @param provider - Social login provider. */ performLogin(provider) { var _a; return __awaiter(this, void 0, void 0, function* () { if (provider === types_1.SocialLoginProvider.GOOGLE) { this.performGoogleLogin(); } else if (provider === types_1.SocialLoginProvider.FACEBOOK) { this.performFacebookLogin(); } else if (provider === types_1.SocialLoginProvider.APPLE) { yield this.performAppleLogin(); } else { void ((_a = this.onLoginComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: 155140, message: 'Invalid social login provider', }, undefined)); } }); } /** * Executes email OTP verification. */ verifyOtp() { this.subscribeMessage(); this.appendIframe(true, 'social/verify-email'); setTimeout(() => { var _a; if (!this.receivedResponseFromService) { void ((_a = this.onComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: 155706, message: 'Network error', }, undefined)); } }, 1000 * 10); } /** * Executes the challenge. * @param challengeId - Challenge ID. * @param onCompleted - Callback function that is called when the challenge is completed. */ execute(challengeId, onCompleted) { this.subscribeMessage(); this.setChallenge({ challengeId }); this.exec(onCompleted, false); } /** * Sets the custom security questions. If the user doesn't provide the custom security questions, the default security questions will be used. * @param questions - Custom security questions. * @param requiredCount - Required number of security questions. * @param securityConfirmItems - Security confirm disclaimer items. */ setCustomSecurityQuestions(questions, requiredCount = 2, securityConfirmItems) { this.securityQuestions = questions; this.securityConfirmItems = securityConfirmItems; if (requiredCount <= 0) { this.securityQuestionsRequiredCount = 2; } else { this.securityQuestionsRequiredCount = requiredCount; } } /** * Sets the localizations. * @param localizations - Localizations. */ setLocalizations(localizations) { this.localizations = localizations; } /** * Sets the resources. * @param resources - Resources. */ setResources(resources) { this.resources = resources; } /** * Sets the theme color. * @param themeColor - Theme color. */ setThemeColor(themeColor) { this.themeColor = themeColor; } /** * Sets the custom links. * @param customLinks - Custom links. */ setCustomLinks(customLinks) { this.customLinks = customLinks; } /** * Sets the callback function that is called when the user clicks the forgot pin button. * @param onForgotPin - Callback function that is called when the user clicks the forgot pin button. * @param shouldCloseModalOnForgotPin - Indicates whether the modal should be closed when the user clicks the forgot pin button. Default is false. */ setOnForgotPin(onForgotPin, shouldCloseModalOnForgotPin = false) { this.shouldCloseModalOnForgotPin = shouldCloseModalOnForgotPin; this.onForgotPin = () => { if (this.shouldCloseModalOnForgotPin) { this.closeModal(); } onForgotPin === null || onForgotPin === void 0 ? void 0 : onForgotPin(); }; } /** * Sets the callback function that is called when the user clicks the resend OTP email button. * @param onResendOtpEmail - Callback function that is called when the user clicks the resend OTP email button. */ setOnResendOtpEmail(onResendOtpEmail) { this.onResendOtpEmail = onResendOtpEmail; } /** * Sets up the instance. * @param configs - Configurations. * @param onLoginComplete - Callback function that is called when the page is redirected back from the social login provider and receives the verification result. */ setupInstance(configs, onLoginComplete) { var _a; if (((_a = configs === null || configs === void 0 ? void 0 : configs.loginConfigs) === null || _a === void 0 ? void 0 : _a.apple) && (0, app_1.getApps)().length === 0) { this.firebaseApp = (0, app_1.initializeApp)(configs.loginConfigs.apple); } else if ((0, app_1.getApps)().length !== 0) { this.firebaseApp = (0, app_1.getApps)()[0]; } this.onLoginComplete = onLoginComplete; this.configs = configs; this.deviceInfo = { model: 'Web', version: package_json_1.default.version, }; void this.execSocialLoginStatusCheck(); } /** * Sets the challenge. * @param challenge - Challenge. */ setChallenge(challenge) { this.challenge = challenge; } /** * Appends the iframe to the document body. * @param showIframe - Indicates whether the iframe should be shown. Default is true. * @param subRoute - Sub route. */ appendIframe(showIframe = true, subRoute = '') { const protocol = this.window.location.protocol; const host = this.window.location.host; const fullDomainWithProtocol = `${protocol}//${host}`; this.iframe.src = `${this.serviceUrl}/${subRoute}?origin=${fullDomainWithProtocol}`; this.iframe.id = 'sdkIframe'; this.iframe.width = showIframe ? '100%' : '0%'; this.iframe.height = showIframe ? '100%' : '0%'; this.iframe.style.zIndex = showIframe ? '2147483647' : '-1'; this.iframe.style.display = 'none'; if (showIframe) { this.iframe.style.position = 'fixed'; this.iframe.style.top = '50%'; this.iframe.style.left = '50%'; this.iframe.style.transform = 'translate(-50%, -50%)'; this.iframe.style.display = ''; } document.body.appendChild(this.iframe); } /** * Executes the challenge. * @param onCompleted - Callback function that is called when the challenge is completed. * @param showIframe - Indicates whether the iframe should be shown. Default is true. */ exec(onCompleted, showIframe = true) { this.appendIframe(showIframe); this.onComplete = onCompleted; setTimeout(() => { var _a; if (!this.receivedResponseFromService) { void ((_a = this.onComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: 155706, message: 'Network error', }, undefined)); } }, 1000 * 10); } performAppleLogin() { var _a; return __awaiter(this, void 0, void 0, function* () { if (!this.firebaseApp) { void ((_a = this.onLoginComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: 155140, message: 'Please provide the Apple social login configurations.', }, undefined)); return; } this.saveOAuthInfo(types_1.SocialLoginProvider.APPLE); const provider = new auth_1.OAuthProvider('apple.com'); const auth = (0, auth_1.getAuth)(this.firebaseApp); try { const cred = yield (0, auth_1.signInWithPopup)(auth, provider); if (!this.extractTokenFromResultAndSave(cred)) { return; } // Send the token to the verification service and reset the social login provider this.verifyTokenViaService(); this.window.localStorage.setItem('socialLoginProvider', ''); } catch (error) { if (error instanceof app_1.FirebaseError && error.code !== 'auth/cancelled-popup-request' && error.code !== 'auth/popup-closed-by-user') { yield this.handleFirebaseFailure(error); } else if (!(error instanceof app_1.FirebaseError)) { this.handleLoginFailure(); } } }); } performFacebookLogin() { var _a, _b, _c; if (!((_b = (_a = this === null || this === void 0 ? void 0 : this.configs) === null || _a === void 0 ? void 0 : _a.loginConfigs) === null || _b === void 0 ? void 0 : _b.facebook)) { void ((_c = this.onLoginComplete) === null || _c === void 0 ? void 0 : _c.call(this, { code: 155140, message: 'Please provide the Facebook social login configurations.', }, undefined)); return; } const { appId, redirectUri } = this.configs.loginConfigs.facebook; const { url = '', state = '' } = this.generateOauthUrlWithParams(types_1.SocialLoginProvider.FACEBOOK, appId, redirectUri) || {}; this.saveOAuthInfo(types_1.SocialLoginProvider.FACEBOOK, state); this.window.location.href = url; } performGoogleLogin() { var _a, _b, _c; if (!((_b = (_a = this.configs) === null || _a === void 0 ? void 0 : _a.loginConfigs) === null || _b === void 0 ? void 0 : _b.google)) { void ((_c = this.onLoginComplete) === null || _c === void 0 ? void 0 : _c.call(this, { code: 155140, message: 'Please provide the Google social login configurations.', }, undefined)); return; } const { clientId, redirectUri, selectAccountPrompt } = this.configs.loginConfigs.google; const { url = '', state = '', nonce = '', } = this.generateOauthUrlWithParams(types_1.SocialLoginProvider.GOOGLE, clientId, redirectUri, selectAccountPrompt) || {}; this.saveOAuthInfo(types_1.SocialLoginProvider.GOOGLE, state, nonce); this.window.location.href = url; } /** * Generates the OAuth URL with the necessary parameters. * @param provider - Social login provider. * @param id - Client ID or Application ID. * @param redirectUri - Redirect URI. * @param selectAccountPrompt - Indicates whether the user should select the account. Default is false. * @returns OAuth URL with the necessary parameters. */ generateOauthUrlWithParams(provider, id, redirectUri, selectAccountPrompt = false) { const state = (0, uuid_1.v4)(); if (provider === types_1.SocialLoginProvider.GOOGLE) { const scope = encodeURIComponent('openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'); const responseType = encodeURIComponent('id_token token'); const nonce = (0, uuid_1.v4)(); return { url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${id}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&state=${state}&response_type=${responseType}&nonce=${nonce}&prompt=${selectAccountPrompt ? 'select_account' : 'none'}`, state, nonce, }; } else if (provider === types_1.SocialLoginProvider.FACEBOOK) { const scope = encodeURIComponent('email'); return { url: `https://www.facebook.com/v13.0/dialog/oauth?client_id=${id}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&state=${state}&response_type=token`, state, }; } } /** * Executes the social login status check before sending the token to the verification service. */ execSocialLoginStatusCheck() { return __awaiter(this, void 0, void 0, function* () { const socialLoginProvider = this.window.localStorage.getItem('socialLoginProvider'); if (socialLoginProvider === types_1.SocialLoginProvider.APPLE) { yield this.handleAppleLoginResponse(); } else if (this.isValidHash(this.window.location.hash)) { this.handleHashLoginResponse(socialLoginProvider); } }); } /** * Handles the Apple login response. * @returns Promise<void>. */ handleAppleLoginResponse() { return __awaiter(this, void 0, void 0, function* () { const auth = (0, auth_1.getAuth)(this.firebaseApp); try { const result = yield (0, auth_1.getRedirectResult)(auth); if (!result || !this.extractTokenFromResultAndSave(result)) { return; } // Send the token to the verification service and reset the social login provider this.verifyTokenViaService(); this.window.localStorage.setItem('socialLoginProvider', ''); } catch (error) { this.handleLoginFailure(); } }); } /** * Handles the hash login responses. * @param socialLoginProvider - Social login provider. */ handleHashLoginResponse(socialLoginProvider) { const hashParams = new URLSearchParams(window.location.hash.slice(1)); if (socialLoginProvider === types_1.SocialLoginProvider.GOOGLE) { this.handleGoogleLogin(hashParams); } else if (socialLoginProvider === types_1.SocialLoginProvider.FACEBOOK) { this.handleFacebookLogin(hashParams); } // Send the token to the verification service this.verifyTokenViaService(); // Clear the hash history.replaceState(null, '', window.location.href.split('#')[0]); } handleGoogleLogin(hashParams) { if (this.isLoginStateValid(hashParams) && this.isLoginNonceValid(hashParams)) { this.socialLoginToken = hashParams.get('id_token'); this.socialLoginProvider = types_1.SocialLoginProvider.GOOGLE; } } handleFacebookLogin(hashParams) { if (this.isLoginStateValid(hashParams)) { this.socialLoginToken = hashParams.get('access_token'); this.socialLoginProvider = types_1.SocialLoginProvider.FACEBOOK; } } isLoginStateValid(hashParams) { return this.checkSocialLoginState(hashParams); } isLoginNonceValid(hashParams) { return this.checkSocialLoginNonce(hashParams); } isValidHash(hash) { const validHashPattern = /^#(?:[a-zA-Z0-9-_.%]+=[^&]*&)*[a-zA-Z0-9-_.%]+=[^&]*$/; return validHashPattern.test(hash); } extractTokenFromResultAndSave(result) { const credential = auth_1.OAuthProvider.credentialFromResult(result); if (credential && credential.idToken) { this.socialLoginToken = credential.idToken; this.socialLoginProvider = types_1.SocialLoginProvider.APPLE; return true; } return false; } handleFirebaseFailure(error) { var _a; return __awaiter(this, void 0, void 0, function* () { yield ((_a = this.onLoginComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: -1, message: error.message, }, undefined)); }); } handleLoginFailure() { var _a; void ((_a = this.onLoginComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: 155140, message: 'Failed to validate the idToken / accessToken', }, undefined)); } verifyTokenViaService() { this.subscribeMessage(); this.appendIframe(false, 'social/verify-token'); setTimeout(() => { var _a; if (!this.receivedResponseFromService) { void ((_a = this.onComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: 155706, message: 'Network error', }, undefined)); } }, 1000 * 10); } /** * Saves the OAuth information to the local storage in order to check the state and nonce value later. * @param provider - Social login provider. * @param state - State value. * @param nonce - Nonce value. */ saveOAuthInfo(provider, state, nonce) { this.window.localStorage.setItem('socialLoginProvider', provider); this.window.localStorage.setItem('state', state !== null && state !== void 0 ? state : ''); this.window.localStorage.setItem('nonce', nonce !== null && nonce !== void 0 ? nonce : ''); } /** * Checks the state value from the social login response. * @param hashParams - Hash parameters. * @returns Indicates whether the state value is valid. */ checkSocialLoginState(hashParams) { var _a; const state = hashParams.get('state'); const storedState = this.window.localStorage.getItem('state'); if (!storedState || state !== storedState) { void ((_a = this.onLoginComplete) === null || _a === void 0 ? void 0 : _a.call(this, { code: 155140, message: 'Failed to validate the idToken / accessToken', }, undefined)); return false; } return true; } /** * Checks the nonce value from the social login response. Only id token is going to have nonce value. * @param hashParams - Hash parameters. * @returns Indicates whether the nonce value is valid. */ checkSocialLoginNonce(hashParams) { var _a, _b, _c; const token = hashParams.get('id_token'); const decodedToken = (0, jsonwebtoken_1.decode)(token || ''); const errorPayload = { code: 155140, message: 'Failed to validate the idToken/ accessToken', }; if (decodedToken === null) { void ((_a = this.onLoginComplete) === null || _a === void 0 ? void 0 : _a.call(this, errorPayload, undefined)); return false; } try { const storedNonce = this.window.localStorage.getItem('nonce'); if (!storedNonce || (decodedToken === null || decodedToken === void 0 ? void 0 : decodedToken.nonce) !== storedNonce) { void ((_b = this.onLoginComplete) === null || _b === void 0 ? void 0 : _b.call(this, errorPayload, undefined)); return false; } } catch (_d) { void ((_c = this.onLoginComplete) === null || _c === void 0 ? void 0 : _c.call(this, errorPayload, undefined)); return false; } return true; } /** * Closes the modal. */ closeModal() { var _a; const iframe = this.window.document.getElementById('sdkIframe'); (_a = iframe === null || iframe === void 0 ? void 0 : iframe.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(iframe); } /** * Subscribes to the postMessage event. */ subscribeMessage() { this.window.addEventListener('message', this.messageHandler, false); } /** * Unsubscribes to the postMessage event. */ unSubscribeMessage() { this.window.removeEventListener('message', this.messageHandler, false); } } exports.W3SSdk = W3SSdk; W3SSdk.instance = null;