@phantom/react-native-sdk 1.0.0-beta.8 → 1.0.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 CHANGED
@@ -1,13 +1,420 @@
1
1
  // src/PhantomProvider.tsx
2
- import { createContext, useContext, useState, useEffect, useMemo } from "react";
2
+ import { useState as useState5, useEffect as useEffect2, useMemo as useMemo2, useCallback as useCallback5 } from "react";
3
3
  import { EmbeddedProvider } from "@phantom/embedded-provider-core";
4
- import { ANALYTICS_HEADERS, DEFAULT_WALLET_API_URL, DEFAULT_EMBEDDED_WALLET_TYPE, DEFAULT_AUTH_URL as DEFAULT_AUTH_URL2 } from "@phantom/constants";
4
+ import {
5
+ ANALYTICS_HEADERS,
6
+ DEFAULT_WALLET_API_URL,
7
+ DEFAULT_EMBEDDED_WALLET_TYPE,
8
+ DEFAULT_AUTH_URL as DEFAULT_AUTH_URL2
9
+ } from "@phantom/constants";
10
+ import { ThemeProvider, darkTheme } from "@phantom/wallet-sdk-ui";
11
+
12
+ // src/ModalProvider.tsx
13
+ import { useState as useState4, useCallback as useCallback4, useMemo } from "react";
14
+
15
+ // src/ModalContext.ts
16
+ import { createContext, useContext } from "react";
17
+ var ModalContext = createContext(void 0);
18
+ function useModal() {
19
+ const context = useContext(ModalContext);
20
+ if (!context) {
21
+ throw new Error("useModal must be used within a ModalProvider");
22
+ }
23
+ return {
24
+ open: context.openModal,
25
+ close: context.closeModal,
26
+ isOpened: context.isModalOpen
27
+ };
28
+ }
29
+
30
+ // src/components/Modal.tsx
31
+ import { Modal as RNModal, View, StyleSheet, SafeAreaView } from "react-native";
32
+ import { useTheme } from "@phantom/wallet-sdk-ui";
33
+ import { jsx, jsxs } from "react/jsx-runtime";
34
+ function Modal({ isVisible, onClose, children }) {
35
+ const theme = useTheme();
36
+ const styles = StyleSheet.create({
37
+ bottomSheet: {
38
+ backgroundColor: theme.background,
39
+ borderTopLeftRadius: 32,
40
+ borderTopRightRadius: 32,
41
+ bottom: 0,
42
+ left: 0,
43
+ paddingBottom: 20,
44
+ position: "absolute",
45
+ right: 0
46
+ },
47
+ handle: {
48
+ alignSelf: "center",
49
+ backgroundColor: theme.secondary,
50
+ borderRadius: 2.5,
51
+ height: 5,
52
+ marginTop: 12,
53
+ opacity: 0.3,
54
+ width: 40
55
+ }
56
+ });
57
+ return /* @__PURE__ */ jsx(RNModal, { visible: isVisible, transparent: true, animationType: "slide", onRequestClose: onClose, children: /* @__PURE__ */ jsxs(SafeAreaView, { style: styles.bottomSheet, children: [
58
+ /* @__PURE__ */ jsx(View, { style: styles.handle }),
59
+ children
60
+ ] }) });
61
+ }
62
+
63
+ // src/PhantomContext.tsx
64
+ import { createContext as createContext2, useContext as useContext2 } from "react";
65
+ var PhantomContext = createContext2(void 0);
66
+ function usePhantom() {
67
+ const context = useContext2(PhantomContext);
68
+ if (context === void 0) {
69
+ throw new Error("usePhantom must be used within a PhantomProvider");
70
+ }
71
+ return context;
72
+ }
73
+
74
+ // src/components/ConnectModalContent.tsx
75
+ import { useState, useCallback as useCallback2 } from "react";
76
+ import { View as View2, Image, StyleSheet as StyleSheet2, ActivityIndicator } from "react-native";
77
+ import { Button, Icon, Text, useTheme as useTheme2, hexToRgba, ModalHeader } from "@phantom/wallet-sdk-ui";
78
+
79
+ // src/hooks/useConnect.ts
80
+ import { useCallback } from "react";
81
+ function useConnect() {
82
+ const { sdk, isConnecting, errors, setWalletId } = usePhantom();
83
+ const connect = useCallback(
84
+ async (options) => {
85
+ if (!sdk) {
86
+ throw new Error("SDK not initialized");
87
+ }
88
+ try {
89
+ const result = await sdk.connect(options);
90
+ if (result.status === "completed" && result.walletId) {
91
+ setWalletId(result.walletId);
92
+ }
93
+ return result;
94
+ } catch (err) {
95
+ const error = err;
96
+ throw error;
97
+ }
98
+ },
99
+ [sdk, setWalletId]
100
+ );
101
+ return {
102
+ connect,
103
+ isConnecting,
104
+ error: errors.connect
105
+ };
106
+ }
107
+
108
+ // src/components/ConnectModalContent.tsx
109
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
110
+ function ConnectModalContent({ appIcon, onClose }) {
111
+ const theme = useTheme2();
112
+ const { isConnecting: contextIsConnecting, allowedProviders } = usePhantom();
113
+ const { connect } = useConnect();
114
+ const [isConnecting, setIsConnecting] = useState(false);
115
+ const [error, setError] = useState(null);
116
+ const [providerType, setProviderType] = useState(null);
117
+ const isLoading = contextIsConnecting || isConnecting;
118
+ const errorBackgroundColor = hexToRgba(theme.error, 0.1);
119
+ const errorBorderColor = hexToRgba(theme.error, 0.3);
120
+ const errorTextColor = theme.error;
121
+ const connectWithAuthProvider = useCallback2(
122
+ async (provider) => {
123
+ try {
124
+ setIsConnecting(true);
125
+ setError(null);
126
+ setProviderType(provider);
127
+ await connect({ provider });
128
+ onClose();
129
+ } catch (err) {
130
+ const error2 = err instanceof Error ? err : new Error(String(err));
131
+ setError(error2);
132
+ } finally {
133
+ setIsConnecting(false);
134
+ setProviderType(null);
135
+ }
136
+ },
137
+ [connect, onClose]
138
+ );
139
+ const styles = StyleSheet2.create({
140
+ appIcon: {
141
+ borderRadius: 28,
142
+ height: 56,
143
+ marginBottom: 12,
144
+ width: 56
145
+ },
146
+ buttonContainer: {
147
+ alignItems: "center",
148
+ flexDirection: "column",
149
+ gap: 12,
150
+ paddingHorizontal: 32,
151
+ width: "100%"
152
+ },
153
+ buttonContent: {
154
+ alignItems: "center",
155
+ flexDirection: "row",
156
+ justifyContent: "space-between",
157
+ width: "100%"
158
+ },
159
+ buttonContentLeft: {
160
+ alignItems: "center",
161
+ flexDirection: "row",
162
+ gap: 8
163
+ },
164
+ container: {
165
+ alignItems: "center",
166
+ flexDirection: "column",
167
+ gap: 12,
168
+ paddingBottom: 24,
169
+ width: "100%"
170
+ },
171
+ errorContainer: {
172
+ backgroundColor: errorBackgroundColor,
173
+ borderColor: errorBorderColor,
174
+ borderRadius: parseInt(theme.borderRadius),
175
+ borderWidth: 1,
176
+ padding: 12,
177
+ width: "100%"
178
+ },
179
+ errorText: {
180
+ color: errorTextColor,
181
+ fontSize: 14
182
+ },
183
+ footer: {
184
+ alignItems: "center",
185
+ borderColor: theme.aux,
186
+ borderTopWidth: 1,
187
+ flexDirection: "row",
188
+ gap: 4,
189
+ justifyContent: "center",
190
+ marginTop: 24,
191
+ padding: 16,
192
+ width: "100%"
193
+ },
194
+ loadingContainer: {
195
+ alignItems: "center",
196
+ flexDirection: "column",
197
+ gap: 12,
198
+ justifyContent: "center",
199
+ padding: 24
200
+ }
201
+ });
202
+ return /* @__PURE__ */ jsxs2(View2, { style: styles.container, children: [
203
+ /* @__PURE__ */ jsx2(ModalHeader, { title: "Login or Sign Up", onClose }),
204
+ appIcon && /* @__PURE__ */ jsx2(Image, { testID: "app-icon", source: { uri: appIcon }, style: styles.appIcon }),
205
+ isLoading ? /* @__PURE__ */ jsxs2(View2, { style: styles.loadingContainer, children: [
206
+ /* @__PURE__ */ jsx2(ActivityIndicator, { testID: "activity-indicator", size: "large", color: theme.brand }),
207
+ /* @__PURE__ */ jsx2(Text, { variant: "label", color: theme.secondary, children: "Loading..." })
208
+ ] }) : /* @__PURE__ */ jsxs2(View2, { style: styles.buttonContainer, children: [
209
+ error && /* @__PURE__ */ jsx2(View2, { style: styles.errorContainer, children: /* @__PURE__ */ jsx2(Text, { style: styles.errorText, children: error.message }) }),
210
+ allowedProviders.includes("google") && /* @__PURE__ */ jsx2(
211
+ Button,
212
+ {
213
+ onClick: () => connectWithAuthProvider("google"),
214
+ disabled: isConnecting,
215
+ isLoading: isConnecting && providerType === "google",
216
+ fullWidth: true,
217
+ children: /* @__PURE__ */ jsxs2(View2, { style: styles.buttonContent, children: [
218
+ /* @__PURE__ */ jsxs2(View2, { style: styles.buttonContentLeft, children: [
219
+ /* @__PURE__ */ jsx2(Icon, { type: "google", size: 20, color: theme.text }),
220
+ /* @__PURE__ */ jsx2(Text, { variant: "captionBold", children: "Continue with Google" })
221
+ ] }),
222
+ /* @__PURE__ */ jsx2(Icon, { type: "chevron-right", size: 16, color: theme.secondary })
223
+ ] })
224
+ }
225
+ ),
226
+ allowedProviders.includes("apple") && /* @__PURE__ */ jsx2(
227
+ Button,
228
+ {
229
+ onClick: () => connectWithAuthProvider("apple"),
230
+ disabled: isConnecting,
231
+ isLoading: isConnecting && providerType === "apple",
232
+ fullWidth: true,
233
+ children: /* @__PURE__ */ jsxs2(View2, { style: styles.buttonContent, children: [
234
+ /* @__PURE__ */ jsxs2(View2, { style: styles.buttonContentLeft, children: [
235
+ /* @__PURE__ */ jsx2(Icon, { type: "apple", size: 20, color: theme.text }),
236
+ /* @__PURE__ */ jsx2(Text, { variant: "captionBold", children: "Continue with Apple" })
237
+ ] }),
238
+ /* @__PURE__ */ jsx2(Icon, { type: "chevron-right", size: 16, color: theme.secondary })
239
+ ] })
240
+ }
241
+ )
242
+ ] }),
243
+ /* @__PURE__ */ jsxs2(View2, { style: styles.footer, children: [
244
+ /* @__PURE__ */ jsx2(Text, { variant: "label", color: theme.secondary, children: "Powered by" }),
245
+ /* @__PURE__ */ jsx2(Icon, { type: "phantom", size: 16, color: theme.secondary }),
246
+ /* @__PURE__ */ jsx2(Text, { variant: "label", color: theme.secondary, children: "Phantom" })
247
+ ] })
248
+ ] });
249
+ }
250
+
251
+ // src/components/ConnectedModalContent.tsx
252
+ import { useState as useState3, useEffect } from "react";
253
+ import { View as View3, StyleSheet as StyleSheet3 } from "react-native";
254
+ import { Button as Button2, Text as Text2, useTheme as useTheme3, hexToRgba as hexToRgba2, ModalHeader as ModalHeader2 } from "@phantom/wallet-sdk-ui";
255
+
256
+ // src/hooks/useDisconnect.ts
257
+ import { useState as useState2, useCallback as useCallback3 } from "react";
258
+ function useDisconnect() {
259
+ const { sdk } = usePhantom();
260
+ const [isDisconnecting, setIsDisconnecting] = useState2(false);
261
+ const [error, setError] = useState2(null);
262
+ const disconnect = useCallback3(async () => {
263
+ if (!sdk) {
264
+ throw new Error("SDK not initialized");
265
+ }
266
+ setIsDisconnecting(true);
267
+ setError(null);
268
+ try {
269
+ await sdk.disconnect();
270
+ } catch (err) {
271
+ const error2 = err;
272
+ setError(error2);
273
+ throw error2;
274
+ } finally {
275
+ setIsDisconnecting(false);
276
+ }
277
+ }, [sdk]);
278
+ return {
279
+ disconnect,
280
+ isDisconnecting,
281
+ error
282
+ };
283
+ }
284
+
285
+ // src/components/ConnectedModalContent.tsx
286
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
287
+ function ConnectedModalContent({ onClose }) {
288
+ const theme = useTheme3();
289
+ const { addresses } = usePhantom();
290
+ const { disconnect } = useDisconnect();
291
+ const [isDisconnecting, setIsDisconnecting] = useState3(false);
292
+ const [disconnectError, setDisconnectError] = useState3(null);
293
+ const errorBackgroundColor = hexToRgba2(theme.error, 0.1);
294
+ const errorBorderColor = hexToRgba2(theme.error, 0.3);
295
+ useEffect(() => {
296
+ setDisconnectError(null);
297
+ }, []);
298
+ const handleDisconnect = async () => {
299
+ try {
300
+ setIsDisconnecting(true);
301
+ setDisconnectError(null);
302
+ await disconnect();
303
+ onClose();
304
+ } catch (err) {
305
+ const error = err instanceof Error ? err : new Error(String(err));
306
+ setDisconnectError(error);
307
+ } finally {
308
+ setIsDisconnecting(false);
309
+ }
310
+ };
311
+ const styles = StyleSheet3.create({
312
+ accountItem: {
313
+ flexDirection: "column",
314
+ gap: 8,
315
+ width: "100%"
316
+ },
317
+ accountList: {
318
+ flexDirection: "column",
319
+ gap: 16,
320
+ width: "100%"
321
+ },
322
+ accountTypeText: {
323
+ textTransform: "uppercase"
324
+ },
325
+ addressText: {
326
+ fontFamily: "monospace"
327
+ },
328
+ container: {
329
+ alignItems: "center",
330
+ flexDirection: "column",
331
+ gap: 24,
332
+ paddingBottom: 24,
333
+ paddingHorizontal: 32,
334
+ width: "100%"
335
+ },
336
+ errorContainer: {
337
+ backgroundColor: errorBackgroundColor,
338
+ borderColor: errorBorderColor,
339
+ borderRadius: theme.borderRadius,
340
+ borderWidth: 1,
341
+ padding: 12,
342
+ width: "100%"
343
+ }
344
+ });
345
+ return /* @__PURE__ */ jsxs3(View3, { style: styles.container, children: [
346
+ /* @__PURE__ */ jsx3(ModalHeader2, { title: "Wallet", onClose }),
347
+ addresses && addresses.length > 0 && /* @__PURE__ */ jsx3(View3, { style: styles.accountList, children: addresses.map((account, index) => /* @__PURE__ */ jsxs3(View3, { style: styles.accountItem, children: [
348
+ /* @__PURE__ */ jsx3(Text2, { variant: "label", color: theme.secondary, style: styles.accountTypeText, children: account.addressType }),
349
+ /* @__PURE__ */ jsx3(Text2, { variant: "caption", style: styles.addressText, children: account.address })
350
+ ] }, index)) }),
351
+ disconnectError && /* @__PURE__ */ jsx3(View3, { style: styles.errorContainer, children: /* @__PURE__ */ jsx3(Text2, { variant: "caption", color: theme.error, children: "Failed to disconnect" }) }),
352
+ /* @__PURE__ */ jsx3(Button2, { onClick: handleDisconnect, disabled: isDisconnecting, isLoading: isDisconnecting, fullWidth: true, children: /* @__PURE__ */ jsx3(Text2, { variant: "captionBold", children: isDisconnecting ? "Disconnecting..." : "Disconnect" }) })
353
+ ] });
354
+ }
355
+
356
+ // src/components/SpendingLimitModalContent.tsx
357
+ import { View as View4, StyleSheet as StyleSheet4 } from "react-native";
358
+ import { Button as Button3, Text as Text3, useTheme as useTheme4 } from "@phantom/wallet-sdk-ui";
359
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
360
+ function SpendingLimitModalContent({ onClose }) {
361
+ const theme = useTheme4();
362
+ const styles = StyleSheet4.create({
363
+ container: {
364
+ flexDirection: "column",
365
+ gap: 16,
366
+ padding: 32
367
+ }
368
+ });
369
+ return /* @__PURE__ */ jsxs4(View4, { style: styles.container, children: [
370
+ /* @__PURE__ */ jsx4(Text3, { variant: "caption", color: theme.secondary, children: "You've reached the maximum daily limit allowed to spend by this application." }),
371
+ /* @__PURE__ */ jsx4(Button3, { fullWidth: true, onClick: onClose, children: /* @__PURE__ */ jsx4(Text3, { variant: "captionBold", children: "Close" }) })
372
+ ] });
373
+ }
374
+
375
+ // src/ModalProvider.tsx
376
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
377
+ function ModalProvider({ children, appIcon, appName }) {
378
+ const { isConnected, errors, clearError } = usePhantom();
379
+ const [isModalOpen, setIsModalOpen] = useState4(false);
380
+ const openModal = useCallback4(() => {
381
+ setIsModalOpen(true);
382
+ }, []);
383
+ const closeModal = useCallback4(() => {
384
+ setIsModalOpen(false);
385
+ clearError("spendingLimit");
386
+ }, [clearError]);
387
+ const isSpendingLimitOpen = !!errors.spendingLimit;
388
+ const modalContextValue = useMemo(
389
+ () => ({
390
+ isModalOpen,
391
+ openModal,
392
+ closeModal
393
+ }),
394
+ [isModalOpen, openModal, closeModal]
395
+ );
396
+ return /* @__PURE__ */ jsxs5(ModalContext.Provider, { value: modalContextValue, children: [
397
+ children,
398
+ /* @__PURE__ */ jsx5(
399
+ Modal,
400
+ {
401
+ isVisible: isModalOpen || isSpendingLimitOpen,
402
+ onClose: closeModal,
403
+ appIcon,
404
+ appName,
405
+ isMobile: true,
406
+ children: isSpendingLimitOpen ? /* @__PURE__ */ jsx5(SpendingLimitModalContent, { onClose: closeModal }) : isConnected ? /* @__PURE__ */ jsx5(ConnectedModalContent, { onClose: closeModal }) : /* @__PURE__ */ jsx5(ConnectModalContent, { appIcon, appName, onClose: closeModal })
407
+ }
408
+ )
409
+ ] });
410
+ }
5
411
 
