Skip to content

Commit 7fb2175

Browse files
authored
fix react native demo (#1374)
* fix react native demo * fix tests
1 parent 462a842 commit 7fb2175

File tree

8 files changed

+106
-128
lines changed

8 files changed

+106
-128
lines changed

packages/sdk-multichain/src/domain/platform/index.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,6 @@ export function getPlatformType() {
6565
return PlatformType.DesktopWeb;
6666
}
6767

68-
/**
69-
* Check if MetaMask extension is installed
70-
*/
71-
export function isMetamaskExtensionInstalled(): boolean {
72-
if (typeof window === 'undefined') {
73-
return false;
74-
}
75-
return Boolean(window.ethereum?.isMetaMask);
76-
}
7768

7869
export function isSecure() {
7970
const platformType = getPlatformType();
@@ -82,7 +73,8 @@ export function isSecure() {
8273

8374
// Immediately start MetaMask detection when module loads
8475
const detectionPromise: Promise<boolean> = (() => {
85-
if (typeof window === 'undefined') {
76+
const pt = getPlatformType();
77+
if (pt === PlatformType.NonBrowser || pt === PlatformType.ReactNative) {
8678
return Promise.resolve(false);
8779
}
8880

packages/sdk-multichain/src/multichain/utils/index.test.ts

Lines changed: 19 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/** biome-ignore-all lint/suspicious/noExplicitAny: Tests require it */
22
/** biome-ignore-all lint/style/noNonNullAssertion: Tests require it */
3+
4+
import type { CaipAccountId } from '@metamask/utils';
35
import * as t from 'vitest';
46
import { vi } from 'vitest';
5-
import type { CaipAccountId } from '@metamask/utils';
67
import packageJson from '../../../package.json';
7-
import type { MultichainOptions } from '../../domain/multichain';
8-
import { getPlatformType, isMetamaskExtensionInstalled, PlatformType } from '../../domain/platform';
98
import type { Scope } from '../../domain';
9+
import type { MultichainOptions } from '../../domain/multichain';
10+
import { getPlatformType, PlatformType } from '../../domain/platform';
1011
import * as utils from '.';
1112

1213
vi.mock('../../domain/platform', async () => {
@@ -264,22 +265,6 @@ t.describe('Utils', () => {
264265
});
265266
});
266267

267-
t.describe('isMetamaskExtensionInstalled', () => {
268-
t.it('should return true if MetaMask is installed', () => {
269-
t.vi.stubGlobal('window', {
270-
ethereum: {
271-
isMetaMask: true,
272-
},
273-
});
274-
t.expect(isMetamaskExtensionInstalled()).toBe(true);
275-
});
276-
277-
t.it('should return false if MetaMask is not installed', () => {
278-
t.vi.stubGlobal('window', undefined);
279-
t.expect(isMetamaskExtensionInstalled()).toBe(false);
280-
});
281-
});
282-
283268
t.describe('isSameScopesAndAccounts', () => {
284269
const mockWalletSession = {
285270
sessionScopes: {
@@ -299,34 +284,19 @@ t.describe('Utils', () => {
299284
t.it('should return true when scopes and accounts match exactly', () => {
300285
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
301286
const proposedScopes: Scope[] = ['eip155:1', 'eip155:137'];
302-
const proposedCaipAccountIds = [
303-
'eip155:1:0x1234567890123456789012345678901234567890',
304-
'eip155:137:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
305-
] as CaipAccountId[];
287+
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890', 'eip155:137:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'] as CaipAccountId[];
306288

307-
const result = utils.isSameScopesAndAccounts(
308-
currentScopes,
309-
proposedScopes,
310-
mockWalletSession,
311-
proposedCaipAccountIds,
312-
);
289+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, mockWalletSession, proposedCaipAccountIds);
313290

314291
t.expect(result).toBe(true);
315292
});
316293

317294
t.it('should return false when scopes do not match', () => {
318295
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
319296
const proposedScopes: Scope[] = ['eip155:1', 'eip155:56']; // Different scope
320-
const proposedCaipAccountIds = [
321-
'eip155:1:0x1234567890123456789012345678901234567890',
322-
] as CaipAccountId[];
297+
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890'] as CaipAccountId[];
323298

324-
const result = utils.isSameScopesAndAccounts(
325-
currentScopes,
326-
proposedScopes,
327-
mockWalletSession,
328-
proposedCaipAccountIds,
329-
);
299+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, mockWalletSession, proposedCaipAccountIds);
330300

331301
t.expect(result).toBe(false);
332302
});
@@ -339,12 +309,7 @@ t.describe('Utils', () => {
339309
'eip155:1:0x9999999999999999999999999999999999999999', // Not in session
340310
] as CaipAccountId[];
341311

342-
const result = utils.isSameScopesAndAccounts(
343-
currentScopes,
344-
proposedScopes,
345-
mockWalletSession,
346-
proposedCaipAccountIds,
347-
);
312+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, mockWalletSession, proposedCaipAccountIds);
348313

349314
t.expect(result).toBe(false);
350315
});
@@ -356,12 +321,7 @@ t.describe('Utils', () => {
356321
'eip155:1:0x1234567890123456789012345678901234567890', // Only one account
357322
] as CaipAccountId[];
358323

359-
const result = utils.isSameScopesAndAccounts(
360-
currentScopes,
361-
proposedScopes,
362-
mockWalletSession,
363-
proposedCaipAccountIds,
364-
);
324+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, mockWalletSession, proposedCaipAccountIds);
365325

366326
t.expect(result).toBe(true);
367327
});
@@ -371,12 +331,7 @@ t.describe('Utils', () => {
371331
const proposedScopes: Scope[] = ['eip155:1', 'eip155:137'];
372332
const proposedCaipAccountIds: CaipAccountId[] = [];
373333

374-
const result = utils.isSameScopesAndAccounts(
375-
currentScopes,
376-
proposedScopes,
377-
mockWalletSession,
378-
proposedCaipAccountIds,
379-
)
334+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, mockWalletSession, proposedCaipAccountIds);
380335

381336
t.expect(result).toBe(true);
382337
});
@@ -387,12 +342,7 @@ t.describe('Utils', () => {
387342
const proposedScopes: Scope[] = [];
388343
const proposedCaipAccountIds: CaipAccountId[] = [];
389344

390-
const result = utils.isSameScopesAndAccounts(
391-
currentScopes,
392-
proposedScopes,
393-
emptySession,
394-
proposedCaipAccountIds,
395-
);
345+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, emptySession, proposedCaipAccountIds);
396346

397347
t.expect(result).toBe(true);
398348
});
@@ -412,12 +362,7 @@ t.describe('Utils', () => {
412362
const proposedScopes: Scope[] = ['eip155:1'];
413363
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890'] as CaipAccountId[];
414364

415-
const result = utils.isSameScopesAndAccounts(
416-
currentScopes,
417-
proposedScopes,
418-
sessionWithoutAccounts,
419-
proposedCaipAccountIds,
420-
);
365+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, sessionWithoutAccounts, proposedCaipAccountIds);
421366

422367
t.expect(result).toBe(false);
423368
});
@@ -437,30 +382,17 @@ t.describe('Utils', () => {
437382
const proposedScopes: Scope[] = ['eip155:1'];
438383
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890'] as CaipAccountId[];
439384

440-
const result = utils.isSameScopesAndAccounts(
441-
currentScopes,
442-
proposedScopes,
443-
sessionWithEmptyAccounts,
444-
proposedCaipAccountIds,
445-
);
385+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, sessionWithEmptyAccounts, proposedCaipAccountIds);
446386

447387
t.expect(result).toBe(false);
448388
});
449389

450390
t.it('should return true when scopes have different order but same content', () => {
451391
const currentScopes: Scope[] = ['eip155:1', 'eip155:137'];
452392
const proposedScopes: Scope[] = ['eip155:137', 'eip155:1']; // Different order
453-
const proposedCaipAccountIds = [
454-
'eip155:1:0x1234567890123456789012345678901234567890',
455-
'eip155:137:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
456-
] as CaipAccountId[];
393+
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890', 'eip155:137:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'] as CaipAccountId[];
457394

458-
const result = utils.isSameScopesAndAccounts(
459-
currentScopes,
460-
proposedScopes,
461-
mockWalletSession,
462-
proposedCaipAccountIds,
463-
);
395+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, mockWalletSession, proposedCaipAccountIds);
464396

465397
t.expect(result).toBe(true);
466398
});
@@ -471,26 +403,16 @@ t.describe('Utils', () => {
471403
'eip155:1': {
472404
methods: ['eth_sendTransaction'],
473405
notifications: ['chainChanged'],
474-
accounts: [
475-
'eip155:1:0x1234567890123456789012345678901234567890',
476-
'eip155:1:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
477-
],
406+
accounts: ['eip155:1:0x1234567890123456789012345678901234567890', 'eip155:1:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'],
478407
},
479408
},
480409
} as any;
481410

482411
const currentScopes: Scope[] = ['eip155:1'];
483412
const proposedScopes: Scope[] = ['eip155:1'];
484-
const proposedCaipAccountIds = [
485-
'eip155:1:0x1234567890123456789012345678901234567890',
486-
] as CaipAccountId[];
413+
const proposedCaipAccountIds = ['eip155:1:0x1234567890123456789012345678901234567890'] as CaipAccountId[];
487414

488-
const result = utils.isSameScopesAndAccounts(
489-
currentScopes,
490-
proposedScopes,
491-
sessionWithMultipleAccounts,
492-
proposedCaipAccountIds,
493-
);
415+
const result = utils.isSameScopesAndAccounts(currentScopes, proposedScopes, sessionWithMultipleAccounts, proposedCaipAccountIds);
494416

495417
t.expect(result).toBe(true);
496418
});

