@phantom/react-native-sdk 1.0.0-beta.9 → 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/README.md +190 -27
- package/dist/index.d.ts +37 -21
- package/dist/index.js +535 -118
- package/dist/index.mjs +535 -116
- package/package.json +18 -13
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,420 @@
|
|
|
1
1
|
// src/PhantomProvider.tsx
|
|
2
|
-
import {
|
|
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 {
|
|
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,
|
|
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 (!
|
|
68
|
-
throw new Error("
|
|
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
|
-
|
|
511
|
+
public_key: publicKey,
|
|
74
512
|
app_id: appId,
|
|
75
513
|
redirect_uri: redirectUrl,
|
|
76
514
|
session_id: sessionId,
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
const [
|
|
414
|
-
const [
|
|
415
|
-
const [
|
|
416
|
-
const [
|
|
417
|
-
const [
|
|
418
|
-
const memoizedConfig =
|
|
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 =
|
|
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 = `${
|
|
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]:
|
|
450
|
-
[ANALYTICS_HEADERS.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
|
|
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
|
-
|
|
928
|
+
useEffect2(() => {
|
|
460
929
|
const handleConnectStart = () => {
|
|
461
930
|
setIsConnecting(true);
|
|
462
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
552
|
-
|
|
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
|
};
|