import axios from "axios" import * as config from "./config" import { createUserCompletedHandler, initialLoadCompletedHandler, initPromotionLoadCompleted, loadActionLoginHandler, loginDataActionHandler, oidcCompletedHandler, } from "./handler" import { BareEventCallback, BNPLInfo, BnplPayload, CompletedBnplProvidersResponse, Environment, EvaluatedOffersList, EventType, GetListPayload, Identifier, JsonObject, Language, Offer, OfferFlowPayload, OfferFlowPayloadBnpl, OfferPayload, PassportThemeConfig, PromotionOffersFlowPayload, PromotionOffersPayload, Provider, PublicEncryptionKey, PushClaimEventCallback, ShowPromotionOffers, UserProfile } from "./type" import { camelize, decamelize, formatCountryCode, formatPhoneNumber, openPopupWindow, submitCustomForm, } from "./utils" import { getPassportWebUrl } from "./config" import { PASSPORT_APP_TITLE, PRODUCT_TYPE } from "./constants/global" import { createPromotionOfferModal } from "./components" import { Credify } from "./entities/credify" let passportWindow: Window | null let marketData: JsonObject let pushClaimCB: PushClaimEventCallback let orgApiKey: string | undefined let orgId: string | undefined let checkPassportClosedTimer: any let proxyLoginCallback: BareEventCallback let passportClosedCallback: BareEventCallback let passportThemeConfig: PassportThemeConfig | undefined let bnplPaymentEventComplete: BareEventCallback let language: Language /** * Instantiates the SDK * @param apiKey - The org's apiKey * @param marketId - The org's ID * @param env - The development environment: "SANDBOX" or "PRODUCTION" * @param theme - The theme customization * */ const initialize = ( env: Environment, apiKey: string, marketId: string, theme?: PassportThemeConfig ) => { config.setEnvironment(env) orgApiKey = apiKey orgId = marketId passportThemeConfig = theme } const getCredifyInstance = async (apiKey = "") => { return await Credify.getInstance(apiKey) } const messageHandler = (event: any) => { const eventData = event.data /* Message handler is the only callback handler for postmessage communication and it contains global window messages as well. Some messages that are sent from the browser (such as meta data messages) is an object type and if we throw the error here then it will confuse the client so I applied just a filter here to allow handling the only messages from the passport web app. */ if ( typeof eventData === "string" && (eventData.includes(config.PASSPORT_APP_EVENT) || eventData.includes(config.BNPL_PROXY_APP_EVENT) || eventData.includes(config.CREDIFY_LOGIN_APP)) ) { const dataJson: EventType = JSON.parse(eventData) console.log({ event: dataJson }) const action = dataJson.action const payload = dataJson.payload switch (action) { case config.ACTION_PROMOTION_LOAD_COMPLETED: initPromotionLoadCompleted(passportWindow, marketData) break case config.INITIAL_LOAD_COMPLETED_ACTION: initialLoadCompletedHandler(passportWindow, marketData) break case config.PROXY_LOGIN_DATA_ACTION: loginDataActionHandler(passportWindow, payload, proxyLoginCallback) break case config.CREATE_USER_COMPLETED_ACTION: createUserCompletedHandler(pushClaimCB, passportWindow, payload) break case config.OIDC_AUTH_COMPLETED_ACTION: oidcCompletedHandler(passportWindow, payload) break case config.BNPL_PAYMENT_COMPLETE: // close the passport window passportWindow?.close() // open popup payment success if (bnplPaymentEventComplete) { bnplPaymentEventComplete() } break case config.OPENER_ACTION_LOGIN_LOAD_COMPLETED: loadActionLoginHandler(passportWindow, marketData?.profile) break default: break } } } /** * To add logic after bnpl payment complete * @param e - The event that will be executed after completeting BNPL payment */ const registerBNPLPaymentCompleteHandler = (e: BareEventCallback) => { bnplPaymentEventComplete = e } const setMarketData = (data: JsonObject) => { if (typeof data !== "object") { throw Error("Data should be a JsonObject") } marketData = data } /** * Credify Login with OIDC Client set up URL */ const login = async (setupUrl: string) => { cleanUp() passportWindow = openPopupWindow(setupUrl) window.addEventListener("message", messageHandler) } /** * Credify Login with Credify endpoint * This one is for debugging */ const loginWithCredify = async (clientId: string, key?: Identifier) => { cleanUp() const query = { clientId, ...key, } const q = new URLSearchParams(decamelize(query)).toString() const url = `${config.getBaseUrl()}/v1/oidc?${q}` passportWindow = openPopupWindow(url) window.addEventListener("message", messageHandler) // for debugging // oidcCompletedHandler(passportWindow, { url: "https://dev-oidc-auth.credify.ninja/redirect?login_challenge=5f148b9a7ad548bb890ed85a716ddb4f" }) } /** * Begins offer flow * @param data - A payload that contains an offer fetched from getOffers API and profile containing user info * @param pushClaimTokenCB - A callback for an organization to push the user's claim tokens * * @deprecated Using `showOfferByCode()` instead */ const startOfferFlow = ( data: OfferFlowPayload, pushClaimTokenCB?: PushClaimEventCallback ) => { console.log({ data }) commonOfferFlow(pushClaimTokenCB) try { setMarketData({ offer: data.offer, profile: data.profile, theme: passportThemeConfig, }) } catch (error) { console.error(error) return } let url = `${getPassportWebUrl()}/dop/initial` handleTogglePopupWindow(url) } /** * Begins offer flow * @param data - A payload that contains an `offerCode` that you want to show, profile containing user info and customization * theme(PassportThemeConfig object). The theme is optional. * @param pushClaimTokenCB - A callback for an organization to push the user's claim tokens */ const showOfferByCode = ( data: OfferPayload, pushClaimTokenCB?: PushClaimEventCallback ) => { console.log({ data }) commonOfferFlow(pushClaimTokenCB) try { setMarketData({ offerCode: data.offerCode, marketId: orgId, profile: data.profile, theme: passportThemeConfig, }) } catch (error) { console.error(error) return } let url = `${getPassportWebUrl()}/dop/initial` handleTogglePopupWindow(url) } /** * Begins promotion offer flow * @param data - A payload that contains an offer fetched from getOffers API and profile containing user info * @param pushClaimTokenCB - A callback for an organization to push the user's claim tokens */ const startPromotionOfferFlow = ( data: PromotionOffersPayload | PromotionOffersFlowPayload, pushClaimTokenCB: PushClaimEventCallback ) => { commonOfferFlow(pushClaimTokenCB) try { if ("offerCodes" in data) { setMarketData({ offerCodes: data.offerCodes, marketId: orgId, profile: data.profile, theme: passportThemeConfig, }) } else if ("offers" in data) { setMarketData({ offers: data.offers, profile: data.profile, theme: passportThemeConfig, }) } } catch (error) { console.error(error) return } let url = `${getPassportWebUrl()}/dop/start` handleTogglePopupWindow(url) } /** * Begins offer flow for BNPL * @param pushClaimTokenCB - A callback for an organization to push the user's claim tokens */ const commonOfferFlow = (pushClaimTokenCB?: PushClaimEventCallback) => { cleanUp() if (pushClaimTokenCB) { pushClaimCB = pushClaimTokenCB } } /** * Begins offer flow for BNPL * @param shouldRedirect - True: Redirect on the current url. False: open new window * @param data - A payload that contains an offer fetched from getOffers API and profile containing user info * @param pushClaimTokenCB - A callback for an organization to push the user's claim tokens * @param newWindow - New window in case * * @deprecated Using `showBnplByCodes()` instead */ const startOfferFlowBnpl = ( shouldRedirect: boolean, data: OfferFlowPayloadBnpl, pushClaimTokenCB?: PushClaimEventCallback, newWindow?: Window ) => { console.log({ data }) const passedData = { offers: data.offers, profile: data.profile, order: { orderId: data.orderId, orderAmount: data.orderAmount, }, completeBnplProviders: data.completeBnplProviders, theme: passportThemeConfig, marketId: data.marketId, language, tenor: data.tenor, } if (shouldRedirect) { handleBnplWithRedirect(passedData) return } commonOfferFlow(pushClaimTokenCB) try { setMarketData(passedData) } catch (error) { console.error(error) return } let url = `${getPassportWebUrl()}/bnpl` handleTogglePopupWindow(url, newWindow) } /** * Begins offer flow for BNPL * @param shouldRedirect - True: Redirect on the current url. False: open new window * @param data - A payload that contains `offerCode` list/`productCode`(get from offer object), * profile containing user info and order info is required. * @param pushClaimTokenCB - A callback for an organization to push the user's claim tokens * @param newWindow - New window in case */ const showBnplByCodes = ( shouldRedirect: boolean, data: BnplPayload, pushClaimTokenCB?: PushClaimEventCallback, newWindow?: Window ) => { const profile = { ...data.profile, phoneNumber: formatPhoneNumber(data.profile?.phoneNumber || ""), countryCode: formatCountryCode(data.profile?.countryCode || ""), } const passedData = { offerCodes: data.offerCodes, packageCode: data.packageCode, profile: profile, order: data.orderInfo, marketId: orgId, theme: passportThemeConfig, productCode: data.productCode, language, bnpl: data.bnpl, } if (shouldRedirect) { handleBnplWithRedirect(passedData) return } commonOfferFlow(pushClaimTokenCB) try { setMarketData(passedData) } catch (error) { console.error(error) return } let url = `${getPassportWebUrl()}/bnpl` handleTogglePopupWindow(url, newWindow) } interface WindowSize { width: number height: number top: number left: number systemZoom: number } /** * Return meta data of new window that preparing to open * @returns `{ width, height, top, left, systemZoom }` */ const calculateResizeWindow = (): WindowSize => { const w = 375 const h = 850 const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : window.screen.width const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : window.screen.height const systemZoom = width / window.screen.availWidth const left = (width - w) / 2 / systemZoom + dualScreenLeft const top = (height - h) / 2 / systemZoom + dualScreenTop const result = { width: w, height: h, top, left, systemZoom, } return result } /** * A function to registerd window object before opening new window. To resolving issue for opening new popup on Safari */ const getPreparedWindowToOpen = (): Window | null => { const { width, height, top, left, systemZoom } = calculateResizeWindow() let windowReference = window.open( "", PASSPORT_APP_TITLE, ` scrollbars=yes, width=${width}, height=${height / systemZoom}, top=${top}, left=${left} ` ) return windowReference } const passportClosedHandler = () => { if (passportClosedCallback) { passportClosedCallback() } } const handleTogglePopupWindow = (url: string, newWindow?: Window) => { passportWindow = openPopupWindow(url, newWindow) window.addEventListener("message", messageHandler) checkPassportClosedTimer = setInterval(function () { if (passportWindow?.closed && checkPassportClosedTimer) { clearInterval(checkPassportClosedTimer) checkPassportClosedTimer = 0 passportClosedHandler() } }, 300) } /** * Gets a list of offers after filtering for a specific user. * @param id - User ID in your service. * @param options - phoneNumber (e.g. 32123456789 - no 0 at the head), countryCode (e.g. +84 - needs +), credifyId (ID in Credify system). * @returns list of offers */ const getOffers = async ( id?: string, options?: { phoneNumber?: string countryCode?: string credifyId?: string productTypes?: string[] }, params?: { [key: string]: any } ): Promise => { if (!orgApiKey) { throw new Error("API KEY is required") } try { const res = await getList(id, options, params) return res.offers! } catch (error) { console.log({ error }) throw error } } /** * Gets a list of offers along with credifyId * @param id - User ID in your service. * @param options - phoneNumber (e.g. 32123456789 - no 0 at the head), countryCode (e.g. +84 - needs +), credifyId (ID in Credify system). * @returns list of offers */ const getEvaluatedOffers = async ( id?: string, options?: { phoneNumber?: string; countryCode?: string; credifyId?: string } ): Promise => { if (!orgApiKey) { throw new Error("API KEY is required") } try { const res = await getList(id, options) return res } catch (error) { console.log({ error }) throw error } } const getCompletedBnplProviders = async ( credifyId: string ): Promise => { try { const credifyInstance = await getCredifyInstance(orgApiKey) const res = await axios( `${config.getBaseUrl()}/v1/integration/bnpl-consumers/completed-bnpl-providers`, { method: "GET", //@ts-ignore headers: { "X-API-KEY": orgApiKey, Authorization: `Bearer ${credifyInstance.accessToken}`, }, params: { credify_id: credifyId }, } ) const data = res.data as CompletedBnplProvidersResponse return data } catch (e) { throw e } } const getList = async ( id?: string, options?: { phoneNumber?: string countryCode?: string credifyId?: string productTypes?: string[] }, params?: { [key: string]: any } ): Promise => { try { const payload: GetListPayload = { localId: id } const credifyInstance = await getCredifyInstance(orgApiKey) if (options?.phoneNumber) { payload.phoneNumber = formatPhoneNumber(options?.phoneNumber) } if (options?.countryCode) { payload.countryCode = formatCountryCode(options?.countryCode) } if (options?.credifyId) { payload.credifyId = options.credifyId } if (options?.productTypes) { payload.productTypes = options.productTypes } console.log("payload", payload) const res = await axios( `${config.getBaseUrl()}/v1/claim-providers/offers`, { method: "POST", //@ts-ignore headers: { Authorization: `Bearer ${credifyInstance.accessToken}` }, data: { ...decamelize(payload) }, params, } ) if (!res.data.data?.offers) { throw new Error("Unexpected object") } return camelize(res.data.data) } catch (e) { throw e } } /** * Handle login for only clients with role market on proxy page * @param loginCallback - Code logic after login */ const proxyLogin = (loginCallback: BareEventCallback) => { cleanUp() proxyLoginCallback = loginCallback passportWindow = openPopupWindow( `${getPassportWebUrl()}/dop/login?fromMarketProxy=true` ) window.addEventListener("message", messageHandler) } /** * If we want to handle events like refresh data when the redemption flow finishes, we need to set the call back here * @param cb - callback */ const setPassportCloseCallback = (cb: BareEventCallback) => { passportClosedCallback = cb } /** * Redirect user to passport consent page to finish the redemption flow */ const redirectToConsentPage = () => { const consentPageUrl = `${config.getPassportWebUrl()}/dop/update-profile` window.location.href = consentPageUrl } const cleanUp = () => { passportWindow = null pushClaimCB = null setMarketData(null) clearInterval(checkPassportClosedTimer) window.removeEventListener("message", messageHandler) } const startServiceInstanceFlow = ( marketId: string, userProfile?: UserProfile, productTypes?: PRODUCT_TYPE[] ) => { setMarketData({ profile: userProfile, }) const productTypesQueryParams = productTypes?.length ? productTypes .map(function (type) { return "product-types[]=" + type }) .join("&") : "" let url = `${getPassportWebUrl()}/pss/service-instance?market-id=${marketId}&${productTypesQueryParams}` handleTogglePopupWindow(url) } /** * Get bnpl information in bnpl flow * @param id user's id * @param options meta data from user profile * @returns result checking if bnpl flow is available, offer list, provider list * * @deprecated It'll be removed in the future. */ const getBNPLInfo = async ( id?: string, options?: { phoneNumber?: string countryCode?: string credifyId?: string productTypes: string[] } ): Promise => { const offers = await getOffers(id?.toString(), options) let completedProviders = [] as Provider[] if (options?.credifyId) { const { data } = await getCompletedBnplProviders(options?.credifyId) if (data?.providers?.length) { completedProviders = data.providers } } return { isAvailable: offers.length > 0 || completedProviders.length > 0, offers, providers: completedProviders, } } const getPublicEncryptionKey = async (): Promise => { try { const credifyInstance = await getCredifyInstance(orgApiKey) const res = await axios(`${config.getPassportBackendUrl()}/api/v1/key`, { method: "GET", //@ts-ignore headers: { Authorization: `Bearer ${credifyInstance.accessToken}` }, }) if (!res.data.data) { throw new Error("Unexpected object") } return camelize(res.data.data) } catch (e) { throw e } } const handleBnplWithRedirect = async (data: JsonObject) => { const credifyInstance = await getCredifyInstance(orgApiKey) const formData = { payload: data, token: credifyInstance.accessToken, } const url = `${config.getPassportBackendUrl()}/api/v1/redirect` try { submitCustomForm(url, decamelize(formData)) } catch (error) { console.log("error", error) } } /** * @deprecated Using `showPromotionOffersByCodes()` instead */ const showPromotionOffers = (props: ShowPromotionOffers, callBack: PushClaimEventCallback) => { const promotionOfferModal = createPromotionOfferModal({ onClick: () => startPromotionOfferFlow({...props}, callBack), }) const root = document.querySelector("body") root?.appendChild(promotionOfferModal); } const showPromotionOffersByCodes = (props: PromotionOffersPayload, callBack: PushClaimEventCallback) => { const promotionOfferModal = createPromotionOfferModal({ onClick: () => startPromotionOfferFlow({...props}, callBack), }) const root = document.querySelector("body") root?.appendChild(promotionOfferModal) } const setLanguage = (newLanguage: Language) => { language = newLanguage } export { initialize, startOfferFlow, showOfferByCode, startOfferFlowBnpl, showBnplByCodes, getOffers, getCompletedBnplProviders, login, loginWithCredify, getEvaluatedOffers, proxyLogin, setPassportCloseCallback, redirectToConsentPage, registerBNPLPaymentCompleteHandler, calculateResizeWindow, getPreparedWindowToOpen, getBNPLInfo, startServiceInstanceFlow, PRODUCT_TYPE, getPublicEncryptionKey, showPromotionOffers, showPromotionOffersByCodes, setLanguage, } export * from "./type" export { loadUserCredential, saveUserCredential } from "./storage"