@thru/wallet 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
- package/app.plugin.cjs +101 -0
- package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +941 -0
- package/dist/index.js.map +1 -0
- package/dist/native/react.d.ts +109 -0
- package/dist/native/react.js +2381 -0
- package/dist/native/react.js.map +1 -0
- package/dist/native.d.ts +329 -0
- package/dist/native.js +1126 -0
- package/dist/native.js.map +1 -0
- package/dist/react-ui.d.ts +5 -0
- package/dist/react-ui.js +266 -0
- package/dist/react-ui.js.map +1 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +1151 -0
- package/dist/react.js.map +1 -0
- package/expo-module.config.json +6 -0
- package/package.json +114 -0
- package/src/BrowserSDK.ts +315 -0
- package/src/index.ts +27 -0
- package/src/interfaces/IThruChain.ts +37 -0
- package/src/interfaces/accounts.ts +61 -0
- package/src/interfaces/index.ts +9 -0
- package/src/interfaces/types.ts +95 -0
- package/src/native/NativeSDK.test.ts +819 -0
- package/src/native/NativeSDK.ts +773 -0
- package/src/native/index.ts +39 -0
- package/src/native/provider/NativeProvider.ts +363 -0
- package/src/native/provider/WebViewBridge.test.ts +339 -0
- package/src/native/provider/WebViewBridge.ts +339 -0
- package/src/native/provider/chains/ThruChain.ts +85 -0
- package/src/native/provider/shell.html +88 -0
- package/src/native/provider/shell.test.ts +56 -0
- package/src/native/provider/shell.ts +111 -0
- package/src/native/provider/shims-html.d.ts +4 -0
- package/src/native/react/ThruContext.ts +37 -0
- package/src/native/react/ThruProvider.tsx +168 -0
- package/src/native/react/ThruWalletSheet.tsx +1162 -0
- package/src/native/react/android-webauthn.ts +37 -0
- package/src/native/react/hooks/useAccounts.ts +35 -0
- package/src/native/react/hooks/useThru.ts +11 -0
- package/src/native/react/hooks/useWallet.ts +71 -0
- package/src/native/react/hooks/useWalletAvailability.ts +31 -0
- package/src/native/react/hooks/waitForWallet.ts +21 -0
- package/src/native/react/index.ts +29 -0
- package/src/protocol/index.ts +2 -0
- package/src/protocol/postMessage.ts +283 -0
- package/src/protocol/walletState.ts +12 -0
- package/src/provider/EmbeddedProvider.ts +330 -0
- package/src/provider/IframeManager.ts +438 -0
- package/src/provider/chains/ThruChain.ts +86 -0
- package/src/provider/index.ts +17 -0
- package/src/provider/types/messages.ts +37 -0
- package/src/react/ThruContext.ts +31 -0
- package/src/react/ThruProvider.tsx +169 -0
- package/src/react/hooks/useAccounts.ts +38 -0
- package/src/react/hooks/useThru.ts +11 -0
- package/src/react/hooks/useWallet.ts +81 -0
- package/src/react/index.ts +30 -0
- package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
- package/src/react-ui/custom.d.ts +8 -0
- package/src/react-ui/index.ts +1 -0
- package/src/static/logo.png +0 -0
- package/src/static/logomark_red.svg +11 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AddressType,
|
|
3
|
+
type IThruChain,
|
|
4
|
+
type ThruSigningContext,
|
|
5
|
+
type ThruTransactionIntent,
|
|
6
|
+
} from "../../../interfaces";
|
|
7
|
+
import { POST_MESSAGE_REQUEST_TYPES, createRequestId } from "../../../protocol";
|
|
8
|
+
import type { NativeProvider } from "../NativeProvider";
|
|
9
|
+
import type { WebViewBridge } from "../WebViewBridge";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* NativeThruChain - mirror of EmbeddedThruChain over the WebView bridge.
|
|
13
|
+
* Sign moments toggle the host bottom sheet via provider.requestShow /
|
|
14
|
+
* requestHide instead of iframe.show / hide.
|
|
15
|
+
*/
|
|
16
|
+
export class NativeThruChain implements IThruChain {
|
|
17
|
+
private readonly bridge: WebViewBridge;
|
|
18
|
+
private readonly provider: NativeProvider;
|
|
19
|
+
private readonly origin: string;
|
|
20
|
+
|
|
21
|
+
constructor(bridge: WebViewBridge, provider: NativeProvider, origin: string) {
|
|
22
|
+
this.bridge = bridge;
|
|
23
|
+
this.provider = provider;
|
|
24
|
+
this.origin = origin;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get connected(): boolean {
|
|
28
|
+
return this.provider.isConnected();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async connect(): Promise<{ publicKey: string }> {
|
|
32
|
+
const result = await this.provider.connect();
|
|
33
|
+
const selectedAccount = result.selectedAccount;
|
|
34
|
+
const thruAccount =
|
|
35
|
+
selectedAccount?.accountType === AddressType.THRU
|
|
36
|
+
? selectedAccount
|
|
37
|
+
: result.accounts.find((addr) => addr.accountType === AddressType.THRU);
|
|
38
|
+
if (!thruAccount) {
|
|
39
|
+
throw new Error("Thru address not found in connection result");
|
|
40
|
+
}
|
|
41
|
+
return { publicKey: thruAccount.address };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async disconnect(): Promise<void> {
|
|
45
|
+
await this.provider.disconnect();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getSigningContext(): Promise<ThruSigningContext> {
|
|
49
|
+
if (!this.provider.isConnected()) {
|
|
50
|
+
throw new Error("Wallet not connected");
|
|
51
|
+
}
|
|
52
|
+
const response = await this.bridge.sendMessage({
|
|
53
|
+
id: createRequestId(),
|
|
54
|
+
type: POST_MESSAGE_REQUEST_TYPES.GET_SIGNING_CONTEXT,
|
|
55
|
+
origin: this.origin,
|
|
56
|
+
});
|
|
57
|
+
return response.result.signingContext;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async signTransaction(transaction: ThruTransactionIntent): Promise<string> {
|
|
61
|
+
if (!this.provider.isConnected()) {
|
|
62
|
+
throw new Error("Wallet not connected");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.provider.requestShow();
|
|
66
|
+
try {
|
|
67
|
+
const response = await this.bridge.sendMessage({
|
|
68
|
+
id: createRequestId(),
|
|
69
|
+
type: POST_MESSAGE_REQUEST_TYPES.SIGN_TRANSACTION,
|
|
70
|
+
payload: {
|
|
71
|
+
walletAddress: transaction.walletAddress,
|
|
72
|
+
programAddress: transaction.programAddress,
|
|
73
|
+
instructionData: transaction.instructionData,
|
|
74
|
+
readWriteAddresses: transaction.readWriteAddresses,
|
|
75
|
+
readOnlyAddresses: transaction.readOnlyAddresses,
|
|
76
|
+
review: transaction.review,
|
|
77
|
+
},
|
|
78
|
+
origin: this.origin,
|
|
79
|
+
});
|
|
80
|
+
return response.result.signedTransaction;
|
|
81
|
+
} finally {
|
|
82
|
+
this.provider.requestHide();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<title>thru-shell</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body, iframe {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
border: 0;
|
|
14
|
+
background: transparent;
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<iframe
|
|
20
|
+
id="w"
|
|
21
|
+
data-src="WALLET_URL_PLACEHOLDER"
|
|
22
|
+
allow="publickey-credentials-get *; publickey-credentials-create *"
|
|
23
|
+
></iframe>
|
|
24
|
+
<script>
|
|
25
|
+
(function () {
|
|
26
|
+
var f = document.getElementById('w');
|
|
27
|
+
var ORIGIN = 'WALLET_ORIGIN_PLACEHOLDER';
|
|
28
|
+
function frameId() {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(f.dataset.src).searchParams.get('tn_frame_id');
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function postShell(type, data) {
|
|
36
|
+
var rn = window.ReactNativeWebView;
|
|
37
|
+
if (!rn || !rn.postMessage) return;
|
|
38
|
+
try {
|
|
39
|
+
rn.postMessage(JSON.stringify({
|
|
40
|
+
type: type,
|
|
41
|
+
frameId: frameId(),
|
|
42
|
+
data: data || {}
|
|
43
|
+
}));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
/* drop unserializable messages */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function postToWallet(msg) {
|
|
49
|
+
if (!f.contentWindow) return;
|
|
50
|
+
var outbound = msg;
|
|
51
|
+
if (msg && typeof msg === 'object') {
|
|
52
|
+
outbound = Object.assign({}, msg, { frameId: frameId() });
|
|
53
|
+
}
|
|
54
|
+
f.contentWindow.postMessage(outbound, ORIGIN);
|
|
55
|
+
}
|
|
56
|
+
window.addEventListener('message', function (e) {
|
|
57
|
+
var fromFrame = e.source === f.contentWindow;
|
|
58
|
+
var fromWalletOrigin = e.origin === ORIGIN;
|
|
59
|
+
var hasFrameId = e.data && e.data.frameId === frameId();
|
|
60
|
+
if (!fromWalletOrigin || (!fromFrame && !hasFrameId)) return;
|
|
61
|
+
var rn = window.ReactNativeWebView;
|
|
62
|
+
if (rn && rn.postMessage) {
|
|
63
|
+
try {
|
|
64
|
+
rn.postMessage(JSON.stringify(e.data));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
/* drop unserializable messages */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
window.__pushIn = postToWallet;
|
|
71
|
+
window.addEventListener('thru:native-sheet-dismiss', function () {
|
|
72
|
+
postToWallet({
|
|
73
|
+
type: 'thru:native-sheet-dismiss',
|
|
74
|
+
frameId: frameId()
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
f.addEventListener('load', function () {
|
|
78
|
+
postShell('shell:iframe-load', { src: f.src });
|
|
79
|
+
});
|
|
80
|
+
f.addEventListener('error', function () {
|
|
81
|
+
postShell('shell:iframe-error', { src: f.src });
|
|
82
|
+
});
|
|
83
|
+
postShell('shell:loading', { src: f.dataset.src });
|
|
84
|
+
f.src = f.dataset.src;
|
|
85
|
+
})();
|
|
86
|
+
</script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { getShellHtml } from './shell';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const TEST_SHELL_OPTIONS = {
|
|
9
|
+
walletUrl: 'https://wallet.thru.org/embedded?tn_frame_id=frame_test',
|
|
10
|
+
walletOrigin: 'https://wallet.thru.org',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('native shell HTML', () => {
|
|
14
|
+
it('forwards WKWebView iframe messages when event.source is unavailable', () => {
|
|
15
|
+
const html = getShellHtml(TEST_SHELL_OPTIONS);
|
|
16
|
+
|
|
17
|
+
expect(html).toContain('var fromFrame = e.source === f.contentWindow;');
|
|
18
|
+
expect(html).toContain('var fromWalletOrigin = e.origin === ORIGIN;');
|
|
19
|
+
expect(html).toContain(
|
|
20
|
+
'var hasFrameId = e.data && e.data.frameId === frameId();'
|
|
21
|
+
);
|
|
22
|
+
expect(html).toContain(
|
|
23
|
+
'if (!fromWalletOrigin || (!fromFrame && !hasFrameId)) return;'
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('forwards native sheet dismissals into the wallet iframe', () => {
|
|
28
|
+
const html = getShellHtml(TEST_SHELL_OPTIONS);
|
|
29
|
+
|
|
30
|
+
expect(html).toContain(
|
|
31
|
+
"window.addEventListener('thru:native-sheet-dismiss', function () {"
|
|
32
|
+
);
|
|
33
|
+
expect(html).toContain("type: 'thru:native-sheet-dismiss'");
|
|
34
|
+
expect(html).toContain('postToWallet({');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('substitutes shell placeholders without reprocessing inserted values', () => {
|
|
38
|
+
const walletUrl =
|
|
39
|
+
'https://wallet.thru.org/embedded?marker=WALLET_ORIGIN_PLACEHOLDER';
|
|
40
|
+
const walletOrigin = 'thru-mobile://WALLET_URL_PLACEHOLDER/$&';
|
|
41
|
+
|
|
42
|
+
const html = getShellHtml({ walletUrl, walletOrigin });
|
|
43
|
+
|
|
44
|
+
expect(html).toContain(`data-src="${walletUrl}"`);
|
|
45
|
+
expect(html).toContain(`var ORIGIN = '${walletOrigin}';`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('tags native requests with the wallet frame id before forwarding', () => {
|
|
49
|
+
const html = getShellHtml(TEST_SHELL_OPTIONS);
|
|
50
|
+
|
|
51
|
+
expect(html).toContain(
|
|
52
|
+
'outbound = Object.assign({}, msg, { frameId: frameId() });'
|
|
53
|
+
);
|
|
54
|
+
expect(html).toContain('f.contentWindow.postMessage(outbound, ORIGIN);');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const SHELL_HTML_TEMPLATE = String.raw`<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<title>thru-shell</title>
|
|
7
|
+
<style>
|
|
8
|
+
html, body, iframe {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
border: 0;
|
|
14
|
+
background: transparent;
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<iframe
|
|
20
|
+
id="w"
|
|
21
|
+
data-src="WALLET_URL_PLACEHOLDER"
|
|
22
|
+
allow="publickey-credentials-get *; publickey-credentials-create *"
|
|
23
|
+
></iframe>
|
|
24
|
+
<script>
|
|
25
|
+
(function () {
|
|
26
|
+
var f = document.getElementById('w');
|
|
27
|
+
var ORIGIN = 'WALLET_ORIGIN_PLACEHOLDER';
|
|
28
|
+
function frameId() {
|
|
29
|
+
try {
|
|
30
|
+
return new URL(f.dataset.src).searchParams.get('tn_frame_id');
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function postShell(type, data) {
|
|
36
|
+
var rn = window.ReactNativeWebView;
|
|
37
|
+
if (!rn || !rn.postMessage) return;
|
|
38
|
+
try {
|
|
39
|
+
rn.postMessage(JSON.stringify({
|
|
40
|
+
type: type,
|
|
41
|
+
frameId: frameId(),
|
|
42
|
+
data: data || {}
|
|
43
|
+
}));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
/* drop unserializable messages */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function postToWallet(msg) {
|
|
49
|
+
if (!f.contentWindow) return;
|
|
50
|
+
var outbound = msg;
|
|
51
|
+
if (msg && typeof msg === 'object') {
|
|
52
|
+
outbound = Object.assign({}, msg, { frameId: frameId() });
|
|
53
|
+
}
|
|
54
|
+
f.contentWindow.postMessage(outbound, ORIGIN);
|
|
55
|
+
}
|
|
56
|
+
window.addEventListener('message', function (e) {
|
|
57
|
+
var fromFrame = e.source === f.contentWindow;
|
|
58
|
+
var fromWalletOrigin = e.origin === ORIGIN;
|
|
59
|
+
var hasFrameId = e.data && e.data.frameId === frameId();
|
|
60
|
+
if (!fromWalletOrigin || (!fromFrame && !hasFrameId)) return;
|
|
61
|
+
var rn = window.ReactNativeWebView;
|
|
62
|
+
if (rn && rn.postMessage) {
|
|
63
|
+
try {
|
|
64
|
+
rn.postMessage(JSON.stringify(e.data));
|
|
65
|
+
} catch (err) {
|
|
66
|
+
/* drop unserializable messages */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
window.__pushIn = postToWallet;
|
|
71
|
+
window.addEventListener('thru:native-sheet-dismiss', function () {
|
|
72
|
+
postToWallet({
|
|
73
|
+
type: 'thru:native-sheet-dismiss',
|
|
74
|
+
frameId: frameId()
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
f.addEventListener('load', function () {
|
|
78
|
+
postShell('shell:iframe-load', { src: f.src });
|
|
79
|
+
});
|
|
80
|
+
f.addEventListener('error', function () {
|
|
81
|
+
postShell('shell:iframe-error', { src: f.src });
|
|
82
|
+
});
|
|
83
|
+
postShell('shell:loading', { src: f.dataset.src });
|
|
84
|
+
f.src = f.dataset.src;
|
|
85
|
+
})();
|
|
86
|
+
</script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>`;
|
|
89
|
+
const SHELL_PLACEHOLDER_PATTERN =
|
|
90
|
+
/WALLET_URL_PLACEHOLDER|WALLET_ORIGIN_PLACEHOLDER/g;
|
|
91
|
+
|
|
92
|
+
export interface ShellOptions {
|
|
93
|
+
walletUrl: string;
|
|
94
|
+
walletOrigin: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns the shell HTML for loading wallet.thru.org/embedded inside a
|
|
99
|
+
* react-native-webview. The shell hosts an <iframe> pointing at the wallet
|
|
100
|
+
* and bridges window.postMessage traffic between the iframe and the
|
|
101
|
+
* react-native-webview's onMessage / injectJavaScript channels.
|
|
102
|
+
*
|
|
103
|
+
* Caller substitutes the placeholders with the runtime wallet URL + origin.
|
|
104
|
+
*/
|
|
105
|
+
export function getShellHtml(opts: ShellOptions): string {
|
|
106
|
+
return SHELL_HTML_TEMPLATE.replace(SHELL_PLACEHOLDER_PATTERN, (placeholder) =>
|
|
107
|
+
placeholder === 'WALLET_URL_PLACEHOLDER'
|
|
108
|
+
? opts.walletUrl
|
|
109
|
+
: opts.walletOrigin
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
NativeSDK,
|
|
4
|
+
WalletAvailability,
|
|
5
|
+
} from "../NativeSDK";
|
|
6
|
+
import type { WalletAccount } from "../../interfaces";
|
|
7
|
+
import type { ManageAccountsResult } from "../../protocol";
|
|
8
|
+
|
|
9
|
+
export const CHECKING_WALLET_AVAILABILITY: WalletAvailability = {
|
|
10
|
+
status: 'checking',
|
|
11
|
+
isAuthorized: false,
|
|
12
|
+
isConnected: false,
|
|
13
|
+
isUnlocked: false,
|
|
14
|
+
hasPasskey: false,
|
|
15
|
+
hasWalletAccount: false,
|
|
16
|
+
accounts: [],
|
|
17
|
+
selectedAccount: null,
|
|
18
|
+
metadata: null,
|
|
19
|
+
error: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface ThruContextValue {
|
|
23
|
+
/** Initialized NativeSDK instance, or null while still constructing. */
|
|
24
|
+
wallet: NativeSDK | null;
|
|
25
|
+
/** Lazily-instantiated Thru chain client (cast at the call site). */
|
|
26
|
+
thru: unknown;
|
|
27
|
+
isConnected: boolean;
|
|
28
|
+
isConnecting: boolean;
|
|
29
|
+
accounts: WalletAccount[];
|
|
30
|
+
selectedAccount: WalletAccount | null;
|
|
31
|
+
walletAvailability: WalletAvailability;
|
|
32
|
+
error: Error | null;
|
|
33
|
+
selectAccount: (account: WalletAccount) => Promise<void>;
|
|
34
|
+
manageAccounts: () => Promise<ManageAccountsResult>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ThruContext = createContext<ThruContextValue | null>(null);
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* React Native mirror of `@thru/wallet/react`'s ThruProvider. Owns the
|
|
2
|
+
NativeSDK instance, mirrors its events into context state. The
|
|
3
|
+
bottom sheet is a separate component (<ThruWalletSheet>) the host
|
|
4
|
+
composes alongside this provider. */
|
|
5
|
+
|
|
6
|
+
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
|
7
|
+
import {
|
|
8
|
+
NativeSDK,
|
|
9
|
+
type NativeSDKConfig,
|
|
10
|
+
type WalletAvailability,
|
|
11
|
+
} from "../NativeSDK";
|
|
12
|
+
import type { WalletAccount } from "../../interfaces";
|
|
13
|
+
import { CHECKING_WALLET_AVAILABILITY, ThruContext } from "./ThruContext";
|
|
14
|
+
|
|
15
|
+
export interface ThruProviderProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
config: NativeSDKConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ThruProvider({ children, config }: ThruProviderProps) {
|
|
21
|
+
const [sdk, setSdk] = useState<NativeSDK | null>(null);
|
|
22
|
+
const [thru, setThru] = useState<unknown>(null);
|
|
23
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
24
|
+
const [accounts, setAccounts] = useState<WalletAccount[]>([]);
|
|
25
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
26
|
+
const [error, setError] = useState<Error | null>(null);
|
|
27
|
+
const [selectedAccount, setSelectedAccount] = useState<WalletAccount | null>(
|
|
28
|
+
null,
|
|
29
|
+
);
|
|
30
|
+
const [walletAvailability, setWalletAvailability] =
|
|
31
|
+
useState<WalletAvailability>(CHECKING_WALLET_AVAILABILITY);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const sdkInstance = new NativeSDK(config);
|
|
35
|
+
setSdk(sdkInstance);
|
|
36
|
+
/* getThru() is lazy in NativeSDK; pull it eagerly so consumers see
|
|
37
|
+
a stable reference. */
|
|
38
|
+
setThru(sdkInstance.getThru());
|
|
39
|
+
|
|
40
|
+
const updateAccountsFromSdk = () => setAccounts(sdkInstance.getAccounts());
|
|
41
|
+
|
|
42
|
+
const updateSelectedAccount = (account?: WalletAccount | null) => {
|
|
43
|
+
if (account) {
|
|
44
|
+
setSelectedAccount(account);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const fallback =
|
|
48
|
+
sdkInstance.getSelectedAccount() ??
|
|
49
|
+
sdkInstance.getAccounts()[0] ??
|
|
50
|
+
null;
|
|
51
|
+
setSelectedAccount(fallback);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/* Initialization is lazy: NativeSDK.connect() will call initialize
|
|
55
|
+
on demand. We don't pre-initialize because the bridge needs a
|
|
56
|
+
WebView ref attached first by ThruWalletSheet. */
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
const handleConnect = (result: any) => {
|
|
60
|
+
if (result?.status === "connecting") {
|
|
61
|
+
setIsConnecting(true);
|
|
62
|
+
setError(null);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setIsConnected(true);
|
|
66
|
+
updateAccountsFromSdk();
|
|
67
|
+
setIsConnecting(false);
|
|
68
|
+
setError(null);
|
|
69
|
+
setWalletAvailability(sdkInstance.getWalletAvailability());
|
|
70
|
+
updateSelectedAccount();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const resetData = () => {
|
|
74
|
+
setIsConnected(false);
|
|
75
|
+
setAccounts([]);
|
|
76
|
+
setIsConnecting(false);
|
|
77
|
+
setSelectedAccount(null);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleDisconnect = () => resetData();
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
82
|
+
const handleError = (err: any) => {
|
|
83
|
+
setError(err?.error ?? err ?? new Error("Unknown error"));
|
|
84
|
+
setIsConnecting(false);
|
|
85
|
+
setWalletAvailability(sdkInstance.getWalletAvailability());
|
|
86
|
+
};
|
|
87
|
+
const handleLock = () => resetData();
|
|
88
|
+
const handleAccountChanged = (
|
|
89
|
+
account: WalletAccount | null | undefined,
|
|
90
|
+
) => {
|
|
91
|
+
updateAccountsFromSdk();
|
|
92
|
+
updateSelectedAccount(account ?? undefined);
|
|
93
|
+
};
|
|
94
|
+
const handleAvailabilityChanged = (availability: WalletAvailability) => {
|
|
95
|
+
setWalletAvailability(availability);
|
|
96
|
+
};
|
|
97
|
+
sdkInstance.on("connect", handleConnect);
|
|
98
|
+
sdkInstance.on("disconnect", handleDisconnect);
|
|
99
|
+
sdkInstance.on("error", handleError);
|
|
100
|
+
sdkInstance.on("lock", handleLock);
|
|
101
|
+
sdkInstance.on("accountChanged", handleAccountChanged);
|
|
102
|
+
sdkInstance.on("availabilityChanged", handleAvailabilityChanged);
|
|
103
|
+
|
|
104
|
+
void sdkInstance.restoreConnection({ hydrate: false }).catch(handleError);
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
sdkInstance.off("connect", handleConnect);
|
|
108
|
+
sdkInstance.off("disconnect", handleDisconnect);
|
|
109
|
+
sdkInstance.off("error", handleError);
|
|
110
|
+
sdkInstance.off("lock", handleLock);
|
|
111
|
+
sdkInstance.off("accountChanged", handleAccountChanged);
|
|
112
|
+
sdkInstance.off("availabilityChanged", handleAvailabilityChanged);
|
|
113
|
+
sdkInstance.destroy();
|
|
114
|
+
};
|
|
115
|
+
/* Empty deps: SDK is constructed once; config changes after mount
|
|
116
|
+
are intentionally ignored to mirror @thru/wallet/react semantics. */
|
|
117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const selectAccount = useCallback(
|
|
121
|
+
async (account: WalletAccount) => {
|
|
122
|
+
if (!sdk) throw new Error("NativeSDK not initialized");
|
|
123
|
+
try {
|
|
124
|
+
const updated = await sdk.selectAccount(account.address);
|
|
125
|
+
setSelectedAccount(updated);
|
|
126
|
+
setAccounts([updated]);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
setError(
|
|
129
|
+
err instanceof Error ? err : new Error("selectAccount failed"),
|
|
130
|
+
);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
[sdk],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const manageAccounts = useCallback(async () => {
|
|
138
|
+
if (!sdk) throw new Error("NativeSDK not initialized");
|
|
139
|
+
try {
|
|
140
|
+
const result = await sdk.manageAccounts();
|
|
141
|
+
setSelectedAccount(result.selectedAccount);
|
|
142
|
+
setAccounts(result.accounts);
|
|
143
|
+
return result;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
setError(err instanceof Error ? err : new Error("manageAccounts failed"));
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}, [sdk]);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<ThruContext.Provider
|
|
152
|
+
value={{
|
|
153
|
+
thru,
|
|
154
|
+
wallet: sdk,
|
|
155
|
+
isConnected,
|
|
156
|
+
accounts,
|
|
157
|
+
isConnecting,
|
|
158
|
+
error,
|
|
159
|
+
selectedAccount,
|
|
160
|
+
walletAvailability,
|
|
161
|
+
selectAccount,
|
|
162
|
+
manageAccounts,
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{children}
|
|
166
|
+
</ThruContext.Provider>
|
|
167
|
+
);
|
|
168
|
+
}
|