@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,39 @@
1
+ export { NativeSDK } from './NativeSDK';
2
+ export type {
3
+ EventCallback,
4
+ ConnectOptions,
5
+ IosWebViewMode,
6
+ NativeSDKConfig,
7
+ NativeSDKStorage,
8
+ NativeSDKUiHandlers,
9
+ RestoreConnectionOptions,
10
+ SDKEvent,
11
+ SignInOptions,
12
+ WalletAvailability,
13
+ } from './NativeSDK';
14
+
15
+ export {
16
+ AddressType,
17
+ ThruTransactionEncoding,
18
+ } from '../interfaces';
19
+ export type {
20
+ AppMetadata,
21
+ ConnectResult,
22
+ IThruChain,
23
+ SignMessageParams,
24
+ SignMessageResult,
25
+ ThruSigningContext,
26
+ ThruTransactionIntent,
27
+ WalletAccount,
28
+ } from '../interfaces';
29
+
30
+ export {
31
+ EMBEDDED_PROVIDER_EVENTS,
32
+ ErrorCode,
33
+ POST_MESSAGE_REQUEST_TYPES,
34
+ } from '../protocol';
35
+ export type {
36
+ ConnectMetadataInput,
37
+ GetConnectionStateResult,
38
+ ManageAccountsResult,
39
+ } from '../protocol';
@@ -0,0 +1,363 @@
1
+ import {
2
+ AddressType,
3
+ normalizeWalletAccountResult,
4
+ resolveWalletAccountByAddress,
5
+ type AddressType as AddressTypeValue,
6
+ type ConnectResult,
7
+ type IThruChain,
8
+ type WalletAccount,
9
+ } from "../../interfaces";
10
+ import {
11
+ EMBEDDED_PROVIDER_EVENTS,
12
+ POST_MESSAGE_REQUEST_TYPES,
13
+ createRequestId,
14
+ type ConnectMetadataInput,
15
+ type ConnectRequestPayload,
16
+ type EmbeddedProviderEvent,
17
+ type GetConnectionStateResult,
18
+ type ManageAccountsResult,
19
+ type SelectAccountPayload,
20
+ normalizeConnectionStateResult,
21
+ } from "../../protocol";
22
+ import { NativeThruChain } from "./chains/ThruChain";
23
+ import {
24
+ WebViewBridge,
25
+ type WebViewMessageEventLike,
26
+ type WebViewRefLike,
27
+ } from "./WebViewBridge";
28
+
29
+ const DEFAULT_WALLET_URL = "https://wallet.thru.org/embedded/native";
30
+ const DEFAULT_ORIGIN = "thru-mobile://app";
31
+
32
+ export interface NativeProviderConfig {
33
+ /** wallet.thru.org/embedded/native URL to load. */
34
+ walletUrl?: string;
35
+ /** Caller-supplied dapp origin. Stamped on every postMessage so
36
+ wallet's ConnectedAppsStorage can scope per-host. */
37
+ origin?: string;
38
+ addressTypes?: AddressTypeValue[];
39
+ }
40
+
41
+ export interface ConnectOptions {
42
+ metadata?: ConnectMetadataInput;
43
+ preferredAccountAddress?: string;
44
+ intent?: ConnectRequestPayload["intent"];
45
+ }
46
+
47
+ export type NativeProviderEvent = EmbeddedProviderEvent;
48
+ export type NativeProviderEventCallback = (data?: unknown) => void;
49
+
50
+ /**
51
+ * RN-side analog of `web/packages/embedded-provider/src/EmbeddedProvider.ts`.
52
+ * Same public surface (connect/disconnect/sign/getAccounts/etc.) over a
53
+ * WebView+iframe bridge instead of a same-origin iframe. Visibility is
54
+ * delegated to the host (ThruWalletSheet) via `requestShow` /
55
+ * `requestHide` callbacks - bottom sheet logic stays in the React layer.
56
+ */
57
+ export class NativeProvider {
58
+ private readonly bridge: WebViewBridge;
59
+ private readonly origin: string;
60
+ private _thruChain?: IThruChain;
61
+ private connected = false;
62
+ private accounts: WalletAccount[] = [];
63
+ private selectedAccount: WalletAccount | null = null;
64
+ private readonly eventListeners = new Map<
65
+ string,
66
+ Set<NativeProviderEventCallback>
67
+ >();
68
+
69
+ /** Set by the host bottom sheet to react to UI_SHOW / completion. */
70
+ public onShowRequested?: () => void;
71
+ public onHideRequested?: () => void;
72
+
73
+ constructor(config: NativeProviderConfig = {}) {
74
+ const walletUrl = config.walletUrl ?? DEFAULT_WALLET_URL;
75
+ this.origin = config.origin ?? DEFAULT_ORIGIN;
76
+ this.bridge = new WebViewBridge({ walletUrl });
77
+
78
+ this.bridge.onEvent = (eventType, payload) => {
79
+ this.emit(eventType as NativeProviderEvent, payload);
80
+
81
+ if (eventType === EMBEDDED_PROVIDER_EVENTS.UI_SHOW) {
82
+ this.requestShow();
83
+ return;
84
+ }
85
+
86
+ if (
87
+ eventType === EMBEDDED_PROVIDER_EVENTS.DISCONNECT ||
88
+ eventType === EMBEDDED_PROVIDER_EVENTS.LOCK
89
+ ) {
90
+ this.clearConnection();
91
+ this.requestHide();
92
+ return;
93
+ }
94
+
95
+ if (eventType === EMBEDDED_PROVIDER_EVENTS.ACCOUNT_CHANGED) {
96
+ const account =
97
+ (payload as { account?: WalletAccount } | null | undefined)
98
+ ?.account ?? null;
99
+ this.refreshAccountCache(account);
100
+ }
101
+ };
102
+
103
+ const addressTypes = config.addressTypes ?? [AddressType.THRU];
104
+ if (addressTypes.includes(AddressType.THRU)) {
105
+ this._thruChain = new NativeThruChain(this.bridge, this, this.origin);
106
+ }
107
+ }
108
+
109
+ /** Hand the bridge a WebView ref. Required before connect/sign. */
110
+ attachWebView(ref: WebViewRefLike): void {
111
+ this.bridge.attachWebView(ref);
112
+ }
113
+
114
+ /** Mark a direct top-level WebView wallet document as ready. */
115
+ markWebViewReady(): void {
116
+ this.bridge.markReady();
117
+ }
118
+
119
+ /** Pass through the WebView's `onMessage` event handler. */
120
+ onMessage = (event: WebViewMessageEventLike): void => {
121
+ this.bridge.onMessage(event);
122
+ };
123
+
124
+ /** Build the URL to load inside the shell <iframe>. The host shell
125
+ template should substitute this for WALLET_URL_PLACEHOLDER. */
126
+ getIframeSrc(): string {
127
+ return this.bridge.getIframeSrc();
128
+ }
129
+
130
+ /** Wallet origin (e.g. https://wallet.thru.org). The shell template
131
+ should substitute this for WALLET_ORIGIN_PLACEHOLDER. */
132
+ getWalletOrigin(): string {
133
+ return this.bridge.walletOrigin;
134
+ }
135
+
136
+ /** Wait for the iframe's IFRAME_READY_EVENT handshake. */
137
+ async initialize(): Promise<void> {
138
+ await this.bridge.awaitReady();
139
+ }
140
+
141
+ /** Open the wallet UI (called internally; also exposed for host). */
142
+ requestShow(): void {
143
+ this.onShowRequested?.();
144
+ }
145
+
146
+ /** Close the wallet UI (called internally; also exposed for host). */
147
+ requestHide(): void {
148
+ this.onHideRequested?.();
149
+ }
150
+
151
+ /** Reject pending requests after a user-driven native sheet dismiss. */
152
+ rejectPendingRequests(message?: string): void {
153
+ this.bridge.rejectPendingRequests(message);
154
+ }
155
+
156
+ async connect(options?: ConnectOptions): Promise<ConnectResult> {
157
+ this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_START, {});
158
+ try {
159
+ this.requestShow();
160
+ const payload: ConnectRequestPayload = {};
161
+ if (options?.metadata) payload.metadata = options.metadata;
162
+ if (options?.preferredAccountAddress) {
163
+ payload.preferredAccountAddress = options.preferredAccountAddress;
164
+ }
165
+ if (options?.intent) payload.intent = options.intent;
166
+
167
+ const response = await this.bridge.sendMessage({
168
+ id: createRequestId(),
169
+ type: POST_MESSAGE_REQUEST_TYPES.CONNECT,
170
+ payload,
171
+ origin: this.origin,
172
+ });
173
+
174
+ const result = normalizeWalletAccountResult(response.result);
175
+ this.connected = true;
176
+ this.accounts = result.accounts;
177
+ this.selectedAccount = result.selectedAccount;
178
+
179
+ this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT, result);
180
+ this.requestHide();
181
+ return result;
182
+ } catch (error) {
183
+ this.requestHide();
184
+ this.emit(EMBEDDED_PROVIDER_EVENTS.CONNECT_ERROR, { error });
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ async getConnectionState(
190
+ options?: ConnectOptions,
191
+ ): Promise<GetConnectionStateResult> {
192
+ const payload: ConnectRequestPayload = {};
193
+ if (options?.metadata) payload.metadata = options.metadata;
194
+ if (options?.preferredAccountAddress) {
195
+ payload.preferredAccountAddress = options.preferredAccountAddress;
196
+ }
197
+
198
+ const response = await this.bridge.sendMessage({
199
+ id: createRequestId(),
200
+ type: POST_MESSAGE_REQUEST_TYPES.GET_CONNECTION_STATE,
201
+ payload,
202
+ origin: this.origin,
203
+ });
204
+
205
+ const result = normalizeConnectionStateResult(response.result);
206
+
207
+ if (
208
+ result.isAuthorized &&
209
+ result.hasPasskey &&
210
+ result.accounts.length > 0
211
+ ) {
212
+ this.hydrateConnection(
213
+ {
214
+ accounts: result.accounts,
215
+ status: "completed",
216
+ metadata: result.metadata ?? undefined,
217
+ selectedAccount: result.selectedAccount,
218
+ },
219
+ result.selectedAccount?.address ?? null,
220
+ );
221
+ } else {
222
+ this.clearConnection();
223
+ }
224
+
225
+ return result;
226
+ }
227
+
228
+ async disconnect(): Promise<void> {
229
+ try {
230
+ await this.bridge.sendMessage({
231
+ id: createRequestId(),
232
+ type: POST_MESSAGE_REQUEST_TYPES.DISCONNECT,
233
+ origin: this.origin,
234
+ });
235
+ this.clearConnection();
236
+ this.emit(EMBEDDED_PROVIDER_EVENTS.DISCONNECT, {});
237
+ } catch (error) {
238
+ this.clearConnection();
239
+ this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
240
+ throw error;
241
+ } finally {
242
+ this.requestHide();
243
+ }
244
+ }
245
+
246
+ isConnected(): boolean {
247
+ return this.connected;
248
+ }
249
+
250
+ hydrateConnection(
251
+ result: ConnectResult,
252
+ selectedAccountAddress?: string | null,
253
+ ): void {
254
+ const selectedAccount =
255
+ resolveWalletAccountByAddress(result.accounts, selectedAccountAddress) ??
256
+ result.selectedAccount ??
257
+ null;
258
+ const normalized = normalizeWalletAccountResult(result, selectedAccount);
259
+ this.connected = true;
260
+ this.accounts = normalized.accounts;
261
+ this.selectedAccount = normalized.selectedAccount;
262
+ }
263
+
264
+ clearConnection(): void {
265
+ this.connected = false;
266
+ this.accounts = [];
267
+ this.selectedAccount = null;
268
+ }
269
+
270
+ getAccounts(): WalletAccount[] {
271
+ return this.accounts;
272
+ }
273
+
274
+ getSelectedAccount(): WalletAccount | null {
275
+ return this.selectedAccount;
276
+ }
277
+
278
+ async selectAccount(publicKey: string): Promise<WalletAccount> {
279
+ if (!this.connected) throw new Error("Wallet not connected");
280
+ const payload: SelectAccountPayload = { publicKey };
281
+ const response = await this.bridge.sendMessage({
282
+ id: createRequestId(),
283
+ type: POST_MESSAGE_REQUEST_TYPES.SELECT_ACCOUNT,
284
+ payload,
285
+ origin: this.origin,
286
+ });
287
+ const account = response.result.account;
288
+ this.refreshAccountCache(account);
289
+ return account;
290
+ }
291
+
292
+ async manageAccounts(): Promise<ManageAccountsResult> {
293
+ if (!this.connected) throw new Error("Wallet not connected");
294
+ try {
295
+ this.requestShow();
296
+ const response = await this.bridge.sendMessage({
297
+ id: createRequestId(),
298
+ type: POST_MESSAGE_REQUEST_TYPES.MANAGE_ACCOUNTS,
299
+ origin: this.origin,
300
+ });
301
+
302
+ const result = normalizeWalletAccountResult(response.result);
303
+ this.accounts = result.accounts;
304
+ this.selectedAccount = result.selectedAccount;
305
+ this.requestHide();
306
+ return result;
307
+ } catch (error) {
308
+ this.requestHide();
309
+ this.emit(EMBEDDED_PROVIDER_EVENTS.ERROR, { error });
310
+ throw error;
311
+ }
312
+ }
313
+
314
+ get thru(): IThruChain {
315
+ if (!this._thruChain) {
316
+ throw new Error("Thru chain not enabled in provider config");
317
+ }
318
+ return this._thruChain;
319
+ }
320
+
321
+ on(event: NativeProviderEvent, cb: NativeProviderEventCallback): void {
322
+ if (!this.eventListeners.has(event)) {
323
+ this.eventListeners.set(event, new Set());
324
+ }
325
+ this.eventListeners.get(event)!.add(cb);
326
+ }
327
+
328
+ off(event: NativeProviderEvent, cb: NativeProviderEventCallback): void {
329
+ this.eventListeners.get(event)?.delete(cb);
330
+ }
331
+
332
+ /** Internal: used by NativeThruChain. */
333
+ getBridge(): WebViewBridge {
334
+ return this.bridge;
335
+ }
336
+
337
+ destroy(): void {
338
+ this.bridge.destroy();
339
+ this.eventListeners.clear();
340
+ this.clearConnection();
341
+ }
342
+
343
+ private emit(event: NativeProviderEvent, data?: unknown): void {
344
+ this.eventListeners.get(event)?.forEach((cb) => {
345
+ try {
346
+ cb(data);
347
+ } catch (err) {
348
+ // eslint-disable-next-line no-console
349
+ console.error(`[NativeProvider] listener error for ${event}:`, err);
350
+ }
351
+ });
352
+ }
353
+
354
+ private refreshAccountCache(account: WalletAccount | null): void {
355
+ if (!account) {
356
+ this.accounts = [];
357
+ this.selectedAccount = null;
358
+ return;
359
+ }
360
+ this.accounts = [account];
361
+ this.selectedAccount = account;
362
+ }
363
+ }