mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-02-20 00:33:07 +01:00
chore(suite-native): use universal links for prod app
This commit is contained in:
@@ -248,6 +248,11 @@ export default ({ config }: ConfigContext): ExpoConfig => {
|
||||
host: 'trezor.io',
|
||||
pathPattern: '/setup/.*',
|
||||
},
|
||||
{
|
||||
scheme: 'https',
|
||||
host: 'trezor.io',
|
||||
pathPattern: '/suite/deeplink/.*',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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, string>,
|
||||
) => 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<typeof importModules>['applyHtmlTemplate'];
|
||||
let buildTradingUrl: ReturnType<typeof importModules>['buildTradingUrl'];
|
||||
let getRequestFormSource: ReturnType<typeof importModules>['getRequestFormSource'];
|
||||
let getSourceForForm: ReturnType<typeof importModules>['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(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -48,16 +92,16 @@ ${' '.repeat(16)}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
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(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -95,101 +139,132 @@ ${' '.repeat(16)}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
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...
|
||||
<form id="buy-form" method="POST" action="post_action" target='_self'>
|
||||
<input type="hidden" name="key1" value="value1"><input type="hidden" name="key2" value="value2">
|
||||
</form>
|
||||
<script type="text/javascript">document.getElementById("buy-form").submit();</script>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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<typeof importModules>['applyHtmlTemplate'];
|
||||
let buildTradingUrl: ReturnType<typeof importModules>['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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"path": "../../suite-common/wallet-core"
|
||||
},
|
||||
{ "path": "../atoms" },
|
||||
{ "path": "../config" },
|
||||
{ "path": "../feature-flags" },
|
||||
{ "path": "../intl" },
|
||||
{ "path": "../sentry" },
|
||||
|
||||
@@ -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:*"
|
||||
|
||||
Reference in New Issue
Block a user