Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ ARBISCAN_API_KEY=
OPSCAN_API_KEY=
BASESCAN_API_KEY=
STATIC_CONTENT_CARDS_ENABLED=
TEST_MODE_NETWORK=
TEST_MODE_NETWORK=
SUMSUB_LEVEL_NAME=
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ allprojects {
maven { url 'https://www.jitpack.io' }
maven { url "https://appboy.github.io/appboy-android-sdk/sdk" }
maven { url "https://dl.appsflyer.com" }
maven { url "https://maven.sumsub.com/repository/maven-public/" }
}
}
4 changes: 4 additions & 0 deletions assets/img/home_identity_verified.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
159 changes: 159 additions & 0 deletions assets/img/kyc_get_verified.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/img/kyc_status_denied.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/img/kyc_status_pending.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions assets/img/kyc_status_verified.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/img/person_identity_verification.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/img/settings-arrow-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,56 @@ declare module '@env' {
export const SENTRY_DSN: string;
export const REGTEST_BASE_BITPAY_URL: string;
export const STATIC_CONTENT_CARDS_ENABLED: string;
export const SUMSUB_LEVEL_NAME: string;
}

declare module '@sumsub/react-native-mobilesdk-module' {
interface SumSubSdkResult {
success: boolean;
status: string;
errorType?: string;
errorMsg?: string;
}

interface SumSubStatusEvent {
prevStatus: string;
newStatus: string;
}

interface SumSubLogEvent {
message: string;
}

interface SumSubEventPayload {
eventType: string;
payload?: Record<string, unknown>;
}

interface SumSubHandlers {
onStatusChanged?: (event: SumSubStatusEvent) => void;
onLog?: (event: SumSubLogEvent) => void;
onEvent?: (event: SumSubEventPayload) => void;
}

interface SumSubSdkBuilder {
withHandlers(handlers: SumSubHandlers): SumSubSdkBuilder;
withLocale(locale: string): SumSubSdkBuilder;
withDebug(debug: boolean): SumSubSdkBuilder;
withAutoCloseOnApprove(delayMs: number): SumSubSdkBuilder;
withAnalyticsEnabled(enabled: boolean): SumSubSdkBuilder;
withApplicantConf(conf: {email?: string; phone?: string}): SumSubSdkBuilder;
build(): {
launch(): Promise<SumSubSdkResult>;
dismiss(): void;
};
}

const SNSMobileSDK: {
init(
accessToken: string,
onTokenExpired: () => Promise<string>,
): SumSubSdkBuilder;
};

export default SNSMobileSDK;
}
6 changes: 5 additions & 1 deletion ios/BitPayApp/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app requires access to Bluetooth for establishing secure connections with Ledger Nano devices, enabling seamless interaction and management of cryptocurrency transactions.</string>
<key>NSCameraUsageDescription</key>
<string>We need permission to access your camera to scan QR codes.</string>
<string>We need permission to access your camera to scan QR codes and verify your identity.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need permission to access your microphone for identity verification.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need permission to access your photo library to upload identity documents.</string>
<key>NSFaceIDUsageDescription</key>
<string>Enabling Face ID allows you quick and secure access to your account.</string>
<key>NSUserTrackingUsageDescription</key>
Expand Down
3 changes: 3 additions & 0 deletions ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ project = Xcodeproj::Project.open(project_path)
min_ios_version_supported = project.build_configurations.first.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
######

source 'https://cdn.cocoapods.org/'
source 'https://github.com/SumSubstance/Specs.git'

platform :ios, min_ios_version_supported
prepare_react_native_project!

Expand Down
19 changes: 17 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ PODS:
- hermes-engine (0.82.0):
- hermes-engine/Pre-built (= 0.82.0)
- hermes-engine/Pre-built (0.82.0)
- IdensicMobileSDK (1.42.0):
- IdensicMobileSDK/Default (= 1.42.0)
- IdensicMobileSDK/Core (1.42.0)
- IdensicMobileSDK/Default (1.42.0):
- IdensicMobileSDK/Core
- InputMask (6.1.0)
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
Expand Down Expand Up @@ -2119,6 +2124,9 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-mobilesdk-module (1.42.0):
- IdensicMobileSDK (= 1.42.0)
- React-Core
- react-native-netinfo (11.4.1):
- React-Core
- react-native-pager-view (6.7.1):
Expand Down Expand Up @@ -3596,6 +3604,7 @@ DEPENDENCIES:
- react-native-keyevent (from `../node_modules/react-native-keyevent`)
- react-native-mail (from `../node_modules/react-native-mail`)
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
- "react-native-mobilesdk-module (from `../node_modules/@sumsub/react-native-mobilesdk-module`)"
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
- react-native-passkey (from `../node_modules/react-native-passkey`)
Expand Down Expand Up @@ -3671,6 +3680,8 @@ DEPENDENCIES:
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)

