diff --git a/packages/checkout/sdk/src/fiatRamp/fiatRamp.test.ts b/packages/checkout/sdk/src/fiatRamp/fiatRamp.test.ts index dbd40084a1..602ec95e93 100644 --- a/packages/checkout/sdk/src/fiatRamp/fiatRamp.test.ts +++ b/packages/checkout/sdk/src/fiatRamp/fiatRamp.test.ts @@ -1,4 +1,5 @@ import { Environment } from '@imtbl/config'; +import { AxiosResponse } from 'axios'; import { CheckoutConfiguration } from '../config'; import { RemoteConfigFetcher } from '../config/remoteConfigFetcher'; import { FiatRampService, FiatRampWidgetParams } from './fiatRamp'; @@ -6,19 +7,7 @@ import { ExchangeType, OnRampProvider } from '../types'; import { HttpClient } from '../api/http'; const defaultURL = 'https://global-stg.transak.com'; -const defaultParams = { - apiKey: 'mock-api-key', - network: 'immutablezkevm', - defaultPaymentMethod: 'credit_debit_card', - disablePaymentMethods: '', - productsAvailed: 'buy', - exchangeScreenTitle: 'Buy', - themeColor: '0D0D0D', -}; - -const defaultWidgetUrl = `${defaultURL}?${new URLSearchParams( - defaultParams, -).toString()}`; +const SANDBOX_WIDGET_URL = 'https://api.sandbox.immutable.com/checkout/v1/widget-url'; jest.mock('../config/remoteConfigFetcher'); @@ -29,6 +18,9 @@ describe('FiatRampService', () => { beforeEach(() => { mockedHttpClient = new HttpClient() as jest.Mocked; + mockedHttpClient.post = jest.fn().mockResolvedValue({ + data: { url: defaultURL }, + } as AxiosResponse); }); describe('feeEstimate', () => { @@ -111,86 +103,114 @@ describe('FiatRampService', () => { ); fiatRampService = new FiatRampService(config); }); + it(`should return widget url with non-configurable query params when onRampProvider is Transak' + 'and default to IMX`, async () => { - const params: FiatRampWidgetParams = { + const params = { exchangeType: ExchangeType.ONRAMP, isPassport: false, - }; - const result = await fiatRampService.createWidgetUrl(params); - expect(result).toContain(defaultWidgetUrl); - expect(result).toContain('&defaultCryptoCurrency=IMX'); - expect(result).not.toContain('&email='); - expect(result).not.toContain( - '&isAutoFillUserData=true&disableWalletAddressForm=true', + } as FiatRampWidgetParams; + const result = await fiatRampService.createWidgetUrl(params, mockedHttpClient); + expect(result).toContain('https://global-stg.transak.com'); + expect(mockedHttpClient.post).toHaveBeenCalledWith( + SANDBOX_WIDGET_URL, + expect.objectContaining({ + default_crypto_currency: 'IMX', + }), + { method: 'POST' }, ); - expect(result).not.toContain('&defaultCryptoAmount='); - expect(result).not.toContain('&walletAddress='); }); it(`should return widget url with encoded email, isAutoFillUserData and disableWalletAddressForm query params for passport users`, async () => { - const params: FiatRampWidgetParams = { + const params = { exchangeType: ExchangeType.ONRAMP, isPassport: true, email: 'passport.user@immutable.com', - }; - const result = await fiatRampService.createWidgetUrl(params); - expect(result).toContain(defaultWidgetUrl); - expect(result).toContain('&email=passport.user%2540immutable.com'); - expect(result).toContain('&isAutoFillUserData=true'); - expect(result).toContain('&disableWalletAddressForm=true'); + } as FiatRampWidgetParams; + const result = await fiatRampService.createWidgetUrl(params, mockedHttpClient); + expect(result).toContain('https://global-stg.transak.com'); + expect(mockedHttpClient.post).toHaveBeenCalledWith( + SANDBOX_WIDGET_URL, + expect.objectContaining({ + email: 'passport.user@immutable.com', + is_auto_fill_user_data: true, + disable_wallet_address_form: true, + }), + { method: 'POST' }, + ); }); it(`should return widget url with defaultFiatAmount and defaultCryptoCurrency query params when tokenAmount and tokenSymbol are not present`, async () => { - const params: FiatRampWidgetParams = { + const params = { exchangeType: ExchangeType.ONRAMP, isPassport: false, - }; - const result = await fiatRampService.createWidgetUrl(params); - expect(result).toContain(defaultWidgetUrl); - expect(result).toContain('&defaultFiatAmount=50'); - expect(result).toContain('&defaultCryptoCurrency=IMX'); - expect(result).not.toContain('&defaultCryptoAmount=100'); - expect(result).not.toContain('&cryptoCurrencyCode=ETH'); + } as FiatRampWidgetParams; + const result = await fiatRampService.createWidgetUrl(params, mockedHttpClient); + expect(result).toContain('https://global-stg.transak.com'); + expect(mockedHttpClient.post).toHaveBeenCalledWith( + SANDBOX_WIDGET_URL, + expect.objectContaining({ + default_fiat_amount: 50, + default_crypto_currency: 'IMX', + }), + { method: 'POST' }, + ); }); it(`should return widget url with defaultCryptoAmount and cryptoCurrencyCode query params when tokenAmount and tokenSymbol is present`, async () => { - const params: FiatRampWidgetParams = { + const params = { exchangeType: ExchangeType.ONRAMP, isPassport: false, tokenAmount: '100', tokenSymbol: 'ETH', - }; - const result = await fiatRampService.createWidgetUrl(params); - expect(result).toContain(defaultWidgetUrl); - expect(result).toContain('&defaultCryptoAmount=100'); - expect(result).toContain('&cryptoCurrencyCode=ETH'); - expect(result).not.toContain('&defaultCryptoCurrency=IMX'); + } as FiatRampWidgetParams; + const result = await fiatRampService.createWidgetUrl(params, mockedHttpClient); + expect(result).toContain('https://global-stg.transak.com'); + expect(mockedHttpClient.post).toHaveBeenCalledWith( + SANDBOX_WIDGET_URL, + expect.objectContaining({ + default_crypto_amount: '100', + crypto_currency_code: 'ETH', + }), + { method: 'POST' }, + ); }); it('should return widget url with walletAddress query params when walletAddress is present', async () => { - const params: FiatRampWidgetParams = { + const params = { exchangeType: ExchangeType.ONRAMP, isPassport: false, walletAddress: '0x1234567890', - }; - const result = await fiatRampService.createWidgetUrl(params); - expect(result).toContain(defaultWidgetUrl); - expect(result).toContain('&walletAddress=0x1234567890'); + } as FiatRampWidgetParams; + const result = await fiatRampService.createWidgetUrl(params, mockedHttpClient); + expect(result).toContain('https://global-stg.transak.com'); + expect(mockedHttpClient.post).toHaveBeenCalledWith( + SANDBOX_WIDGET_URL, + expect.objectContaining({ + wallet_address: '0x1234567890', + }), + { method: 'POST' }, + ); }); it('should return widget url with allowed crypto tokens in query params when allowed list is present', async () => { - const params: FiatRampWidgetParams = { + const params = { exchangeType: ExchangeType.ONRAMP, isPassport: false, allowedTokens: ['ETH', 'IMX'], - }; - const result = await fiatRampService.createWidgetUrl(params); - expect(result).toContain(defaultWidgetUrl); - expect(result).toContain('&cryptoCurrencyList=eth%2Cimx'); + } as FiatRampWidgetParams; + const result = await fiatRampService.createWidgetUrl(params, mockedHttpClient); + expect(result).toContain('https://global-stg.transak.com'); + expect(mockedHttpClient.post).toHaveBeenCalledWith( + SANDBOX_WIDGET_URL, + expect.objectContaining({ + crypto_currency_list: 'eth,imx', + }), + { method: 'POST' }, + ); }); }); }); diff --git a/packages/checkout/sdk/src/fiatRamp/fiatRamp.ts b/packages/checkout/sdk/src/fiatRamp/fiatRamp.ts index 267a60adbc..3207aa1a94 100644 --- a/packages/checkout/sdk/src/fiatRamp/fiatRamp.ts +++ b/packages/checkout/sdk/src/fiatRamp/fiatRamp.ts @@ -1,16 +1,19 @@ import { ExchangeType } from '../types/fiatRamp'; import { OnRampConfig, OnRampProvider, OnRampProviderFees } from '../types'; import { CheckoutConfiguration } from '../config'; -import { TRANSAK_API_BASE_URL } from '../env'; +import { IMMUTABLE_API_BASE_URL } from '../env'; +import { HttpClient } from '../api/http'; export interface FiatRampWidgetParams { exchangeType: ExchangeType; isPassport: boolean; - walletAddress?: string; - tokenAmount?: string; - tokenSymbol?: string; - email?: string; - allowedTokens?: string[]; + walletAddress: string | undefined; + tokenAmount: string | undefined; + tokenSymbol: string | undefined; + email: string | undefined; + allowedTokens: string[] | undefined; + showMenu: boolean | undefined; + customSubTitle: string | undefined; } export class FiatRampService { @@ -31,35 +34,42 @@ export class FiatRampService { return config[OnRampProvider.TRANSAK]?.fees; } - public async createWidgetUrl(params: FiatRampWidgetParams): Promise { - return await this.getTransakWidgetUrl(params); + public async createWidgetUrl( + params: FiatRampWidgetParams, + httpClient = new HttpClient(), + ): Promise { + return await this.getTransakWidgetUrl(params, httpClient); } private async getTransakWidgetUrl( params: FiatRampWidgetParams, + httpClient: HttpClient, ): Promise { const onRampConfig = (await this.config.remote.getConfig( 'onramp', )) as OnRampConfig; - const widgetUrl = TRANSAK_API_BASE_URL[this.config.environment]; + const createWidgetUrl = `${IMMUTABLE_API_BASE_URL[this.config.environment]}/checkout/v1/widget-url`; + let widgetParams: Record = { - apiKey: onRampConfig[OnRampProvider.TRANSAK].publishableApiKey, + api_key: onRampConfig[OnRampProvider.TRANSAK].publishableApiKey, network: 'immutablezkevm', - defaultPaymentMethod: 'credit_debit_card', - disablePaymentMethods: '', - productsAvailed: 'buy', - exchangeScreenTitle: 'Buy', - themeColor: '0D0D0D', - defaultCryptoCurrency: params.tokenSymbol || 'IMX', + default_payment_method: 'credit_debit_card', + disable_payment_methods: '', + products_availed: 'buy', + exchange_screen_title: params.customSubTitle === '' ? ' ' : (params.customSubTitle ?? 'Buy'), + theme_color: 'FFFFFF', // this only controls the background colour of the Buy button + default_crypto_currency: params.tokenSymbol || 'IMX', + hide_menu: !(params.showMenu ?? true), + referrer_domain: window.location.origin, }; if (params.isPassport && params.email) { widgetParams = { ...widgetParams, - email: encodeURIComponent(params.email), - isAutoFillUserData: true, - disableWalletAddressForm: true, + email: params.email, + is_auto_fill_user_data: true, + disable_wallet_address_form: true, }; } @@ -69,31 +79,32 @@ export class FiatRampService { if (params.tokenAmount && params.tokenSymbol) { widgetParams = { ...widgetParams, - defaultCryptoAmount: params.tokenAmount, - cryptoCurrencyCode: params.tokenSymbol, + default_crypto_amount: params.tokenAmount, + crypto_currency_code: params.tokenSymbol, }; } else { widgetParams = { ...widgetParams, - defaultFiatAmount: 50, - defaultFiatCurrency: 'usd', + default_fiat_amount: 50, + default_fiat_currency: 'usd', }; } if (params.walletAddress) { widgetParams = { ...widgetParams, - walletAddress: params.walletAddress, + wallet_address: params.walletAddress, }; } if (params.allowedTokens) { widgetParams = { ...widgetParams, - cryptoCurrencyList: params.allowedTokens?.join(',').toLowerCase(), + crypto_currency_list: params.allowedTokens?.join(',').toLowerCase(), }; } - return `${widgetUrl}?${new URLSearchParams(widgetParams).toString()}`; + const response = await httpClient.post(createWidgetUrl, widgetParams, { method: 'POST' }); + return response.data.url; } } diff --git a/packages/checkout/widgets-lib/src/components/Transak/TransakIframe.tsx b/packages/checkout/widgets-lib/src/components/Transak/TransakIframe.tsx index 74d2a37eba..cdefe275bd 100644 --- a/packages/checkout/widgets-lib/src/components/Transak/TransakIframe.tsx +++ b/packages/checkout/widgets-lib/src/components/Transak/TransakIframe.tsx @@ -99,6 +99,7 @@ export function TransakIframe(props: TransakIframeProps) { }} onLoad={onLoad} onError={() => onFailedToLoad?.()} + referrerPolicy="strict-origin-when-cross-origin" /> ); } diff --git a/packages/checkout/widgets-lib/src/components/Transak/useTransakIframe.ts b/packages/checkout/widgets-lib/src/components/Transak/useTransakIframe.ts index 84a43c0421..5116bd4610 100644 --- a/packages/checkout/widgets-lib/src/components/Transak/useTransakIframe.ts +++ b/packages/checkout/widgets-lib/src/components/Transak/useTransakIframe.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import { useCallback, useEffect, useState } from 'react'; import { Environment } from '@imtbl/config'; @@ -50,6 +51,12 @@ export const TRANSAK_API_KEY = { [Environment.PRODUCTION]: 'ad1bca70-d917-4628-bb0f-5609537498bc', }; +export const IMMUTABLE_API_BASE_URL = { + development: 'https://api.dev.immutable.com', + [Environment.SANDBOX]: 'https://api.sandbox.immutable.com', + [Environment.PRODUCTION]: 'https://api.immutable.com', +}; + export const useTransakIframe = (props: UseTransakIframeProps) => { const { contractId, environment, transakParams, onError, @@ -64,7 +71,10 @@ export const useTransakIframe = (props: UseTransakIframeProps) => { estimatedGasLimit, cryptoCurrencyCode, excludeFiatCurrencies, - ...restWidgetParams + exchangeScreenTitle, + email, + walletAddress, + partnerOrderId, } = transakParams; // FIXME: defaulting to first nft in the list @@ -104,21 +114,39 @@ export const useTransakIframe = (props: UseTransakIframeProps) => { const { id: nftTransactionId } = await response.json(); - const baseWidgetUrl = `${TRANSAK_WIDGET_BASE_URL[environment]}?`; - const queryParams = new URLSearchParams({ - apiKey: TRANSAK_API_KEY[environment], - environment: TRANSAK_ENVIRONMENT[environment], - isNFT: 'true', - nftTransactionId, - themeColor: '0D0D0D', - ...restWidgetParams, - }); + const requestBody: Record = { + api_key: TRANSAK_API_KEY[environment], + nft_transaction_id: nftTransactionId, + theme_color: '0D0D0D', + exchange_screen_title: exchangeScreenTitle, + wallet_address: walletAddress, + partner_order_id: partnerOrderId, + referrer_domain: window.location.origin, + }; + + if (email) { + requestBody.email = email; + } if (excludeFiatCurrencies) { - queryParams.append('excludeFiatCurrencies', excludeFiatCurrencies.join(',')); + requestBody.exclude_fiat_currencies = excludeFiatCurrencies.join(','); + } + + const widgetUrlResponse = await fetch(`${IMMUTABLE_API_BASE_URL[environment]}/checkout/v1/widget-url`, { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!widgetUrlResponse.ok) { + throw new Error('Failed to get widget URL'); } - return `${baseWidgetUrl}${queryParams.toString()}`; + const { url } = await widgetUrlResponse.json(); + return url; } catch { onError?.(); } diff --git a/packages/checkout/widgets-lib/src/widgets/on-ramp/views/OnRampMain.tsx b/packages/checkout/widgets-lib/src/widgets/on-ramp/views/OnRampMain.tsx index 244e6a638e..7474640aef 100644 --- a/packages/checkout/widgets-lib/src/widgets/on-ramp/views/OnRampMain.tsx +++ b/packages/checkout/widgets-lib/src/widgets/on-ramp/views/OnRampMain.tsx @@ -1,11 +1,14 @@ import { Passport } from '@imtbl/passport'; import { Box } from '@biom3/react'; import { + useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { - ExchangeType, fetchRiskAssessment, IMTBLWidgetEvents, isAddressSanctioned, + Checkout, + ExchangeType, IMTBLWidgetEvents, } from '@imtbl/checkout-sdk'; +import { Web3Provider } from '@ethersproject/providers'; import url from 'url'; import { useTranslation } from 'react-i18next'; import { HeaderNavigation } from '../../../components/Header/HeaderNavigation'; @@ -31,6 +34,7 @@ import { ConnectLoaderContext } from '../../../context/connect-loader-context/Co import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; import { TRANSAK_ORIGIN } from '../../../components/Transak/useTransakEvents'; import { orchestrationEvents } from '../../../lib/orchestrationEvents'; +import { isPassportProvider } from '../../../lib/provider'; const transakIframeId = 'transak-iframe'; const IN_PROGRESS_VIEW_DELAY_MS = 6000; // 6 second @@ -40,13 +44,66 @@ interface OnRampProps { tokenAddress?: string; passport?: Passport; showBackButton?: boolean; + showMenu?: boolean; + customTitle?: string; + customSubTitle?: string; + showHeader?: boolean; } + +function useWidgetUrl( + checkout: Checkout | undefined, + provider: Web3Provider | undefined, + tokenAddress: string | undefined, + tokenAmount: string | undefined, + passport: Passport | undefined, + showMenu: boolean | undefined, + customSubTitle: string | undefined, +) { + const [widgetUrl, setWidgetUrl] = useState(undefined); + + useEffect(() => { + if (!checkout || !provider) return; + + const params = { + exchangeType: ExchangeType.ONRAMP, + web3Provider: provider, + tokenAddress, + tokenAmount, + passport, + showMenu, + customSubTitle, + }; + + checkout.createFiatRampUrl(params).then(setWidgetUrl); + }, [checkout, provider, tokenAddress, tokenAmount, passport, showMenu, customSubTitle]); + + return widgetUrl; +} + +function useWalletAddress(provider: Web3Provider | undefined) { + const [userWalletAddress, setUserWalletAddress] = useState(undefined); + + useEffect(() => { + if (!provider) return; + (async () => { + const walletAddress = await provider.getSigner().getAddress(); + setUserWalletAddress(walletAddress); + })(); + }, [provider]); + + return userWalletAddress; +} + export function OnRampMain({ passport, showIframe, tokenAmount, tokenAddress, showBackButton, + showMenu, + customTitle, + customSubTitle, + showHeader = true, }: OnRampProps) { const { connectLoaderState } = useContext(ConnectLoaderContext); const { checkout, provider } = connectLoaderState; @@ -56,16 +113,17 @@ export function OnRampMain({ const { t } = useTranslation(); const { viewState, viewDispatch } = useContext(ViewContext); - const [widgetUrl, setWidgetUrl] = useState(''); + const widgetUrl = useWidgetUrl(checkout, provider, tokenAddress, tokenAmount, passport, showMenu, customSubTitle); + const userWalletAddress = useWalletAddress(provider); const eventTimer = useRef(); - const isPassport = !!passport && (provider?.provider as any)?.isPassport; + const isPassport = !!passport && isPassportProvider(provider); const openedFromTopUpView = useMemo( () => viewState.history.length > 2 && viewState.history[viewState.history.length - 2].type - === SharedViews.TOP_UP_VIEW, + === SharedViews.TOP_UP_VIEW, [viewState.history], ); @@ -73,7 +131,7 @@ export function OnRampMain({ const { track } = useAnalytics(); - const trackSegmentEvents = async ( + const trackSegmentEvents = useCallback(async ( event: TransakEventData, walletAddress: string, ) => { @@ -138,9 +196,9 @@ export function OnRampMain({ break; default: } - }; + }, [isPassport, track]); - const transakEventHandler = (event: TransakEventData) => { + const transakEventHandler = useCallback((event: TransakEventData) => { if (eventTimer.current) clearTimeout(eventTimer.current); if (event.event_id === TransakEvents.TRANSAK_WIDGET_OPEN) { @@ -220,72 +278,41 @@ export function OnRampMain({ }, }); } - }; + }, [viewDispatch, tokenAmount, tokenAddress, viewState.view.data?.amount, viewState.view.data?.tokenAddress]); useEffect(() => { - if (!checkout || !provider) return; - - let userWalletAddress = ''; - - (async () => { - const walletAddress = await provider.getSigner().getAddress(); - - const assessment = await fetchRiskAssessment([walletAddress], checkout.config); - - if (isAddressSanctioned(assessment)) { - viewDispatch({ - payload: { - type: ViewActions.UPDATE_VIEW, - view: { - type: SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW, - error: new Error('Sanctioned address'), - }, - }, - }); - - return; - } - - const params = { - exchangeType: ExchangeType.ONRAMP, - web3Provider: provider, - tokenAddress, - tokenAmount, - passport, - }; - - setWidgetUrl(await checkout.createFiatRampUrl(params)); - userWalletAddress = await provider!.getSigner().getAddress(); - })(); - - const domIframe: HTMLIFrameElement = document.getElementById( + const domIframe = document.getElementById( transakIframeId, - ) as HTMLIFrameElement; + ) as HTMLIFrameElement | null; if (!domIframe) return; const handleTransakEvents = (event: any) => { - if (!domIframe) return; - const host = url.parse(event.origin)?.host?.toLowerCase(); if ( event.source === domIframe.contentWindow && host && TRANSAK_ORIGIN.includes(host) ) { - trackSegmentEvents(event.data, userWalletAddress); + trackSegmentEvents(event.data, userWalletAddress ?? ''); transakEventHandler(event.data); } }; + window.addEventListener('message', handleTransakEvents); - }, [checkout, provider, tokenAmount, tokenAddress, passport]); + + // eslint-disable-next-line consistent-return + return () => { + window.removeEventListener('message', handleTransakEvents); + }; + }, [trackSegmentEvents, transakEventHandler, userWalletAddress]); return ( sendOnRampWidgetCloseEvent(eventTarget)} showBack={showBack} onBackButtonClick={() => { @@ -296,7 +323,7 @@ export function OnRampMain({ ); }} /> - )} + ) : undefined} footerBackgroundColor="base.color.translucent.emphasis.200" > @@ -311,6 +338,7 @@ export function OnRampMain({ border: 'none', position: 'absolute', }} + referrerPolicy="strict-origin-when-cross-origin" /> diff --git a/packages/x-client/src/utils/formatError.test.ts b/packages/x-client/src/utils/formatError.test.ts index d93957c192..97307ad907 100644 --- a/packages/x-client/src/utils/formatError.test.ts +++ b/packages/x-client/src/utils/formatError.test.ts @@ -24,7 +24,7 @@ describe('formatError', () => { environment: Environment.SANDBOX, })); await expect(client.getUser('')).rejects.toThrowError( - 'Error: Request failed with status code 405', + 'AxiosError: Request failed with status code 404', ); });