playground/multichain-react-native/app/_layout.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
import 'react-native-get-random-values';
2-
32
import { SDKProvider } from '../src/sdk/SDKProvider';
4-
import { Slot } from 'expo-router';
3+
import { Slot, SplashScreen } from 'expo-router';
4+
import { useEffect } from 'react';
5+
import AsyncStorage from '@react-native-async-storage/async-storage';
6+
7+
// Prevent the splash screen from auto-hiding before asset loading is complete.
8+
SplashScreen.preventAutoHideAsync();
59

610
export default function RootLayout() {
11+
useEffect(() => {
12+
const performCleanStartIfNeeded = async () => {
13+
try {
14+
// Check for the environment variable to trigger a hard reset
15+
if (process.env.EXPO_PUBLIC_CLEAR_STORAGE === 'true') {
16+
console.log('[Hard Reset] Wiping all data from AsyncStorage...');
17+
await AsyncStorage.clear();
18+
console.log('[Hard Reset] AsyncStorage has been cleared successfully.');
19+
}
20+
} catch (e) {
21+
console.error('[Hard Reset] Failed to clear AsyncStorage.', e);
22+
} finally {
23+
// Hide the splash screen once the storage check is complete
24+
await SplashScreen.hideAsync();
25+
}
26+
};
27+
28+
performCleanStartIfNeeded();
29+
}, []); // The empty dependency array ensures this effect runs only once on mount.
30+
731
return (
832
<SDKProvider>
933
<Slot />

playground/multichain-react-native/app/index.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ export default function Page() {
8989

9090
{isConnecting && (
9191
<>
92-
<TouchableOpacity onPress={connect} style={sharedStyles.button} disabled>
93-
<Text style={sharedStyles.buttonText}>Connecting...</Text>
94-
</TouchableOpacity>
95-
<TouchableOpacity onPress={disconnect} style={sharedStyles.buttonCancell}>
96-
<Text style={sharedStyles.buttonText}>Cancell</Text>
97-
</TouchableOpacity>
98-
</>
92+
<TouchableOpacity onPress={connect} style={sharedStyles.button} disabled>
93+
<Text style={sharedStyles.buttonText}>Connecting...</Text>
94+
</TouchableOpacity>
95+
<TouchableOpacity onPress={disconnect} style={sharedStyles.buttonCancel}>
96+
<Text style={sharedStyles.buttonText}>Cancel</Text>
97+
</TouchableOpacity>
98+
</>
9999
)}
100100

101101
{isDisconnected && (

playground/multichain-react-native/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.0.0",
44
"main": "expo-router/entry",
55
"scripts": {
6-
"start": "expo start",
6+
"start": "expo start --tunnel",
77
"android": "expo start --android",
88
"ios": "expo start --ios",
99
"web": "expo start --web",
@@ -16,6 +16,7 @@
1616
"@metamask/multichain-sdk": "workspace:^",
1717
"@metamask/utils": "^11.8.1",
1818
"@open-rpc/meta-schema": "^1.14.9",
19+
"@react-native-async-storage/async-storage": "^2.2.0",
1920
"@react-native-picker/picker": "^2.11.2",
2021
"@react-navigation/bottom-tabs": "^7.3.10",
2122
"@react-navigation/elements": "^2.3.8",
@@ -52,6 +53,7 @@
5253
"@metamask/api-specs": "^0.14.0",
5354
"@types/react": "~19.1.10",
5455
"@types/react-native-get-random-values": "^1.8.2",
56+
"cross-env": "^10.1.0",
5557
"eslint": "^9.25.0",
5658
"eslint-config-expo": "~10.0.0",
5759
"typescript": "~5.9.2"

playground/multichain-react-native/src/sdk/SDKProvider.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
import { createMetamaskSDK, type SDKState, type InvokeMethodOptions, type Scope, type SessionData, type MultichainCore } from '@metamask/multichain-sdk';
44
import type { CaipAccountId } from '@metamask/utils';
55
import type React from 'react';
6-
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
6+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
77
import { METAMASK_PROD_CHROME_ID } from '../constants';
88
import { Linking } from 'react-native';
99

1010
const SDKContext = createContext<
1111
| {
12-
session: SessionData | undefined;
13-
state: SDKState;
14-
error: Error | null;
15-
connect: (scopes: Scope[], caipAccountIds: CaipAccountId[]) => Promise<void>;
16-
disconnect: () => Promise<void>;
17-
invokeMethod: (options: InvokeMethodOptions) => Promise<any>;
18-
}
12+
session: SessionData | undefined;
13+
state: SDKState;
14+
error: Error | null;
15+
connect: (scopes: Scope[], caipAccountIds: CaipAccountId[]) => Promise<void>;
16+
disconnect: () => Promise<void>;
17+
invokeMethod: (options: InvokeMethodOptions) => Promise<any>;
18+
}
1919
| undefined
2020
>(undefined);
2121

@@ -33,6 +33,11 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => {
3333
name: 'playground',
3434
url: 'https://playground.metamask.io',
3535
},
36+
mobile: {
37+
preferredOpenLink: (deeplink: string) => {
38+
Linking.openURL(deeplink);
39+
},
40+
},
3641
analytics: {
3742
enabled: false,
3843
},
@@ -72,7 +77,7 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => {
7277
const sdkInstance = await sdkRef.current;
7378
await sdkInstance.connect(scopes, caipAccountIds);
7479
} catch (error) {
75-
setError(error as Error);
80+
setError(error as Error);
7681
}
7782
},
7883
[sdkRef.current],
@@ -87,7 +92,7 @@ export const SDKProvider = ({ children }: { children: React.ReactNode }) => {
8792
const sdkInstance = await sdkRef.current;
8893
return sdkInstance.invokeMethod(options);
8994
} catch (error) {
90-
setError(error as Error);
95+
setError(error as Error);
9196
}
9297
},
9398
[sdkRef.current],

playground/multichain-react-native/src/styles/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export const sharedStyles = StyleSheet.create({
115115
alignItems: 'center',
116116
justifyContent: 'center',
117117
},
118-
buttonCancell: {
118+
buttonCancel: {
119119
backgroundColor: colors.red600,
120120
paddingVertical: 12,
121121
paddingHorizontal: 20,

0 commit comments

Comments
 (0)