SPEC REPOS:
https://github.com/SumSubstance/Specs.git:
- IdensicMobileSDK
trunk:
- AppsFlyerFramework
- BrazeKit
Expand Down Expand Up @@ -3807,6 +3818,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-mail"
react-native-mmkv:
:path: "../node_modules/react-native-mmkv"
react-native-mobilesdk-module:
:path: "../node_modules/@sumsub/react-native-mobilesdk-module"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-pager-view:
Expand Down Expand Up @@ -3967,6 +3980,7 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 8642d8f14a548ab718ec112e9bebdfdd154138b5
IdensicMobileSDK: a8ec2cf5c216ae138b00e2ff32e0e31c5e366cec
InputMask: 71d291dc54d2deaeac6512afb6ec2304228c0bb7
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mixpanel-swift: de454db5987bf6f601106520a44663fc556e8cfa
Expand Down Expand Up @@ -4021,6 +4035,7 @@ SPEC CHECKSUMS:
react-native-keyevent: fa167ff93e90b5d86b1678885669ff8ec099bf09
react-native-mail: 8fdcd3aef007c33a6877a18eb4cf7447a1d4ce4a
react-native-mmkv: 31524b627cc06d5099f45c9128d8e918e0a4a71d
react-native-mobilesdk-module: ac83e25da8dde7dc1455e57d8ce57578cbfad2da
react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
react-native-pager-view: e17f0602f115f6a6a8953e58f2182dbe8eeb0bd5
react-native-passkey: a47610edc6e8a5b829f6679ac169bd9f87e5a51a
Expand Down Expand Up @@ -4099,6 +4114,6 @@ SPEC CHECKSUMS:
VisionCamera: 05e4bc4783174689a5878a0797015ab32afae9e4
Yoga: 93bc00d78638987f9ffd928f4a9f895d3e601bc3

PODFILE CHECKSUM: 91e915b372fe50cffa909299965ad28bb54bc3f7
PODFILE CHECKSUM: ba7e8fb1bcf09886d28c9f47483308a00448ff8b

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
13 changes: 9 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
],
transformIgnorePatterns: [
'\\.snap$',
'node_modules/(?!(@walletconnect/react-native-compat|@freakycoder|@react-native|react-native|(react-native(-.*))|\@react-navigation|(react-navigation(-.*))|\@sentry|uuid|victory|(victory(-.*))|lodash-es))',
'node_modules/(?!(@walletconnect/react-native-compat|@freakycoder|@react-native|react-native|(react-native(-.*))|@react-navigation|(react-navigation(-.*))|@sentry|uuid|victory|(victory(-.*))|lodash-es))',
],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'ts-jest',
Expand All @@ -25,12 +25,17 @@ module.exports = {
'@test/(.*)': '<rootDir>/test/$1',
// Redirect bare styled-components to the native version (some files import
// 'styled-components' instead of 'styled-components/native', which breaks in tests)
'^styled-components$': '<rootDir>/node_modules/styled-components/native/dist/styled-components.native.cjs.js',
'^styled-components$':
'<rootDir>/node_modules/styled-components/native/dist/styled-components.native.cjs.js',
// Force ESM-only crypto packages to CJS builds
'^uuid$': require.resolve('uuid'),
'^paillier-bigint$': '<rootDir>/node_modules/paillier-bigint/dist/cjs/index.node.cjs',
'^bigint-crypto-utils$': '<rootDir>/node_modules/bigint-crypto-utils/dist/cjs/index.node.cjs',
'^paillier-bigint$':
'<rootDir>/node_modules/paillier-bigint/dist/cjs/index.node.cjs',
'^bigint-crypto-utils$':
'<rootDir>/node_modules/bigint-crypto-utils/dist/cjs/index.node.cjs',
'^@env$': '<rootDir>/test/mock.js',
'^@sumsub/react-native-mobilesdk-module$':
'<rootDir>/test/mocks/sumsubSdkMock.js',
},
roots: ['<rootDir>/src/'],
collectCoverageFrom: [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@solana/sysvars": "3.0.2",
"@solana/web3.js": "1.98.2",
"@solana/webcrypto-ed25519-polyfill": "2.1.1",
"@sumsub/react-native-mobilesdk-module": "1.42.0",
"@tradle/react-native-http": "2.0.1",
"@walletconnect/jsonrpc-types": "1.0.4",
"@walletconnect/react-native-compat": "2.21.8",
Expand Down
43 changes: 43 additions & 0 deletions scripts/get-sumsub-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Script to generate a Sumsub SDK token for development.
// Run with `node scripts/get-sumsub-token.js` and paste the output into `src/api/sumsub/index.ts`.
const crypto = require('crypto');

const APP_TOKEN = ''; // Application token from Sumsub dashboard
const SECRET_KEY = ''; // Secret key from Sumsub dashboard
const USER_ID = ''; // BitPay ID of the user for whom you want to generate the token
const LEVEL_NAME = ''; // Level name of the Sumsub flow, e.g. 'basic-kyc-level' or 'advanced-kyc-level'

const method = 'POST';
const path = '/resources/accessTokens/sdk';
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
userId: USER_ID,
levelName: LEVEL_NAME,
ttlInSecs: 600,
});

