@phantom/react-native-sdk 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,434 @@
1
+ // src/PhantomProvider.tsx
2
+ import { createContext, useContext, useState, useEffect, useCallback, useMemo } from "react";
3
+ import { EmbeddedProvider } from "@phantom/embedded-provider-core";
4
+
5
+ // src/providers/embedded/storage.ts
6
+ import * as SecureStore from "expo-secure-store";
7
+ var ExpoSecureStorage = class {
8
+ constructor(requireAuth = false) {
9
+ this.sessionKey = "phantom_session";
10
+ this.requireAuth = requireAuth;
11
+ }
12
+ async saveSession(session) {
13
+ try {
14
+ await SecureStore.setItemAsync(this.sessionKey, JSON.stringify(session), {
15
+ requireAuthentication: this.requireAuth,
16
+ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
17
+ });
18
+ } catch (error) {
19
+ console.error("[ExpoSecureStorage] Failed to save session", { error: error.message });
20
+ throw new Error(`Failed to save session: ${error.message}`);
21
+ }
22
+ }
23
+ async getSession() {
24
+ try {
25
+ const sessionData = await SecureStore.getItemAsync(this.sessionKey, {
26
+ requireAuthentication: this.requireAuth
27
+ });
28
+ if (!sessionData) {
29
+ return null;
30
+ }
31
+ return JSON.parse(sessionData);
32
+ } catch (error) {
33
+ console.error("[ExpoSecureStorage] Failed to load session", { error: error.message });
34
+ return null;
35
+ }
36
+ }
37
+ async clearSession() {
38
+ try {
39
+ await SecureStore.deleteItemAsync(this.sessionKey);
40
+ } catch (error) {
41
+ console.error("[ExpoSecureStorage] Failed to clear session", { error: error.message });
42
+ }
43
+ }
44
+ async isAvailable() {
45
+ return await SecureStore.isAvailableAsync();
46
+ }
47
+ // Method to update authentication requirement
48
+ setRequireAuth(_requireAuth) {
49
+ console.warn("[ExpoSecureStorage] Cannot change requireAuth after initialization");
50
+ }
51
+ };
52
+
53
+ // src/providers/embedded/auth.ts
54
+ import * as WebBrowser from "expo-web-browser";
55
+ var ExpoAuthProvider = class {
56
+ async authenticate(options) {
57
+ if ("jwtToken" in options) {
58
+ return;
59
+ }
60
+ const { authUrl, redirectUrl } = options;
61
+ if (!authUrl || !redirectUrl) {
62
+ throw new Error("authUrl and redirectUrl are required for web browser authentication");
63
+ }
64
+ try {
65
+ console.log("[ExpoAuthProvider] Starting authentication", {
66
+ authUrl: authUrl.substring(0, 50) + "...",
67
+ redirectUrl
68
+ });
69
+ await WebBrowser.warmUpAsync();
70
+ const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUrl, {
71
+ // Use system browser on iOS for ASWebAuthenticationSession
72
+ preferEphemeralSession: false
73
+ });
74
+ console.log("[ExpoAuthProvider] Authentication result", {
75
+ type: result.type,
76
+ url: result.type === "success" && result.url ? result.url.substring(0, 100) + "..." : void 0
77
+ });
78
+ if (result.type === "success" && result.url) {
79
+ const url = new URL(result.url);
80
+ const walletId = url.searchParams.get("walletId");
81
+ const provider = url.searchParams.get("provider");
82
+ if (!walletId) {
83
+ throw new Error("Authentication failed: no walletId in redirect URL");
84
+ }
85
+ const userInfo = {};
86
+ url.searchParams.forEach((value, key) => {
87
+ userInfo[key] = value;
88
+ });
89
+ return {
90
+ walletId,
91
+ provider: provider || void 0,
92
+ userInfo
93
+ };
94
+ } else if (result.type === "cancel") {
95
+ throw new Error("User cancelled authentication");
96
+ } else {
97
+ throw new Error("Authentication failed");
98
+ }
99
+ } catch (error) {
100
+ console.error("[ExpoAuthProvider] Authentication error", error);
101
+ throw error;
102
+ } finally {
103
+ await WebBrowser.coolDownAsync();
104
+ }
105
+ }
106
+ isAvailable() {
107
+ return Promise.resolve(true);
108
+ }
109
+ };
110
+
111
+ // src/providers/embedded/url-params.ts
112
+ import { Linking } from "react-native";
113
+ var ExpoURLParamsAccessor = class {
114
+ constructor() {
115
+ this.listeners = /* @__PURE__ */ new Set();
116
+ this.subscription = null;
117
+ this.currentParams = {};
118
+ }
119
+ getParam(key) {
120
+ return this.currentParams[key] || null;
121
+ }
122
+ async getInitialParams() {
123
+ try {
124
+ const url = await Linking.getInitialURL();
125
+ if (!url) {
126
+ return null;
127
+ }
128
+ const params = this.parseURLParams(url);
129
+ this.currentParams = params;
130
+ return params;
131
+ } catch (error) {
132
+ console.error("[ExpoURLParamsAccessor] Failed to get initial URL", error);
133
+ return null;
134
+ }
135
+ }
136
+ startListening() {
137
+ if (this.subscription) {
138
+ return;
139
+ }
140
+ this.subscription = Linking.addEventListener("url", ({ url }) => {
141
+ const params = this.parseURLParams(url);
142
+ if (params && Object.keys(params).length > 0) {
143
+ this.currentParams = { ...this.currentParams, ...params };
144
+ this.listeners.forEach((listener) => listener(params));
145
+ }
146
+ });
147
+ }
148
+ stopListening() {
149
+ if (this.subscription) {
150
+ this.subscription.remove();
151
+ this.subscription = null;
152
+ }
153
+ }
154
+ addListener(callback) {
155
+ this.listeners.add(callback);
156
+ return () => {
157
+ this.listeners.delete(callback);
158
+ };
159
+ }
160
+ parseURLParams(url) {
161
+ try {
162
+ const parsed = new URL(url);
163
+ const params = {};
164
+ parsed.searchParams.forEach((value, key) => {
165
+ params[key] = value;
166
+ });
167
+ return params;
168
+ } catch (error) {
169
+ console.error("[ExpoURLParamsAccessor] Failed to parse URL", url, error);
170
+ return {};
171
+ }
172
+ }
173
+ dispose() {
174
+ this.stopListening();
175
+ this.listeners.clear();
176
+ }
177
+ };
178
+
179
+ // src/providers/embedded/logger.ts
180
+ var ExpoLogger = class {
181
+ constructor(enabled = false) {
182
+ this.enabled = enabled;
183
+ }
184
+ info(category, message, data) {
185
+ if (this.enabled) {
186
+ console.info(`[${category}] ${message}`, data);
187
+ }
188
+ }
189
+ warn(category, message, data) {
190
+ if (this.enabled) {
191
+ console.warn(`[${category}] ${message}`, data);
192
+ }
193
+ }
194
+ error(category, message, data) {
195
+ if (this.enabled) {
196
+ console.error(`[${category}] ${message}`, data);
197
+ }
198
+ }
199
+ log(category, message, data) {
200
+ if (this.enabled) {
201
+ console.log(`[${category}] ${message}`, data);
202
+ }
203
+ }
204
+ };
205
+
206
+ // src/PhantomProvider.tsx
207
+ import { jsx } from "react/jsx-runtime";
208
+ var PhantomContext = createContext(void 0);
209
+ function PhantomProvider({ children, config }) {
210
+ const sdk = useMemo(() => {
211
+ const redirectUrl = config.authOptions?.redirectUrl || `${config.scheme}://phantom-auth-callback`;
212
+ const embeddedConfig = {
213
+ apiBaseUrl: config.apiBaseUrl,
214
+ organizationId: config.organizationId,
215
+ authOptions: {
216
+ ...config.authOptions,
217
+ redirectUrl
218
+ },
219
+ embeddedWalletType: config.embeddedWalletType,
220
+ addressTypes: config.addressTypes,
221
+ solanaProvider: config.solanaProvider || "web3js"
222
+ };
223
+ const storage = new ExpoSecureStorage();
224
+ const authProvider = new ExpoAuthProvider();
225
+ const urlParamsAccessor = new ExpoURLParamsAccessor();
226
+ const logger = new ExpoLogger(config.debug);
227
+ const platform = {
228
+ storage,
229
+ authProvider,
230
+ urlParamsAccessor
231
+ };
232
+ return new EmbeddedProvider(embeddedConfig, platform, logger);
233
+ }, [config]);
234
+ const [isConnected, setIsConnected] = useState(false);
235
+ const [addresses, setAddresses] = useState([]);
236
+ const [walletId, setWalletId] = useState(null);
237
+ const [error, setError] = useState(null);
238
+ const updateConnectionState = useCallback(() => {
239
+ try {
240
+ const connected = sdk.isConnected();
241
+ setIsConnected(connected);
242
+ if (connected) {
243
+ const addrs = sdk.getAddresses();
244
+ setAddresses(addrs);
245
+ } else {
246
+ setAddresses([]);
247
+ setWalletId(null);
248
+ }
249
+ } catch (err) {
250
+ console.error("[PhantomProvider] Error updating connection state", err);
251
+ setError(err);
252
+ }
253
+ }, [sdk]);
254
+ useEffect(() => {
255
+ updateConnectionState();
256
+ }, [updateConnectionState]);
257
+ const value = {
258
+ sdk,
259
+ isConnected,
260
+ addresses,
261
+ walletId,
262
+ error,
263
+ updateConnectionState,
264
+ setWalletId
265
+ };
266
+ return /* @__PURE__ */ jsx(PhantomContext.Provider, { value, children });
267
+ }
268
+ function usePhantom() {
269
+ const context = useContext(PhantomContext);
270
+ if (!context) {
271
+ throw new Error("usePhantom must be used within a PhantomProvider");
272
+ }
273
+ return context;
274
+ }
275
+
276
+ // src/hooks/useConnect.ts
277
+ import { useState as useState2, useCallback as useCallback2 } from "react";
278
+ function useConnect() {
279
+ const { sdk, updateConnectionState, setWalletId } = usePhantom();
280
+ const [isConnecting, setIsConnecting] = useState2(false);
281
+ const [error, setError] = useState2(null);
282
+ const connect = useCallback2(
283
+ async (options) => {
284
+ if (!sdk) {
285
+ throw new Error("SDK not initialized");
286
+ }
287
+ setIsConnecting(true);
288
+ setError(null);
289
+ try {
290
+ const result = await sdk.connect(options);
291
+ if (result.status === "completed") {
292
+ if (result.walletId) {
293
+ setWalletId(result.walletId);
294
+ }
295
+ updateConnectionState();
296
+ }
297
+ return result;
298
+ } catch (err) {
299
+ const error2 = err;
300
+ setError(error2);
301
+ throw error2;
302
+ } finally {
303
+ setIsConnecting(false);
304
+ }
305
+ },
306
+ [sdk, updateConnectionState, setWalletId]
307
+ );
308
+ return {
309
+ connect,
310
+ isConnecting,
311
+ error
312
+ };
313
+ }
314
+
315
+ // src/hooks/useDisconnect.ts
316
+ import { useState as useState3, useCallback as useCallback3 } from "react";
317
+ function useDisconnect() {
318
+ const { sdk, updateConnectionState } = usePhantom();
319
+ const [isDisconnecting, setIsDisconnecting] = useState3(false);
320
+ const [error, setError] = useState3(null);
321
+ const disconnect = useCallback3(async () => {
322
+ if (!sdk) {
323
+ throw new Error("SDK not initialized");
324
+ }
325
+ setIsDisconnecting(true);
326
+ setError(null);
327
+ try {
328
+ await sdk.disconnect();
329
+ updateConnectionState();
330
+ } catch (err) {
331
+ const error2 = err;
332
+ setError(error2);
333
+ throw error2;
334
+ } finally {
335
+ setIsDisconnecting(false);
336
+ }
337
+ }, [sdk, updateConnectionState]);
338
+ return {
339
+ disconnect,
340
+ isDisconnecting,
341
+ error
342
+ };
343
+ }
344
+
345
+ // src/hooks/useAccounts.ts
346
+ function useAccounts() {
347
+ const { addresses, isConnected, walletId, error } = usePhantom();
348
+ return {
349
+ addresses,
350
+ isConnected,
351
+ walletId,
352
+ error
353
+ };
354
+ }
355
+
356
+ // src/hooks/useSignMessage.ts
357
+ import { useState as useState4, useCallback as useCallback4 } from "react";
358
+ function useSignMessage() {
359
+ const { sdk } = usePhantom();
360
+ const [isSigning, setIsSigning] = useState4(false);
361
+ const [error, setError] = useState4(null);
362
+ const signMessage = useCallback4(
363
+ async (params) => {
364
+ if (!sdk) {
365
+ throw new Error("SDK not initialized");
366
+ }
367
+ setIsSigning(true);
368
+ setError(null);
369
+ try {
370
+ const signature = await sdk.signMessage(params);
371
+ return signature;
372
+ } catch (err) {
373
+ const error2 = err;
374
+ setError(error2);
375
+ throw error2;
376
+ } finally {
377
+ setIsSigning(false);
378
+ }
379
+ },
380
+ [sdk]
381
+ );
382
+ return {
383
+ signMessage,
384
+ isSigning,
385
+ error
386
+ };
387
+ }
388
+
389
+ // src/hooks/useSignAndSendTransaction.ts
390
+ import { useState as useState5, useCallback as useCallback5 } from "react";
391
+ function useSignAndSendTransaction() {
392
+ const { sdk } = usePhantom();
393
+ const [isSigning, setIsSigning] = useState5(false);
394
+ const [error, setError] = useState5(null);
395
+ const signAndSendTransaction = useCallback5(
396
+ async (params) => {
397
+ if (!sdk) {
398
+ throw new Error("SDK not initialized");
399
+ }
400
+ setIsSigning(true);
401
+ setError(null);
402
+ try {
403
+ const result = await sdk.signAndSendTransaction(params);
404
+ return result;
405
+ } catch (err) {
406
+ const error2 = err;
407
+ setError(error2);
408
+ throw error2;
409
+ } finally {
410
+ setIsSigning(false);
411
+ }
412
+ },
413
+ [sdk]
414
+ );
415
+ return {
416
+ signAndSendTransaction,
417
+ isSigning,
418
+ error
419
+ };
420
+ }
421
+
422
+ // src/index.ts
423
+ import { AddressType, NetworkId } from "@phantom/client";
424
+ export {
425
+ AddressType,
426
+ NetworkId,
427
+ PhantomProvider,
428
+ useAccounts,
429
+ useConnect,
430
+ useDisconnect,
431
+ usePhantom,
432
+ useSignAndSendTransaction,
433
+ useSignMessage
434
+ };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@phantom/react-native-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Phantom Wallet SDK for React Native and Expo applications",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "license": "MIT",
19
+ "scripts": {
20
+ "?pack-release": "When https://github.com/changesets/changesets/issues/432 has a solution we can remove this trick",
21
+ "pack-release": "rimraf ./_release && yarn pack && mkdir ./_release && tar zxvf ./package.tgz --directory ./_release && rm ./package.tgz",
22
+ "build": "rimraf ./dist && tsup",
23
+ "dev": "rimraf ./dist && tsup --watch",
24
+ "clean": "rm -rf dist",
25
+ "test": "jest",
26
+ "test:watch": "jest --watch",
27
+ "lint": "tsc --noEmit && eslint --cache . --ext .ts,.tsx",
28
+ "check-types": "tsc --noEmit",
29
+ "prettier": "prettier --write \"src/**/*.{ts,tsx}\""
30
+ },
31
+ "keywords": [
32
+ "phantom",
33
+ "wallet",
34
+ "react-native",
35
+ "expo",
36
+ "solana",
37
+ "ethereum",
38
+ "crypto",
39
+ "web3"
40
+ ],
41
+ "author": "Phantom",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/phantom/wallet-sdk.git",
45
+ "directory": "packages/react-native-sdk"
46
+ },
47
+ "dependencies": {
48
+ "@phantom/client": "^0.1.3",
49
+ "@phantom/embedded-provider-core": "^0.1.1"
50
+ },
51
+ "peerDependencies": {
52
+ "expo": ">=53.0.0",
53
+ "expo-auth-session": ">=5.0.0",
54
+ "expo-secure-store": ">=12.0.0",
55
+ "expo-web-browser": ">=12.0.0",
56
+ "react": ">=19.0.0",
57
+ "react-native": ">=0.79.0",
58
+ "react-native-get-random-values": ">=1.8.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/jest": "^29.5.14",
62
+ "@types/react": "~19.0.10",
63
+ "@types/react-native": "^0.72.0",
64
+ "eslint": "8.53.0",
65
+ "eslint-define-config": "^1.24.1",
66
+ "eslint-plugin-react": "^7.33.0",
67
+ "eslint-plugin-react-native": "^4.1.0",
68
+ "expo": "^53.0.20",
69
+ "expo-auth-session": "^6.2.1",
70
+ "expo-secure-store": "^14.2.3",
71
+ "expo-web-browser": "^14.2.0",
72
+ "jest": "^29.7.0",
73
+ "jest-environment-jsdom": "^29.7.0",
74
+ "prettier": "^3.5.2",
75
+ "react": "19.1.1",
76
+ "react-native": "0.79.5",
77
+ "rimraf": "^6.0.1",
78
+ "ts-jest": "^29",
79
+ "tsup": "^6.7.0",
80
+ "typescript": "^5.8.3"
81
+ },
82
+ "publishConfig": {
83
+ "directory": "_release/package"
84
+ }
85
+ }