@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.
Files changed (69) hide show
  1. package/README.md +67 -0
  2. package/android/build.gradle +37 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/org/thru/walletnative/ThruWebViewBridgeModule.kt +77 -0
  5. package/app.plugin.cjs +101 -0
  6. package/dist/BrowserSDK-CpRFiJsW.d.ts +409 -0
  7. package/dist/index.d.ts +23 -0
  8. package/dist/index.js +941 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/native/react.d.ts +109 -0
  11. package/dist/native/react.js +2381 -0
  12. package/dist/native/react.js.map +1 -0
  13. package/dist/native.d.ts +329 -0
  14. package/dist/native.js +1126 -0
  15. package/dist/native.js.map +1 -0
  16. package/dist/react-ui.d.ts +5 -0
  17. package/dist/react-ui.js +266 -0
  18. package/dist/react-ui.js.map +1 -0
  19. package/dist/react.d.ts +66 -0
  20. package/dist/react.js +1151 -0
  21. package/dist/react.js.map +1 -0
  22. package/expo-module.config.json +6 -0
  23. package/package.json +114 -0
  24. package/src/BrowserSDK.ts +315 -0
  25. package/src/index.ts +27 -0
  26. package/src/interfaces/IThruChain.ts +37 -0
  27. package/src/interfaces/accounts.ts +61 -0
  28. package/src/interfaces/index.ts +9 -0
  29. package/src/interfaces/types.ts +95 -0
  30. package/src/native/NativeSDK.test.ts +819 -0
  31. package/src/native/NativeSDK.ts +773 -0
  32. package/src/native/index.ts +39 -0
  33. package/src/native/provider/NativeProvider.ts +363 -0
  34. package/src/native/provider/WebViewBridge.test.ts +339 -0
  35. package/src/native/provider/WebViewBridge.ts +339 -0
  36. package/src/native/provider/chains/ThruChain.ts +85 -0
  37. package/src/native/provider/shell.html +88 -0
  38. package/src/native/provider/shell.test.ts +56 -0
  39. package/src/native/provider/shell.ts +111 -0
  40. package/src/native/provider/shims-html.d.ts +4 -0
  41. package/src/native/react/ThruContext.ts +37 -0
  42. package/src/native/react/ThruProvider.tsx +168 -0
  43. package/src/native/react/ThruWalletSheet.tsx +1162 -0
  44. package/src/native/react/android-webauthn.ts +37 -0
  45. package/src/native/react/hooks/useAccounts.ts +35 -0
  46. package/src/native/react/hooks/useThru.ts +11 -0
  47. package/src/native/react/hooks/useWallet.ts +71 -0
  48. package/src/native/react/hooks/useWalletAvailability.ts +31 -0
  49. package/src/native/react/hooks/waitForWallet.ts +21 -0
  50. package/src/native/react/index.ts +29 -0
  51. package/src/protocol/index.ts +2 -0
  52. package/src/protocol/postMessage.ts +283 -0
  53. package/src/protocol/walletState.ts +12 -0
  54. package/src/provider/EmbeddedProvider.ts +330 -0
  55. package/src/provider/IframeManager.ts +438 -0
  56. package/src/provider/chains/ThruChain.ts +86 -0
  57. package/src/provider/index.ts +17 -0
  58. package/src/provider/types/messages.ts +37 -0
  59. package/src/react/ThruContext.ts +31 -0
  60. package/src/react/ThruProvider.tsx +169 -0
  61. package/src/react/hooks/useAccounts.ts +38 -0
  62. package/src/react/hooks/useThru.ts +11 -0
  63. package/src/react/hooks/useWallet.ts +81 -0
  64. package/src/react/index.ts +30 -0
  65. package/src/react-ui/ThruAccountSwitcher.tsx +187 -0
  66. package/src/react-ui/custom.d.ts +8 -0
  67. package/src/react-ui/index.ts +1 -0
  68. package/src/static/logo.png +0 -0
  69. 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,4 @@
1
+ declare module '*.html' {
2
+ const content: string;
3
+ export default content;
4
+ }
@@ -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
+ }