diff --git a/suite-native/app/app.config.ts b/suite-native/app/app.config.ts index 3ea1ec0678..91072293db 100644 --- a/suite-native/app/app.config.ts +++ b/suite-native/app/app.config.ts @@ -248,6 +248,11 @@ export default ({ config }: ConfigContext): ExpoConfig => { host: 'trezor.io', pathPattern: '/setup/.*', }, + { + scheme: 'https', + host: 'trezor.io', + pathPattern: '/suite/deeplink/.*', + }, ] : [ { diff --git a/suite-native/trading-browser-auth/package.json b/suite-native/trading-browser-auth/package.json index 30588630f5..028fa58e79 100644 --- a/suite-native/trading-browser-auth/package.json +++ b/suite-native/trading-browser-auth/package.json @@ -21,6 +21,7 @@ "@suite-common/trading": "workspace:*", "@suite-common/wallet-core": "workspace:*", "@suite-native/atoms": "workspace:*", + "@suite-native/config": "workspace:*", "@suite-native/feature-flags": "workspace:*", "@suite-native/intl": "workspace:*", "@suite-native/sentry": "workspace:*", diff --git a/suite-native/trading-browser-auth/src/consts.ts b/suite-native/trading-browser-auth/src/consts.ts index c285e1860c..9c3354c124 100644 --- a/suite-native/trading-browser-auth/src/consts.ts +++ b/suite-native/trading-browser-auth/src/consts.ts @@ -1,2 +1,9 @@ -export const TRADING_URL_BASE = 'trezorsuite://trading'; +import { isProduction } from '@suite-native/config'; + +const TRADING_URL_BASE_PRODUCTION = 'https://trezor.io/suite/deeplink/trade'; +const TRADING_URL_BASE_DEV = 'trezorsuite://trading'; + +// we don't want to expose custom scheme in production, +// but we want to use custom scheme for develop as dev app has different app package +export const TRADING_URL_BASE = isProduction() ? TRADING_URL_BASE_PRODUCTION : TRADING_URL_BASE_DEV; export const TRADING_URL_DEFAULT_BACK = `${TRADING_URL_BASE}/back`; diff --git a/suite-native/trading-browser-auth/src/utils/__tests__/formUtils.test.ts b/suite-native/trading-browser-auth/src/utils/__tests__/formUtils.test.ts index 32ab609ce2..c64ff3478c 100644 --- a/suite-native/trading-browser-auth/src/utils/__tests__/formUtils.test.ts +++ b/suite-native/trading-browser-auth/src/utils/__tests__/formUtils.test.ts @@ -1,16 +1,60 @@ +import type { FormResponse } from 'invity-api'; + import { trezorLogo } from '@suite-common/suite-constants'; -import { - applyHtmlTemplate, - buildTradingUrl, - getRequestFormSource, - getSourceForForm, -} from '../formUtils'; +import type { BuildTradingUrlProps } from '../formUtils'; + +const mockIsProduction = jest.fn(); + +jest.mock('@suite-native/config', () => ({ + isProduction: () => mockIsProduction(), +})); + +const importModules = () => { + const { TRADING_URL_BASE, TRADING_URL_DEFAULT_BACK } = require('../../consts'); + const { + applyHtmlTemplate, + buildTradingUrl, + getRequestFormSource, + getSourceForForm, + } = require('../formUtils'); + + return { + TRADING_URL_BASE: TRADING_URL_BASE as string, + TRADING_URL_DEFAULT_BACK: TRADING_URL_DEFAULT_BACK as string, + applyHtmlTemplate: applyHtmlTemplate as ( + content?: string, + options?: Record, + ) => string, + buildTradingUrl: buildTradingUrl as (props: BuildTradingUrlProps) => string, + getRequestFormSource: getRequestFormSource as (args: { + form?: FormResponse['form']; + }) => { uri?: string; html?: string } | null, + getSourceForForm: getSourceForForm as ( + form: FormResponse['form'] | undefined, + backUrl?: string, + ) => { uri?: string; html?: string } | null, + }; +}; describe('formUtils', () => { - describe('applyHtmlTemplate', () => { - it('should have content', () => { - expect(applyHtmlTemplate('CONTENT_TO_EMBED')).toBe(` + describe('dev environment', () => { + let applyHtmlTemplate: ReturnType['applyHtmlTemplate']; + let buildTradingUrl: ReturnType['buildTradingUrl']; + let getRequestFormSource: ReturnType['getRequestFormSource']; + let getSourceForForm: ReturnType['getSourceForForm']; + + beforeAll(() => { + mockIsProduction.mockReturnValue(false); + jest.isolateModules(() => { + ({ applyHtmlTemplate, buildTradingUrl, getRequestFormSource, getSourceForForm } = + importModules()); + }); + }); + + describe('applyHtmlTemplate', () => { + it('should have content with dev back URL', () => { + expect(applyHtmlTemplate('CONTENT_TO_EMBED')).toBe(` @@ -48,16 +92,16 @@ ${' '.repeat(16)} `); - }); + }); - it('should have content with options', () => { - expect( - applyHtmlTemplate('CONTENT_TO_EMBED', { - title: 'TITLE', - script: 'SCRIPT', - backUrl: 'BACK_URL', - }), - ).toStrictEqual(` + it('should have content with options', () => { + expect( + applyHtmlTemplate('CONTENT_TO_EMBED', { + title: 'TITLE', + script: 'SCRIPT', + backUrl: 'BACK_URL', + }), + ).toStrictEqual(` @@ -95,101 +139,132 @@ ${' '.repeat(16)} `); - }); - }); - - describe('getRequestFormSource', () => { - it('should return null when no form is provided', () => { - expect(getRequestFormSource({})).toBeNull(); - }); - - it('should return uri for GET formMethod', () => { - expect( - getRequestFormSource({ - form: { - formMethod: 'GET', - formAction: 'get_action', - fields: {}, - }, - }), - ).toStrictEqual({ - uri: 'get_action', }); }); - it('should return null for IFRAME formMethod', () => { - expect( - getRequestFormSource({ - form: { - formMethod: 'IFRAME', - formAction: 'get_action', - fields: {}, - }, - }), - ).toBeNull(); - }); + describe('getRequestFormSource', () => { + it('should return null when no form is provided', () => { + expect(getRequestFormSource({})).toBeNull(); + }); - it('should create script with form for POST formMethod', () => { - expect( - getRequestFormSource({ - form: { - formMethod: 'POST', - formAction: 'post_action', - fields: { key1: 'value1', key2: 'value2' }, - }, - }), - ).toStrictEqual({ - html: ` + it('should return uri for GET formMethod', () => { + expect( + getRequestFormSource({ + form: { + formMethod: 'GET', + formAction: 'get_action', + fields: {}, + }, + }), + ).toStrictEqual({ + uri: 'get_action', + }); + }); + + it('should return null for IFRAME formMethod', () => { + expect( + getRequestFormSource({ + form: { + formMethod: 'IFRAME', + formAction: 'get_action', + fields: {}, + }, + }), + ).toBeNull(); + }); + + it('should create script with form for POST formMethod', () => { + expect( + getRequestFormSource({ + form: { + formMethod: 'POST', + formAction: 'post_action', + fields: { key1: 'value1', key2: 'value2' }, + }, + }), + ).toStrictEqual({ + html: ` Forwarding to post_action...
`, + }); + }); + }); + + describe('getSourceForForm', () => { + it('should return null when no form is provided', () => { + expect(getSourceForForm(undefined)).toBeNull(); + }); + + it('should return uri object for GET form', () => { + expect( + getSourceForForm({ + formMethod: 'GET', + formAction: 'get_action', + fields: {}, + }), + ).toStrictEqual({ + uri: 'get_action', + }); + }); + + it('should return html object for POST form', () => { + const result = getSourceForForm( + { + formMethod: 'POST', + formAction: 'post_action', + fields: { key: 'value' }, + }, + 'custom_back_url', + ); + + expect(result).toHaveProperty('html'); + expect(result?.html).toContain('post_action'); + expect(result?.html).toContain('custom_back_url'); + }); + }); + + describe('buildTradingUrl', () => { + it('should return correct url format with dev base', () => { + expect( + buildTradingUrl({ + actionType: 'quote', + tradeType: 'buy', + orderId: '1234', + }), + ).toBe('trezorsuite://trading?action=quote&tradeType=buy&orderId=1234'); }); }); }); - describe('getSourceForForm', () => { - it('should return null when no form is provided', () => { - expect(getSourceForForm(undefined)).toBeNull(); - }); + describe('production environment', () => { + let applyHtmlTemplate: ReturnType['applyHtmlTemplate']; + let buildTradingUrl: ReturnType['buildTradingUrl']; - it('should return uri object for GET form', () => { - expect( - getSourceForForm({ - formMethod: 'GET', - formAction: 'get_action', - fields: {}, - }), - ).toStrictEqual({ - uri: 'get_action', + beforeAll(() => { + mockIsProduction.mockReturnValue(true); + jest.isolateModules(() => { + ({ applyHtmlTemplate, buildTradingUrl } = importModules()); }); }); - it('should return html object for POST form', () => { - const result = getSourceForForm( - { - formMethod: 'POST', - formAction: 'post_action', - fields: { key: 'value' }, - }, - 'custom_back_url', - ); - - expect(result).toHaveProperty('html'); - expect(result?.html).toContain('post_action'); - expect(result?.html).toContain('custom_back_url'); + it('should use production back URL in applyHtmlTemplate', () => { + const html = applyHtmlTemplate('CONTENT_TO_EMBED'); + expect(html).toContain('href="https://trezor.io/suite/deeplink/trade/back"'); }); - }); - describe('buildTradingUrl', () => { - it('should return correct url format', () => { + + it('should return correct url format with production base', () => { expect( buildTradingUrl({ actionType: 'quote', tradeType: 'buy', orderId: '1234', }), - ).toBe('trezorsuite://trading?action=quote&tradeType=buy&orderId=1234'); + ).toBe( + 'https://trezor.io/suite/deeplink/trade?action=quote&tradeType=buy&orderId=1234', + ); }); }); }); diff --git a/suite-native/trading-browser-auth/src/utils/__tests__/utils.test.ts b/suite-native/trading-browser-auth/src/utils/__tests__/utils.test.ts index a3f1b0ae7c..b3596d1174 100644 --- a/suite-native/trading-browser-auth/src/utils/__tests__/utils.test.ts +++ b/suite-native/trading-browser-auth/src/utils/__tests__/utils.test.ts @@ -1,43 +1,91 @@ -import { TRADING_URL_DEFAULT_BACK } from '../../consts'; -import { doesUrlContainCloseCallbackUrl } from '../utils'; +const mockIsProduction = jest.fn(); + +jest.mock('@suite-native/config', () => ({ + isProduction: () => mockIsProduction(), +})); + +const importModules = () => { + const { TRADING_URL_BASE, TRADING_URL_DEFAULT_BACK } = require('../../consts'); + const { doesUrlContainCloseCallbackUrl } = require('../utils'); + + return { TRADING_URL_BASE, TRADING_URL_DEFAULT_BACK, doesUrlContainCloseCallbackUrl }; +}; describe('utils', () => { - describe('doesUrlContainCloseCallbackUrl', () => { - const closeCallbackUrl = 'trezorsuite://trading'; + describe('doesUrlContainCloseCallbackUrl - dev', () => { + let doesUrlContainCloseCallbackUrl: (url: string, closeCallbackUrl?: string) => boolean; - it('should return true when URL contains closeCallbackUrl', () => { - const url = 'trezorsuite://trading?action=trade&tradeType=buy&orderId=123'; - expect(doesUrlContainCloseCallbackUrl(url, closeCallbackUrl)).toBe(true); + beforeAll(() => { + mockIsProduction.mockReturnValue(false); + jest.isolateModules(() => { + ({ doesUrlContainCloseCallbackUrl } = importModules()); + }); }); - it('should return true when URL contains TRADING_URL_DEFAULT_BACK', () => { - const url = `${TRADING_URL_DEFAULT_BACK}?action=trade&tradeType=buy&orderId=123`; + it('should return true when URL contains dev base', () => { + const url = 'trezorsuite://trading?action=trade&tradeType=buy&orderId=123'; + expect(doesUrlContainCloseCallbackUrl(url, 'trezorsuite://trading')).toBe(true); + }); + + it('should return true when URL contains dev default back', () => { + const url = 'trezorsuite://trading/back?action=trade&tradeType=buy&orderId=123'; expect(doesUrlContainCloseCallbackUrl(url)).toBe(true); }); - it('should return false when URL does not contain TRADING_URL_DEFAULT_BACK', () => { + it('should return false when URL does not match', () => { const url = 'https://example.com/trading?action=trade&tradeType=buy&orderId=123'; expect(doesUrlContainCloseCallbackUrl(url)).toBe(false); }); it('should return false when URL does not contain closeCallbackUrl', () => { const url = 'https://example.com/trading?action=trade&tradeType=buy&orderId=123'; - expect(doesUrlContainCloseCallbackUrl(url, closeCallbackUrl)).toBe(false); + expect(doesUrlContainCloseCallbackUrl(url, 'trezorsuite://trading')).toBe(false); }); it('should handle empty URL', () => { - expect(doesUrlContainCloseCallbackUrl('', closeCallbackUrl)).toBe(false); + expect(doesUrlContainCloseCallbackUrl('', 'trezorsuite://trading')).toBe(false); }); it('should handle URL with special characters', () => { const url = 'trezorsuite://trading?action=trade&tradeType=buy&orderId=dd070b73-fe29-4769-8be1-4075d6b43265&transactionId=8c9476a7-958b-412b-a378-3a3f59b6105a&baseCurrencyCode=czk&baseCurrencyAmount=384.78&transactionStatus=completed'; - expect(doesUrlContainCloseCallbackUrl(url, closeCallbackUrl)).toBe(true); + expect(doesUrlContainCloseCallbackUrl(url, 'trezorsuite://trading')).toBe(true); + }); + }); + + describe('doesUrlContainCloseCallbackUrl - production', () => { + let TRADING_URL_BASE: string; + let TRADING_URL_DEFAULT_BACK: string; + let doesUrlContainCloseCallbackUrl: (url: string, closeCallbackUrl?: string) => boolean; + + beforeAll(() => { + mockIsProduction.mockReturnValue(true); + jest.isolateModules(() => { + ({ TRADING_URL_BASE, TRADING_URL_DEFAULT_BACK, doesUrlContainCloseCallbackUrl } = + importModules()); + }); }); - it('should handle url without specifying closeCallbackUrl', () => { - const url = `${TRADING_URL_DEFAULT_BACK}?action=trade&tradeType=buy&orderId=123`; + it('should use production URL base', () => { + expect(TRADING_URL_BASE).toBe('https://trezor.io/suite/deeplink/trade'); + expect(TRADING_URL_DEFAULT_BACK).toBe('https://trezor.io/suite/deeplink/trade/back'); + }); + + it('should return true when URL contains production base', () => { + const url = + 'https://trezor.io/suite/deeplink/trade?action=trade&tradeType=buy&orderId=123'; + expect(doesUrlContainCloseCallbackUrl(url, TRADING_URL_BASE)).toBe(true); + }); + + it('should return true when URL contains production default back', () => { + const url = + 'https://trezor.io/suite/deeplink/trade/back?action=trade&tradeType=buy&orderId=123'; expect(doesUrlContainCloseCallbackUrl(url)).toBe(true); }); + + it('should not match dev URL with production base', () => { + const url = 'trezorsuite://trading?action=trade&tradeType=buy&orderId=123'; + expect(doesUrlContainCloseCallbackUrl(url, TRADING_URL_BASE)).toBe(false); + }); }); }); diff --git a/suite-native/trading-browser-auth/tsconfig.json b/suite-native/trading-browser-auth/tsconfig.json index 97ee3d8993..98efdb6721 100644 --- a/suite-native/trading-browser-auth/tsconfig.json +++ b/suite-native/trading-browser-auth/tsconfig.json @@ -13,6 +13,7 @@ "path": "../../suite-common/wallet-core" }, { "path": "../atoms" }, + { "path": "../config" }, { "path": "../feature-flags" }, { "path": "../intl" }, { "path": "../sentry" }, diff --git a/yarn.lock b/yarn.lock index 122e8f5288..d47577de9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14253,6 +14253,7 @@ __metadata: "@suite-common/trading": "workspace:*" "@suite-common/wallet-core": "workspace:*" "@suite-native/atoms": "workspace:*" + "@suite-native/config": "workspace:*" "@suite-native/feature-flags": "workspace:*" "@suite-native/intl": "workspace:*" "@suite-native/sentry": "workspace:*"