6
412
  // src/providers/embedded/storage.ts
7
413
  import * as SecureStore from "expo-secure-store";
8
414
  var ExpoSecureStorage = class {
9
415
  constructor(requireAuth = false) {
10
416
  this.sessionKey = "phantom_session";
417
+ this.logoutFlagKey = "phantom_should_clear_previous_session";
11
418
  this.requireAuth = requireAuth;
12
419
  }
13
420
  async saveSession(session) {
@@ -42,6 +449,36 @@ var ExpoSecureStorage = class {
42
449
  console.error("[ExpoSecureStorage] Failed to clear session", { error: error.message });
43
450
  }
44
451
  }
452
+ async getShouldClearPreviousSession() {
453
+ try {
454
+ const flagData = await SecureStore.getItemAsync(this.logoutFlagKey, {
455
+ requireAuthentication: false
456
+ // Don't require auth for this flag
457
+ });
458
+ if (!flagData) {
459
+ return false;
460
+ }
461
+ return flagData === "true";
462
+ } catch (error) {
463
+ console.error("[ExpoSecureStorage] Failed to get shouldClearPreviousSession flag", {
464
+ error: error.message
465
+ });
466
+ return false;
467
+ }
468
+ }
469
+ async setShouldClearPreviousSession(should) {
470
+ try {
471
+ await SecureStore.setItemAsync(this.logoutFlagKey, should.toString(), {
472
+ requireAuthentication: false,
473
+ // Don't require auth for this flag
474
+ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
475
+ });
476
+ } catch (error) {
477
+ console.error("[ExpoSecureStorage] Failed to set shouldClearPreviousSession flag", {
478
+ error: error.message
479
+ });
480
+ }
481
+ }
45
482
  async isAvailable() {
46
483
  return await SecureStore.isAvailableAsync();
47
484
  }
@@ -53,6 +490,7 @@ var ExpoSecureStorage = class {
53
490
 
54
491
  // src/providers/embedded/auth.ts
55
492
  import * as WebBrowser from "expo-web-browser";
493
+ import { Platform } from "react-native";
56
494
  import { DEFAULT_AUTH_URL } from "@phantom/constants";
57
495
  var ExpoAuthProvider = class {
58
496
  async authenticate(options) {
@@ -60,22 +498,26 @@ var ExpoAuthProvider = class {
60
498
  return;
61
499
  }
62
500
  const phantomOptions = options;
63
- const { authUrl, redirectUrl, organizationId, sessionId, provider, customAuthData, appId } = phantomOptions;
501
+ const { authUrl, redirectUrl, publicKey, sessionId, provider, appId } = phantomOptions;
64
502
  if (!redirectUrl) {
65
503
  throw new Error("redirectUrl is required for web browser authentication");
66
504
  }
67
- if (!organizationId || !sessionId || !appId) {
68
- throw new Error("organizationId, sessionId and appId are required for authentication");
505
+ if (!publicKey || !sessionId || !appId) {
506
+ throw new Error("publicKey, sessionId and appId are required for authentication");
69
507
  }
70
508
  try {
71
509
  const baseUrl = authUrl || DEFAULT_AUTH_URL;
72
510
  const params = new URLSearchParams({
73
- organization_id: organizationId,
511
+ public_key: publicKey,
74
512
  app_id: appId,
75
513
  redirect_uri: redirectUrl,
76
514
  session_id: sessionId,
77
- clear_previous_session: "true",
78
- sdk_version: "1.0.0-beta.8"
515
+ // OAuth session management - defaults to allow refresh unless explicitly clearing after logout
516
+ clear_previous_session: (phantomOptions.clearPreviousSession ?? false).toString(),
517
+ allow_refresh: (phantomOptions.allowRefresh ?? true).toString(),
518
+ sdk_version: "1.0.0",
519
+ sdk_type: "react-native",
520
+ platform: Platform.OS
79
521
  });
80
522
  if (provider) {
81
523
  console.log("[ExpoAuthProvider] Provider specified, will skip selection", { provider });
@@ -84,18 +526,13 @@ var ExpoAuthProvider = class {
84
526
  console.log("[ExpoAuthProvider] No provider specified, defaulting to Google");
85
527
  params.append("provider", "google");
86
528
  }
87
- if (customAuthData) {
88
- console.log("[ExpoAuthProvider] Adding custom auth data");
89
- params.append("authData", JSON.stringify(customAuthData));
90
- }
91
529
  const fullAuthUrl = `${baseUrl}?${params.toString()}`;
92
530
  console.log("[ExpoAuthProvider] Starting authentication", {
93
531
  baseUrl,
94
532
  redirectUrl,
95
- organizationId,
533
+ publicKey,
96
534
  sessionId,
97
- provider,
98
- hasCustomData: !!customAuthData
535
+ provider
99
536
  });
100
537
  await WebBrowser.warmUpAsync();
101
538
  const result = await WebBrowser.openAuthSessionAsync(fullAuthUrl, redirectUrl, {
@@ -109,15 +546,32 @@ var ExpoAuthProvider = class {
109
546
  if (result.type === "success" && result.url) {
110
547
  const url = new URL(result.url);
111
548
  const walletId = url.searchParams.get("wallet_id");
112
- const provider2 = url.searchParams.get("provider");
549
+ const organizationId = url.searchParams.get("organization_id");
113
550
  const accountDerivationIndex = url.searchParams.get("selected_account_index");
551
+ const expiresInMs = url.searchParams.get("expires_in_ms");
552
+ const authUserId = url.searchParams.get("auth_user_id");
114
553
  if (!walletId) {
115
554
  throw new Error("Authentication failed: no walletId in redirect URL");
116
555
  }
556
+ if (!organizationId) {
557
+ console.error("[ExpoAuthProvider] Missing organizationId in redirect URL", { url: result.url });
558
+ throw new Error("Authentication failed: no organizationId in redirect URL");
559
+ }
560
+ console.log("[ExpoAuthProvider] Auth redirect parameters", {
561
+ walletId,
562
+ organizationId,
563
+ provider,
564
+ accountDerivationIndex,
565
+ expiresInMs,
566
+ authUserId
567
+ });
117
568
  return {
118
569
  walletId,
119
- provider: provider2 || void 0,
120
- accountDerivationIndex: accountDerivationIndex ? parseInt(accountDerivationIndex) : void 0
570
+ organizationId,
571
+ provider: provider || void 0,
572
+ accountDerivationIndex: accountDerivationIndex ? parseInt(accountDerivationIndex) : 0,
573
+ expiresInMs: expiresInMs ? parseInt(expiresInMs) : 0,
574
+ authUserId: authUserId || void 0
121
575
  };
122
576
  } else if (result.type === "cancel") {
123
577
  throw new Error("User cancelled authentication");
@@ -405,17 +859,31 @@ var ExpoLogger = class {
405
859
  }
406
860
  };
407
861
 
862
+ // src/providers/embedded/phantom-app.ts
863
+ var ReactNativePhantomAppProvider = class {
864
+ isAvailable() {
865
+ return false;
866
+ }
867
+ authenticate(_options) {
868
+ return Promise.reject(
869
+ new Error(
870
+ "Phantom app authentication is not available in React Native. Please use other authentication methods like Google, Apple, or JWT."
871
+ )
872
+ );
873
+ }
874
+ };
875
+
408
876
  // src/PhantomProvider.tsx
409
- import { Platform } from "react-native";
410
- import { jsx } from "react/jsx-runtime";
411
- var PhantomContext = createContext(void 0);
412
- function PhantomProvider({ children, config, debugConfig }) {
413
- const [isConnected, setIsConnected] = useState(false);
414
- const [isConnecting, setIsConnecting] = useState(false);
415
- const [connectError, setConnectError] = useState(null);
416
- const [addresses, setAddresses] = useState([]);
417
- const [walletId, setWalletId] = useState(null);
418
- const memoizedConfig = useMemo(() => {
877
+ import { Platform as Platform2 } from "react-native";
878
+ import { jsx as jsx6 } from "react/jsx-runtime";
879
+ function PhantomProvider({ children, config, debugConfig, theme, appIcon, appName }) {
880
+ const [isConnected, setIsConnected] = useState5(false);
881
+ const [isConnecting, setIsConnecting] = useState5(false);
882
+ const [errors, setErrors] = useState5({});
883
+ const [addresses, setAddresses] = useState5([]);
884
+ const [walletId, setWalletId] = useState5(null);
885
+ const [user, setUser] = useState5(null);
886
+ const memoizedConfig = useMemo2(() => {
419
887
  const redirectUrl = config.authOptions?.redirectUrl || `${config.scheme}://phantom-auth-callback`;
420
888
  return {
421
889
  ...config,
@@ -428,7 +896,7 @@ function PhantomProvider({ children, config, debugConfig }) {
428
896
  }
429
897
  };
430
898
  }, [config]);
431
- const sdk = useMemo(() => {
899
+ const sdk = useMemo2(() => {
432
900
  const storage = new ExpoSecureStorage();
433
901
  const authProvider = new ExpoAuthProvider();
434
902
  const urlParamsAccessor = new ExpoURLParamsAccessor();
@@ -437,34 +905,36 @@ function PhantomProvider({ children, config, debugConfig }) {
437
905
  keyPrefix: `phantom-rn-${memoizedConfig.appId}`,
438
906
  appId: memoizedConfig.appId
439
907
  });
440
- const platformName = `${Platform.OS}-${Platform.Version}`;
908
+ const platformName = `${Platform2.OS}-${Platform2.Version}`;
441
909
  const platform = {
442
910
  storage,
443
911
  authProvider,
444
912
  urlParamsAccessor,
445
913
  stamper,
914
+ phantomAppProvider: new ReactNativePhantomAppProvider(),
446
915
  name: platformName,
447
916
  analyticsHeaders: {
448
917
  [ANALYTICS_HEADERS.SDK_TYPE]: "react-native",
449
- [ANALYTICS_HEADERS.PLATFORM]: Platform.OS,
450
- [ANALYTICS_HEADERS.PLATFORM_VERSION]: `${Platform.Version}`,
918
+ [ANALYTICS_HEADERS.PLATFORM]: Platform2.OS,
919
+ [ANALYTICS_HEADERS.PLATFORM_VERSION]: `${Platform2.Version}`,
451
920
  [ANALYTICS_HEADERS.APP_ID]: config.appId,
452
921
  [ANALYTICS_HEADERS.WALLET_TYPE]: config.embeddedWalletType,
453
- [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.0-beta.8"
922
+ [ANALYTICS_HEADERS.SDK_VERSION]: "1.0.0"
454
923
  // Replaced at build time
455
924
  }
456
925
  };
457
926
  return new EmbeddedProvider(memoizedConfig, platform, logger);
458
927
  }, [memoizedConfig, debugConfig, config.appId, config.embeddedWalletType]);
459
- useEffect(() => {
928
+ useEffect2(() => {
460
929
  const handleConnectStart = () => {
461
930
  setIsConnecting(true);
462
- setConnectError(null);
931
+ setErrors((prev) => ({ ...prev, connect: void 0 }));
463
932
  };
464
- const handleConnect = async () => {
933
+ const handleConnect = async (data) => {
465
934
  try {
466
935
  setIsConnected(true);
467
936
  setIsConnecting(false);
937
+ setUser(data);
468
938
  const addrs = await sdk.getAddresses();
469
939
  setAddresses(addrs);
470
940
  } catch (err) {
@@ -478,110 +948,57 @@ function PhantomProvider({ children, config, debugConfig }) {
478
948
  };
479
949
  const handleConnectError = (errorData) => {
480
950
  setIsConnecting(false);
481
- setConnectError(new Error(errorData.error || "Connection failed"));
951
+ setErrors((prev) => ({ ...prev, connect: new Error(errorData.error || "Connection failed") }));
952
+ setAddresses([]);
482
953
  };
483
954
  const handleDisconnect = () => {
484
955
  setIsConnected(false);
485
956
  setIsConnecting(false);
486
- setConnectError(null);
957
+ setErrors({});
487
958
  setAddresses([]);
488
959
  setWalletId(null);
960
+ setUser(null);
961
+ };
962
+ const handleSpendingLimitReached = () => {
963
+ setErrors((prev) => ({ ...prev, spendingLimit: true }));
489
964
  };
490
965
  sdk.on("connect_start", handleConnectStart);
491
966
  sdk.on("connect", handleConnect);
492
967
  sdk.on("connect_error", handleConnectError);
493
968
  sdk.on("disconnect", handleDisconnect);
969
+ sdk.on("spending_limit_reached", handleSpendingLimitReached);
494
970
  return () => {
495
971
  sdk.off("connect_start", handleConnectStart);
496
972
  sdk.off("connect", handleConnect);
497
973
  sdk.off("connect_error", handleConnectError);
498
974
  sdk.off("disconnect", handleDisconnect);
975
+ sdk.off("spending_limit_reached", handleSpendingLimitReached);
499
976
  };
500
977
  }, [sdk]);
501
- useEffect(() => {
502
- if (config.autoConnect !== false) {
503
- sdk.autoConnect().catch(() => {
504
- });
505
- }
506
- }, [sdk, config.autoConnect]);
507
- const value = useMemo(
978
+ useEffect2(() => {
979
+ sdk.autoConnect().catch(() => {
980
+ });
981
+ }, [sdk]);
982
+ const clearError = useCallback5((key) => {
983
+ setErrors(({ [key]: _, ...next }) => next);
984
+ }, []);
985
+ const value = useMemo2(
508
986
  () => ({
509
987
  sdk,
510
988
  isConnected,
511
989
  isConnecting,
512
- connectError,
990
+ errors,
513
991
  addresses,
514
992
  walletId,
515
- setWalletId
993
+ setWalletId,
994
+ user,
995
+ allowedProviders: config.providers,
996
+ clearError
516
997
  }),
517
- [sdk, isConnected, isConnecting, connectError, addresses, walletId, setWalletId]
518
- );
519
- return /* @__PURE__ */ jsx(PhantomContext.Provider, { value, children });
520
- }
521
- function usePhantom() {
522
- const context = useContext(PhantomContext);
523
- if (context === void 0) {
524
- throw new Error("usePhantom must be used within a PhantomProvider");
525
- }
526
- return context;
527
- }
528
-
529
- // src/hooks/useConnect.ts
530
- import { useCallback } from "react";
531
- function useConnect() {
532
- const { sdk, isConnecting, connectError, setWalletId } = usePhantom();
533
- const connect = useCallback(
534
- async (_options) => {
535
- if (!sdk) {
536
- throw new Error("SDK not initialized");
537
- }
538
- try {
539
- const result = await sdk.connect();
540
- if (result.status === "completed" && result.walletId) {
541
- setWalletId(result.walletId);
542
- }
543
- return result;
544
- } catch (err) {
545
- const error = err;
546
- throw error;
547
- }
548
- },
549
- [sdk, setWalletId]
998
+ [sdk, isConnected, isConnecting, errors, addresses, walletId, setWalletId, user, config.providers, clearError]
550
999
  );
551
- return {
552
- connect,
553
- isConnecting,
554
- error: connectError
555
- };
556
- }
557
-
558
- // src/hooks/useDisconnect.ts
559
- import { useState as useState2, useCallback as useCallback2 } from "react";
560
- function useDisconnect() {
561
- const { sdk } = usePhantom();
562
- const [isDisconnecting, setIsDisconnecting] = useState2(false);
563
- const [error, setError] = useState2(null);
564
- const disconnect = useCallback2(async () => {
565
- if (!sdk) {
566
- throw new Error("SDK not initialized");
567
- }
568
- setIsDisconnecting(true);
569
- setError(null);
570
- try {
571
- await sdk.disconnect();
572
- } catch (err) {
573
- const error2 = err;
574
- setError(error2);
575
- throw error2;
576
- } finally {
577
- setIsDisconnecting(false);
578
- }
579
- }, [sdk]);
580
- return {
581
- disconnect,
582
- isDisconnecting,
583
- error
584
- };
1000
+ const resolvedTheme = theme || darkTheme;
1001
+ return /* @__PURE__ */ jsx6(ThemeProvider, { theme: resolvedTheme, children: /* @__PURE__ */ jsx6(PhantomContext.Provider, { value, children: /* @__PURE__ */ jsx6(ModalProvider, { appIcon, appName, children }) }) });
585
1002
  }
586
1003
 
587
1004
  // src/hooks/useAccounts.ts
@@ -598,9 +1015,7 @@ function useAccounts() {
598
1015
  function useSolana() {
599
1016
  const { sdk, isConnected } = usePhantom();
600
1017
  return {
601
- // Chain instance with connection enforcement for signing methods
602
1018
  solana: sdk.solana,
603
- // State
604
1019
  isAvailable: !!isConnected
605
1020
  };
606
1021
  }
@@ -619,14 +1034,18 @@ function useEthereum() {
619
1034
  // src/index.ts
620
1035
  import { AddressType } from "@phantom/client";
621
1036
  import { NetworkId } from "@phantom/constants";
1037
+ import { darkTheme as darkTheme2, lightTheme } from "@phantom/wallet-sdk-ui";
622
1038
  export {
623
1039
  AddressType,
624
1040
  NetworkId,
625
1041
  PhantomProvider,
1042
+ darkTheme2 as darkTheme,
1043
+ lightTheme,
626
1044
  useAccounts,
627
1045
  useConnect,
628
1046
  useDisconnect,
629
1047
  useEthereum,
1048
+ useModal,
630
1049
  usePhantom,
631
1050
  useSolana
632
1051
  };