const signature = crypto
.createHmac('sha256', SECRET_KEY)
.update(ts + method + path + body)
.digest('hex');

fetch(`https://api.sumsub.com${path}`, {
method,
headers: {
'X-App-Token': APP_TOKEN,
'X-App-Access-Ts': ts,
'X-App-Access-Sig': signature,
'Content-Type': 'application/json',
},
body,
})
.then(r => r.json())
.then(data => {
if (data.token) {
console.log('\nPaste into src/api/sumsub/index.ts:\n');
console.log(`export const SUMSUB_DEV_TOKEN = '${data.token}';\n`);
} else {
console.error('Unexpected response:', JSON.stringify(data, null, 2));
}
})
.catch(console.error);
88 changes: 88 additions & 0 deletions src/api/sumsub/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Tests for src/api/sumsub/index.ts (SumSubApi.fetchAccessToken)
*/

import {SumSubApi} from './index';
import {Network} from '../../constants';
import {BASE_BITPAY_URLS} from '../../constants/config';

const API_TOKEN = 'api-token-xyz';
const USER_ID = 'user with spaces+&';

const mockFetch = jest.fn();
// @ts-ignore - override global fetch for the test
global.fetch = mockFetch;

const okResponse = (body: unknown) => ({
ok: true,
status: 200,
json: jest.fn().mockResolvedValue(body),
});

beforeEach(() => {
jest.clearAllMocks();
});

describe('SumSubApi.fetchAccessToken', () => {
it('calls the BitPay token endpoint for the current network with the identity header', async () => {
mockFetch.mockResolvedValue(okResponse({token: 't', userId: USER_ID}));

await SumSubApi.fetchAccessToken(Network.mainnet, API_TOKEN, USER_ID);

expect(mockFetch).toHaveBeenCalledTimes(1);
const [url, options] = mockFetch.mock.calls[0];
expect(url).toContain(`${BASE_BITPAY_URLS[Network.mainnet]}/api/v2/sumsub/token`);
expect(options).toMatchObject({
method: 'GET',
headers: {
'x-identity': API_TOKEN,
'Content-Type': 'application/json',
},
});
});

it('url-encodes the userId query parameter', async () => {
mockFetch.mockResolvedValue(okResponse({token: 't', userId: USER_ID}));

await SumSubApi.fetchAccessToken(Network.mainnet, API_TOKEN, USER_ID);

const [url] = mockFetch.mock.calls[0];
expect(url).toContain(`userId=${encodeURIComponent(USER_ID)}`);
// The raw, un-encoded value must not leak into the URL.
expect(url).not.toContain(USER_ID);
});

it('targets the testnet base URL when network is testnet', async () => {
mockFetch.mockResolvedValue(okResponse({token: 't', userId: USER_ID}));

await SumSubApi.fetchAccessToken(Network.testnet, API_TOKEN, USER_ID);

const [url] = mockFetch.mock.calls[0];
expect(url).toContain(BASE_BITPAY_URLS[Network.testnet]);
});

it('resolves with the parsed JSON token response', async () => {
const body = {token: 'access-token', userId: USER_ID};
mockFetch.mockResolvedValue(okResponse(body));

await expect(
SumSubApi.fetchAccessToken(Network.mainnet, API_TOKEN, USER_ID),
).resolves.toEqual(body);
});

it('throws with the status code when the response is not ok', async () => {
mockFetch.mockResolvedValue({ok: false, status: 401, json: jest.fn()});

await expect(
SumSubApi.fetchAccessToken(Network.mainnet, API_TOKEN, USER_ID),
).rejects.toThrow('Failed to fetch SumSub access token: 401');
});

it('does not swallow network errors from fetch', async () => {
mockFetch.mockRejectedValue(new Error('network down'));

await expect(
SumSubApi.fetchAccessToken(Network.mainnet, API_TOKEN, USER_ID),
).rejects.toThrow('network down');
});
});
Loading
Loading