@phantom/react-native-sdk 0.1.0 → 0.1.2
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 +11 -5
- package/dist/index.d.ts +8 -13
- package/dist/index.js +201 -17
- package/dist/index.mjs +201 -17
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -94,9 +94,12 @@ export default function App() {
|
|
|
94
94
|
embeddedWalletType: "user-wallet",
|
|
95
95
|
addressTypes: [AddressType.solana],
|
|
96
96
|
apiBaseUrl: "https://api.phantom.app/v1/wallets",
|
|
97
|
+
solanaProvider: "web3js",
|
|
97
98
|
authOptions: {
|
|
98
99
|
redirectUrl: "mywalletapp://phantom-auth-callback",
|
|
99
100
|
},
|
|
101
|
+
appName: "My Wallet App", // Optional branding
|
|
102
|
+
debug: false, // Optional debug logging
|
|
100
103
|
}}
|
|
101
104
|
>
|
|
102
105
|
<YourAppContent />
|
|
@@ -115,7 +118,7 @@ import { useConnect, useAccounts, useSignMessage, useDisconnect } from "@phantom
|
|
|
115
118
|
|
|
116
119
|
export function WalletScreen() {
|
|
117
120
|
const { connect, isConnecting, error: connectError } = useConnect();
|
|
118
|
-
const { addresses, isConnected
|
|
121
|
+
const { addresses, isConnected } = useAccounts();
|
|
119
122
|
const { signMessage, isSigning } = useSignMessage();
|
|
120
123
|
const { disconnect } = useDisconnect();
|
|
121
124
|
|
|
@@ -156,7 +159,6 @@ export function WalletScreen() {
|
|
|
156
159
|
return (
|
|
157
160
|
<View style={{ padding: 20 }}>
|
|
158
161
|
<Text style={{ fontSize: 18, marginBottom: 10 }}>Wallet Connected</Text>
|
|
159
|
-
<Text>Wallet ID: {walletId}</Text>
|
|
160
162
|
<Text>Address: {addresses[0]?.address}</Text>
|
|
161
163
|
|
|
162
164
|
<Button
|
|
@@ -187,16 +189,20 @@ The main provider component that initializes the SDK and provides context to all
|
|
|
187
189
|
#### Configuration Options
|
|
188
190
|
|
|
189
191
|
```typescript
|
|
190
|
-
interface
|
|
192
|
+
interface PhantomSDKConfig {
|
|
191
193
|
organizationId: string; // Your Phantom organization ID
|
|
192
194
|
scheme: string; // Custom URL scheme for your app
|
|
193
195
|
embeddedWalletType: "user-wallet" | "app-wallet";
|
|
194
|
-
addressTypes: AddressType[];
|
|
195
|
-
apiBaseUrl: "https://api.phantom.app/v1/wallets"
|
|
196
|
+
addressTypes: AddressType[]; // e.g., [AddressType.solana]
|
|
197
|
+
apiBaseUrl: string; // e.g., "https://api.phantom.app/v1/wallets"
|
|
198
|
+
solanaProvider: "web3js" | "kit"; // Solana provider to use
|
|
196
199
|
authOptions?: {
|
|
197
200
|
authUrl?: string; // Custom auth URL (optional)
|
|
198
201
|
redirectUrl?: string; // Custom redirect URL (optional)
|
|
199
202
|
};
|
|
203
|
+
appName?: string; // Optional app name for branding
|
|
204
|
+
appLogo?: string; // Optional app logo URL for branding
|
|
205
|
+
debug?: boolean; // Enable debug logging (optional)
|
|
200
206
|
}
|
|
201
207
|
```
|
|
202
208
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import * as _phantom_embedded_provider_core from '@phantom/embedded-provider-core';
|
|
4
|
-
import { EmbeddedProviderConfig,
|
|
5
|
-
export { ConnectResult, SignAndSendTransactionParams, SignMessageParams, SignedTransaction, WalletAddress } from '@phantom/embedded-provider-core';
|
|
6
|
-
export { AddressType
|
|
4
|
+
import { EmbeddedProviderConfig, EmbeddedProvider, WalletAddress, ConnectResult, SignMessageParams, SignMessageResult, SignAndSendTransactionParams, SignedTransaction } from '@phantom/embedded-provider-core';
|
|
5
|
+
export { ConnectResult, SignAndSendTransactionParams, SignMessageParams, SignMessageResult, SignedTransaction, WalletAddress } from '@phantom/embedded-provider-core';
|
|
6
|
+
export { AddressType } from '@phantom/client';
|
|
7
|
+
export { NetworkId } from '@phantom/constants';
|
|
7
8
|
|
|
8
|
-
interface
|
|
9
|
+
interface PhantomSDKConfig extends EmbeddedProviderConfig {
|
|
9
10
|
/** Custom URL scheme for your app (e.g., "myapp") */
|
|
10
11
|
scheme: string;
|
|
11
|
-
/** Authentication options */
|
|
12
|
-
authOptions?: ReactNativeAuthOptions;
|
|
13
12
|
/** Enable debug logging */
|
|
14
13
|
debug?: boolean;
|
|
15
14
|
}
|
|
16
|
-
interface ReactNativeAuthOptions extends AuthOptions {
|
|
17
|
-
/** Custom redirect URL - defaults to {scheme}://phantom-auth-callback */
|
|
18
|
-
redirectUrl?: string;
|
|
19
|
-
}
|
|
20
15
|
interface ConnectOptions {
|
|
21
16
|
/** OAuth provider to use */
|
|
22
17
|
provider?: "google" | "apple" | "jwt";
|
|
@@ -37,7 +32,7 @@ interface PhantomContextValue {
|
|
|
37
32
|
}
|
|
38
33
|
interface PhantomProviderProps {
|
|
39
34
|
children: ReactNode;
|
|
40
|
-
config:
|
|
35
|
+
config: PhantomSDKConfig;
|
|
41
36
|
}
|
|
42
37
|
declare function PhantomProvider({ children, config }: PhantomProviderProps): react_jsx_runtime.JSX.Element;
|
|
43
38
|
/**
|
|
@@ -66,7 +61,7 @@ declare function useAccounts(): {
|
|
|
66
61
|
};
|
|
67
62
|
|
|
68
63
|
declare function useSignMessage(): {
|
|
69
|
-
signMessage: (params: SignMessageParams) => Promise<
|
|
64
|
+
signMessage: (params: SignMessageParams) => Promise<SignMessageResult>;
|
|
70
65
|
isSigning: boolean;
|
|
71
66
|
error: Error | null;
|
|
72
67
|
};
|
|
@@ -77,4 +72,4 @@ declare function useSignAndSendTransaction(): {
|
|
|
77
72
|
error: Error | null;
|
|
78
73
|
};
|
|
79
74
|
|
|
80
|
-
export { ConnectOptions, PhantomProvider,
|
|
75
|
+
export { ConnectOptions, PhantomProvider, PhantomSDKConfig, useAccounts, useConnect, useDisconnect, usePhantom, useSignAndSendTransaction, useSignMessage };
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
AddressType: () => import_client.AddressType,
|
|
34
|
-
NetworkId: () =>
|
|
34
|
+
NetworkId: () => import_constants.NetworkId,
|
|
35
35
|
PhantomProvider: () => PhantomProvider,
|
|
36
36
|
useAccounts: () => useAccounts,
|
|
37
37
|
useConnect: () => useConnect,
|
|
@@ -96,22 +96,66 @@ var ExpoSecureStorage = class {
|
|
|
96
96
|
|
|
97
97
|
// src/providers/embedded/auth.ts
|
|
98
98
|
var WebBrowser = __toESM(require("expo-web-browser"));
|
|
99
|
+
var DEFAULT_AUTH_URL = "https://auth.phantom.app";
|
|
99
100
|
var ExpoAuthProvider = class {
|
|
100
101
|
async authenticate(options) {
|
|
101
102
|
if ("jwtToken" in options) {
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
const phantomOptions = options;
|
|
106
|
+
const {
|
|
107
|
+
authUrl,
|
|
108
|
+
redirectUrl,
|
|
109
|
+
organizationId,
|
|
110
|
+
parentOrganizationId,
|
|
111
|
+
sessionId,
|
|
112
|
+
provider,
|
|
113
|
+
customAuthData,
|
|
114
|
+
appName,
|
|
115
|
+
appLogo
|
|
116
|
+
} = phantomOptions;
|
|
117
|
+
if (!redirectUrl) {
|
|
118
|
+
throw new Error("redirectUrl is required for web browser authentication");
|
|
119
|
+
}
|
|
120
|
+
if (!organizationId || !sessionId) {
|
|
121
|
+
throw new Error("organizationId and sessionId are required for authentication");
|
|
107
122
|
}
|
|
108
123
|
try {
|
|
124
|
+
const baseUrl = authUrl || DEFAULT_AUTH_URL;
|
|
125
|
+
const params = new URLSearchParams({
|
|
126
|
+
organization_id: organizationId,
|
|
127
|
+
parent_organization_id: parentOrganizationId,
|
|
128
|
+
redirect_uri: redirectUrl,
|
|
129
|
+
session_id: sessionId,
|
|
130
|
+
clear_previous_session: "true",
|
|
131
|
+
app_name: appName || "",
|
|
132
|
+
// Optional app name
|
|
133
|
+
app_logo: appLogo || ""
|
|
134
|
+
// Optional app logo URL
|
|
135
|
+
});
|
|
136
|
+
if (provider) {
|
|
137
|
+
console.log("[ExpoAuthProvider] Provider specified, will skip selection", { provider });
|
|
138
|
+
params.append("provider", provider);
|
|
139
|
+
} else {
|
|
140
|
+
console.log("[ExpoAuthProvider] No provider specified, defaulting to Google");
|
|
141
|
+
params.append("provider", "google");
|
|
142
|
+
}
|
|
143
|
+
if (customAuthData) {
|
|
144
|
+
console.log("[ExpoAuthProvider] Adding custom auth data");
|
|
145
|
+
params.append("authData", JSON.stringify(customAuthData));
|
|
146
|
+
}
|
|
147
|
+
const fullAuthUrl = `${baseUrl}?${params.toString()}`;
|
|
109
148
|
console.log("[ExpoAuthProvider] Starting authentication", {
|
|
110
|
-
|
|
111
|
-
redirectUrl
|
|
149
|
+
baseUrl,
|
|
150
|
+
redirectUrl,
|
|
151
|
+
organizationId,
|
|
152
|
+
parentOrganizationId,
|
|
153
|
+
sessionId,
|
|
154
|
+
provider,
|
|
155
|
+
hasCustomData: !!customAuthData
|
|
112
156
|
});
|
|
113
157
|
await WebBrowser.warmUpAsync();
|
|
114
|
-
const result = await WebBrowser.openAuthSessionAsync(
|
|
158
|
+
const result = await WebBrowser.openAuthSessionAsync(fullAuthUrl, redirectUrl, {
|
|
115
159
|
// Use system browser on iOS for ASWebAuthenticationSession
|
|
116
160
|
preferEphemeralSession: false
|
|
117
161
|
});
|
|
@@ -121,19 +165,14 @@ var ExpoAuthProvider = class {
|
|
|
121
165
|
});
|
|
122
166
|
if (result.type === "success" && result.url) {
|
|
123
167
|
const url = new URL(result.url);
|
|
124
|
-
const walletId = url.searchParams.get("
|
|
125
|
-
const
|
|
168
|
+
const walletId = url.searchParams.get("wallet_id");
|
|
169
|
+
const provider2 = url.searchParams.get("provider");
|
|
126
170
|
if (!walletId) {
|
|
127
171
|
throw new Error("Authentication failed: no walletId in redirect URL");
|
|
128
172
|
}
|
|
129
|
-
const userInfo = {};
|
|
130
|
-
url.searchParams.forEach((value, key) => {
|
|
131
|
-
userInfo[key] = value;
|
|
132
|
-
});
|
|
133
173
|
return {
|
|
134
174
|
walletId,
|
|
135
|
-
provider:
|
|
136
|
-
userInfo
|
|
175
|
+
provider: provider2 || void 0
|
|
137
176
|
};
|
|
138
177
|
} else if (result.type === "cancel") {
|
|
139
178
|
throw new Error("User cancelled authentication");
|
|
@@ -247,7 +286,134 @@ var ExpoLogger = class {
|
|
|
247
286
|
}
|
|
248
287
|
};
|
|
249
288
|
|
|
289
|
+
// src/providers/embedded/stamper.ts
|
|
290
|
+
var SecureStore2 = __toESM(require("expo-secure-store"));
|
|
291
|
+
var import_api_key_stamper = require("@phantom/api-key-stamper");
|
|
292
|
+
var import_crypto = require("@phantom/crypto");
|
|
293
|
+
var import_base64url = require("@phantom/base64url");
|
|
294
|
+
var import_sdk_types = require("@phantom/sdk-types");
|
|
295
|
+
var ReactNativeStamper = class {
|
|
296
|
+
constructor(config = {}) {
|
|
297
|
+
this.keyInfo = null;
|
|
298
|
+
this.algorithm = import_sdk_types.Algorithm.ed25519;
|
|
299
|
+
this.keyPrefix = config.keyPrefix || "phantom-rn-stamper";
|
|
300
|
+
this.organizationId = config.organizationId || "default";
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Initialize the stamper and generate/load cryptographic keys
|
|
304
|
+
*/
|
|
305
|
+
async init() {
|
|
306
|
+
const storedSecretKey = await this.getStoredSecretKey();
|
|
307
|
+
if (storedSecretKey) {
|
|
308
|
+
const keyInfo2 = await this.getStoredKeyInfo();
|
|
309
|
+
if (keyInfo2) {
|
|
310
|
+
this.keyInfo = keyInfo2;
|
|
311
|
+
return keyInfo2;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const keyInfo = await this.generateAndStoreKeyPair();
|
|
315
|
+
this.keyInfo = keyInfo;
|
|
316
|
+
return keyInfo;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get the current key information
|
|
320
|
+
*/
|
|
321
|
+
getKeyInfo() {
|
|
322
|
+
return this.keyInfo;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Generate and store a new key pair, replacing any existing keys
|
|
326
|
+
*/
|
|
327
|
+
async resetKeyPair() {
|
|
328
|
+
await this.clear();
|
|
329
|
+
const keyInfo = await this.generateAndStoreKeyPair();
|
|
330
|
+
this.keyInfo = keyInfo;
|
|
331
|
+
return keyInfo;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Create X-Phantom-Stamp header value using stored secret key
|
|
335
|
+
* @param data - Data to sign (Buffer)
|
|
336
|
+
* @returns Complete X-Phantom-Stamp header value
|
|
337
|
+
*/
|
|
338
|
+
async stamp({ data }) {
|
|
339
|
+
if (!this.keyInfo) {
|
|
340
|
+
throw new Error("Stamper not initialized. Call init() first.");
|
|
341
|
+
}
|
|
342
|
+
const storedSecretKey = await this.getStoredSecretKey();
|
|
343
|
+
if (!storedSecretKey) {
|
|
344
|
+
throw new Error("Secret key not found in secure storage");
|
|
345
|
+
}
|
|
346
|
+
const apiKeyStamper = new import_api_key_stamper.ApiKeyStamper({ apiSecretKey: storedSecretKey });
|
|
347
|
+
return await apiKeyStamper.stamp({ data });
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Clear all stored keys from SecureStore
|
|
351
|
+
*/
|
|
352
|
+
async clear() {
|
|
353
|
+
const infoKey = this.getInfoKey();
|
|
354
|
+
const secretKey = this.getSecretKey();
|
|
355
|
+
try {
|
|
356
|
+
await SecureStore2.deleteItemAsync(infoKey);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await SecureStore2.deleteItemAsync(secretKey);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
}
|
|
363
|
+
this.keyInfo = null;
|
|
364
|
+
}
|
|
365
|
+
async generateAndStoreKeyPair() {
|
|
366
|
+
const keypair = (0, import_crypto.generateKeyPair)();
|
|
367
|
+
const keyId = this.createKeyId(keypair.publicKey);
|
|
368
|
+
const keyInfo = {
|
|
369
|
+
keyId,
|
|
370
|
+
publicKey: keypair.publicKey
|
|
371
|
+
};
|
|
372
|
+
await this.storeKeyPair(keypair.secretKey, keyInfo);
|
|
373
|
+
return keyInfo;
|
|
374
|
+
}
|
|
375
|
+
createKeyId(publicKey) {
|
|
376
|
+
return (0, import_base64url.base64urlEncode)(new TextEncoder().encode(publicKey)).substring(0, 16);
|
|
377
|
+
}
|
|
378
|
+
async storeKeyPair(secretKey, keyInfo) {
|
|
379
|
+
const infoKey = this.getInfoKey();
|
|
380
|
+
const secretKeyName = this.getSecretKey();
|
|
381
|
+
await SecureStore2.setItemAsync(infoKey, JSON.stringify(keyInfo), {
|
|
382
|
+
requireAuthentication: false
|
|
383
|
+
});
|
|
384
|
+
await SecureStore2.setItemAsync(secretKeyName, secretKey, {
|
|
385
|
+
requireAuthentication: false
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async getStoredKeyInfo() {
|
|
389
|
+
try {
|
|
390
|
+
const infoKey = this.getInfoKey();
|
|
391
|
+
const storedInfo = await SecureStore2.getItemAsync(infoKey);
|
|
392
|
+
if (storedInfo) {
|
|
393
|
+
return JSON.parse(storedInfo);
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
async getStoredSecretKey() {
|
|
400
|
+
try {
|
|
401
|
+
const secretKeyName = this.getSecretKey();
|
|
402
|
+
return await SecureStore2.getItemAsync(secretKeyName);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
getInfoKey() {
|
|
408
|
+
return `${this.keyPrefix}-${this.organizationId}-info`;
|
|
409
|
+
}
|
|
410
|
+
getSecretKey() {
|
|
411
|
+
return `${this.keyPrefix}-${this.organizationId}-secret`;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
250
415
|
// src/PhantomProvider.tsx
|
|
416
|
+
var import_react_native2 = require("react-native");
|
|
251
417
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
252
418
|
var PhantomContext = (0, import_react.createContext)(void 0);
|
|
253
419
|
function PhantomProvider({ children, config }) {
|
|
@@ -262,16 +428,25 @@ function PhantomProvider({ children, config }) {
|
|
|
262
428
|
},
|
|
263
429
|
embeddedWalletType: config.embeddedWalletType,
|
|
264
430
|
addressTypes: config.addressTypes,
|
|
265
|
-
solanaProvider: config.solanaProvider || "web3js"
|
|
431
|
+
solanaProvider: config.solanaProvider || "web3js",
|
|
432
|
+
appName: config.appName,
|
|
433
|
+
appLogo: config.appLogo
|
|
434
|
+
// Optional app logo URL
|
|
266
435
|
};
|
|
267
436
|
const storage = new ExpoSecureStorage();
|
|
268
437
|
const authProvider = new ExpoAuthProvider();
|
|
269
438
|
const urlParamsAccessor = new ExpoURLParamsAccessor();
|
|
270
439
|
const logger = new ExpoLogger(config.debug);
|
|
440
|
+
const stamper = new ReactNativeStamper({
|
|
441
|
+
keyPrefix: `phantom-rn-${config.organizationId}`,
|
|
442
|
+
organizationId: config.organizationId
|
|
443
|
+
});
|
|
271
444
|
const platform = {
|
|
272
445
|
storage,
|
|
273
446
|
authProvider,
|
|
274
|
-
urlParamsAccessor
|
|
447
|
+
urlParamsAccessor,
|
|
448
|
+
stamper,
|
|
449
|
+
name: `${import_react_native2.Platform.OS}-${import_react_native2.Platform.Version}`
|
|
275
450
|
};
|
|
276
451
|
return new import_embedded_provider_core.EmbeddedProvider(embeddedConfig, platform, logger);
|
|
277
452
|
}, [config]);
|
|
@@ -293,6 +468,14 @@ function PhantomProvider({ children, config }) {
|
|
|
293
468
|
} catch (err) {
|
|
294
469
|
console.error("[PhantomProvider] Error updating connection state", err);
|
|
295
470
|
setError(err);
|
|
471
|
+
try {
|
|
472
|
+
sdk.disconnect();
|
|
473
|
+
setIsConnected(false);
|
|
474
|
+
setAddresses([]);
|
|
475
|
+
setWalletId(null);
|
|
476
|
+
} catch (disconnectErr) {
|
|
477
|
+
console.error("[PhantomProvider] Error disconnecting after error", disconnectErr);
|
|
478
|
+
}
|
|
296
479
|
}
|
|
297
480
|
}, [sdk]);
|
|
298
481
|
(0, import_react.useEffect)(() => {
|
|
@@ -465,6 +648,7 @@ function useSignAndSendTransaction() {
|
|
|
465
648
|
|
|
466
649
|
// src/index.ts
|
|
467
650
|
var import_client = require("@phantom/client");
|
|
651
|
+
var import_constants = require("@phantom/constants");
|
|
468
652
|
// Annotate the CommonJS export names for ESM import in node:
|
|
469
653
|
0 && (module.exports = {
|
|
470
654
|
AddressType,
|
package/dist/index.mjs
CHANGED
|
@@ -52,22 +52,66 @@ var ExpoSecureStorage = class {
|
|
|
52
52
|
|
|
53
53
|
// src/providers/embedded/auth.ts
|
|
54
54
|
import * as WebBrowser from "expo-web-browser";
|
|
55
|
+
var DEFAULT_AUTH_URL = "https://auth.phantom.app";
|
|
55
56
|
var ExpoAuthProvider = class {
|
|
56
57
|
async authenticate(options) {
|
|
57
58
|
if ("jwtToken" in options) {
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const phantomOptions = options;
|
|
62
|
+
const {
|
|
63
|
+
authUrl,
|
|
64
|
+
redirectUrl,
|
|
65
|
+
organizationId,
|
|
66
|
+
parentOrganizationId,
|
|
67
|
+
sessionId,
|
|
68
|
+
provider,
|
|
69
|
+
customAuthData,
|
|
70
|
+
appName,
|
|
71
|
+
appLogo
|
|
72
|
+
} = phantomOptions;
|
|
73
|
+
if (!redirectUrl) {
|
|
74
|
+
throw new Error("redirectUrl is required for web browser authentication");
|
|
75
|
+
}
|
|
76
|
+
if (!organizationId || !sessionId) {
|
|
77
|
+
throw new Error("organizationId and sessionId are required for authentication");
|
|
63
78
|
}
|
|
64
79
|
try {
|
|
80
|
+
const baseUrl = authUrl || DEFAULT_AUTH_URL;
|
|
81
|
+
const params = new URLSearchParams({
|
|
82
|
+
organization_id: organizationId,
|
|
83
|
+
parent_organization_id: parentOrganizationId,
|
|
84
|
+
redirect_uri: redirectUrl,
|
|
85
|
+
session_id: sessionId,
|
|
86
|
+
clear_previous_session: "true",
|
|
87
|
+
app_name: appName || "",
|
|
88
|
+
// Optional app name
|
|
89
|
+
app_logo: appLogo || ""
|
|
90
|
+
// Optional app logo URL
|
|
91
|
+
});
|
|
92
|
+
if (provider) {
|
|
93
|
+
console.log("[ExpoAuthProvider] Provider specified, will skip selection", { provider });
|
|
94
|
+
params.append("provider", provider);
|
|
95
|
+
} else {
|
|
96
|
+
console.log("[ExpoAuthProvider] No provider specified, defaulting to Google");
|
|
97
|
+
params.append("provider", "google");
|
|
98
|
+
}
|
|
99
|
+
if (customAuthData) {
|
|
100
|
+
console.log("[ExpoAuthProvider] Adding custom auth data");
|
|
101
|
+
params.append("authData", JSON.stringify(customAuthData));
|
|
102
|
+
}
|
|
103
|
+
const fullAuthUrl = `${baseUrl}?${params.toString()}`;
|
|
65
104
|
console.log("[ExpoAuthProvider] Starting authentication", {
|
|
66
|
-
|
|
67
|
-
redirectUrl
|
|
105
|
+
baseUrl,
|
|
106
|
+
redirectUrl,
|
|
107
|
+
organizationId,
|
|
108
|
+
parentOrganizationId,
|
|
109
|
+
sessionId,
|
|
110
|
+
provider,
|
|
111
|
+
hasCustomData: !!customAuthData
|
|
68
112
|
});
|
|
69
113
|
await WebBrowser.warmUpAsync();
|
|
70
|
-
const result = await WebBrowser.openAuthSessionAsync(
|
|
114
|
+
const result = await WebBrowser.openAuthSessionAsync(fullAuthUrl, redirectUrl, {
|
|
71
115
|
// Use system browser on iOS for ASWebAuthenticationSession
|
|
72
116
|
preferEphemeralSession: false
|
|
73
117
|
});
|
|
@@ -77,19 +121,14 @@ var ExpoAuthProvider = class {
|
|
|
77
121
|
});
|
|
78
122
|
if (result.type === "success" && result.url) {
|
|
79
123
|
const url = new URL(result.url);
|
|
80
|
-
const walletId = url.searchParams.get("
|
|
81
|
-
const
|
|
124
|
+
const walletId = url.searchParams.get("wallet_id");
|
|
125
|
+
const provider2 = url.searchParams.get("provider");
|
|
82
126
|
if (!walletId) {
|
|
83
127
|
throw new Error("Authentication failed: no walletId in redirect URL");
|
|
84
128
|
}
|
|
85
|
-
const userInfo = {};
|
|
86
|
-
url.searchParams.forEach((value, key) => {
|
|
87
|
-
userInfo[key] = value;
|
|
88
|
-
});
|
|
89
129
|
return {
|
|
90
130
|
walletId,
|
|
91
|
-
provider:
|
|
92
|
-
userInfo
|
|
131
|
+
provider: provider2 || void 0
|
|
93
132
|
};
|
|
94
133
|
} else if (result.type === "cancel") {
|
|
95
134
|
throw new Error("User cancelled authentication");
|
|
@@ -203,7 +242,134 @@ var ExpoLogger = class {
|
|
|
203
242
|
}
|
|
204
243
|
};
|
|
205
244
|
|
|
245
|
+
// src/providers/embedded/stamper.ts
|
|
246
|
+
import * as SecureStore2 from "expo-secure-store";
|
|
247
|
+
import { ApiKeyStamper } from "@phantom/api-key-stamper";
|
|
248
|
+
import { generateKeyPair } from "@phantom/crypto";
|
|
249
|
+
import { base64urlEncode } from "@phantom/base64url";
|
|
250
|
+
import { Algorithm } from "@phantom/sdk-types";
|
|
251
|
+
var ReactNativeStamper = class {
|
|
252
|
+
constructor(config = {}) {
|
|
253
|
+
this.keyInfo = null;
|
|
254
|
+
this.algorithm = Algorithm.ed25519;
|
|
255
|
+
this.keyPrefix = config.keyPrefix || "phantom-rn-stamper";
|
|
256
|
+
this.organizationId = config.organizationId || "default";
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Initialize the stamper and generate/load cryptographic keys
|
|
260
|
+
*/
|
|
261
|
+
async init() {
|
|
262
|
+
const storedSecretKey = await this.getStoredSecretKey();
|
|
263
|
+
if (storedSecretKey) {
|
|
264
|
+
const keyInfo2 = await this.getStoredKeyInfo();
|
|
265
|
+
if (keyInfo2) {
|
|
266
|
+
this.keyInfo = keyInfo2;
|
|
267
|
+
return keyInfo2;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const keyInfo = await this.generateAndStoreKeyPair();
|
|
271
|
+
this.keyInfo = keyInfo;
|
|
272
|
+
return keyInfo;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get the current key information
|
|
276
|
+
*/
|
|
277
|
+
getKeyInfo() {
|
|
278
|
+
return this.keyInfo;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Generate and store a new key pair, replacing any existing keys
|
|
282
|
+
*/
|
|
283
|
+
async resetKeyPair() {
|
|
284
|
+
await this.clear();
|
|
285
|
+
const keyInfo = await this.generateAndStoreKeyPair();
|
|
286
|
+
this.keyInfo = keyInfo;
|
|
287
|
+
return keyInfo;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Create X-Phantom-Stamp header value using stored secret key
|
|
291
|
+
* @param data - Data to sign (Buffer)
|
|
292
|
+
* @returns Complete X-Phantom-Stamp header value
|
|
293
|
+
*/
|
|
294
|
+
async stamp({ data }) {
|
|
295
|
+
if (!this.keyInfo) {
|
|
296
|
+
throw new Error("Stamper not initialized. Call init() first.");
|
|
297
|
+
}
|
|
298
|
+
const storedSecretKey = await this.getStoredSecretKey();
|
|
299
|
+
if (!storedSecretKey) {
|
|
300
|
+
throw new Error("Secret key not found in secure storage");
|
|
301
|
+
}
|
|
302
|
+
const apiKeyStamper = new ApiKeyStamper({ apiSecretKey: storedSecretKey });
|
|
303
|
+
return await apiKeyStamper.stamp({ data });
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Clear all stored keys from SecureStore
|
|
307
|
+
*/
|
|
308
|
+
async clear() {
|
|
309
|
+
const infoKey = this.getInfoKey();
|
|
310
|
+
const secretKey = this.getSecretKey();
|
|
311
|
+
try {
|
|
312
|
+
await SecureStore2.deleteItemAsync(infoKey);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await SecureStore2.deleteItemAsync(secretKey);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
}
|
|
319
|
+
this.keyInfo = null;
|
|
320
|
+
}
|
|
321
|
+
async generateAndStoreKeyPair() {
|
|
322
|
+
const keypair = generateKeyPair();
|
|
323
|
+
const keyId = this.createKeyId(keypair.publicKey);
|
|
324
|
+
const keyInfo = {
|
|
325
|
+
keyId,
|
|
326
|
+
publicKey: keypair.publicKey
|
|
327
|
+
};
|
|
328
|
+
await this.storeKeyPair(keypair.secretKey, keyInfo);
|
|
329
|
+
return keyInfo;
|
|
330
|
+
}
|
|
331
|
+
createKeyId(publicKey) {
|
|
332
|
+
return base64urlEncode(new TextEncoder().encode(publicKey)).substring(0, 16);
|
|
333
|
+
}
|
|
334
|
+
async storeKeyPair(secretKey, keyInfo) {
|
|
335
|
+
const infoKey = this.getInfoKey();
|
|
336
|
+
const secretKeyName = this.getSecretKey();
|
|
337
|
+
await SecureStore2.setItemAsync(infoKey, JSON.stringify(keyInfo), {
|
|
338
|
+
requireAuthentication: false
|
|
339
|
+
});
|
|
340
|
+
await SecureStore2.setItemAsync(secretKeyName, secretKey, {
|
|
341
|
+
requireAuthentication: false
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async getStoredKeyInfo() {
|
|
345
|
+
try {
|
|
346
|
+
const infoKey = this.getInfoKey();
|
|
347
|
+
const storedInfo = await SecureStore2.getItemAsync(infoKey);
|
|
348
|
+
if (storedInfo) {
|
|
349
|
+
return JSON.parse(storedInfo);
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
async getStoredSecretKey() {
|
|
356
|
+
try {
|
|
357
|
+
const secretKeyName = this.getSecretKey();
|
|
358
|
+
return await SecureStore2.getItemAsync(secretKeyName);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
getInfoKey() {
|
|
364
|
+
return `${this.keyPrefix}-${this.organizationId}-info`;
|
|
365
|
+
}
|
|
366
|
+
getSecretKey() {
|
|
367
|
+
return `${this.keyPrefix}-${this.organizationId}-secret`;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
206
371
|
// src/PhantomProvider.tsx
|
|
372
|
+
import { Platform } from "react-native";
|
|
207
373
|
import { jsx } from "react/jsx-runtime";
|
|
208
374
|
var PhantomContext = createContext(void 0);
|
|
209
375
|
function PhantomProvider({ children, config }) {
|
|
@@ -218,16 +384,25 @@ function PhantomProvider({ children, config }) {
|
|
|
218
384
|
},
|
|
219
385
|
embeddedWalletType: config.embeddedWalletType,
|
|
220
386
|
addressTypes: config.addressTypes,
|
|
221
|
-
solanaProvider: config.solanaProvider || "web3js"
|
|
387
|
+
solanaProvider: config.solanaProvider || "web3js",
|
|
388
|
+
appName: config.appName,
|
|
389
|
+
appLogo: config.appLogo
|
|
390
|
+
// Optional app logo URL
|
|
222
391
|
};
|
|
223
392
|
const storage = new ExpoSecureStorage();
|
|
224
393
|
const authProvider = new ExpoAuthProvider();
|
|
225
394
|
const urlParamsAccessor = new ExpoURLParamsAccessor();
|
|
226
395
|
const logger = new ExpoLogger(config.debug);
|
|
396
|
+
const stamper = new ReactNativeStamper({
|
|
397
|
+
keyPrefix: `phantom-rn-${config.organizationId}`,
|
|
398
|
+
organizationId: config.organizationId
|
|
399
|
+
});
|
|
227
400
|
const platform = {
|
|
228
401
|
storage,
|
|
229
402
|
authProvider,
|
|
230
|
-
urlParamsAccessor
|
|
403
|
+
urlParamsAccessor,
|
|
404
|
+
stamper,
|
|
405
|
+
name: `${Platform.OS}-${Platform.Version}`
|
|
231
406
|
};
|
|
232
407
|
return new EmbeddedProvider(embeddedConfig, platform, logger);
|
|
233
408
|
}, [config]);
|
|
@@ -249,6 +424,14 @@ function PhantomProvider({ children, config }) {
|
|
|
249
424
|
} catch (err) {
|
|
250
425
|
console.error("[PhantomProvider] Error updating connection state", err);
|
|
251
426
|
setError(err);
|
|
427
|
+
try {
|
|
428
|
+
sdk.disconnect();
|
|
429
|
+
setIsConnected(false);
|
|
430
|
+
setAddresses([]);
|
|
431
|
+
setWalletId(null);
|
|
432
|
+
} catch (disconnectErr) {
|
|
433
|
+
console.error("[PhantomProvider] Error disconnecting after error", disconnectErr);
|
|
434
|
+
}
|
|
252
435
|
}
|
|
253
436
|
}, [sdk]);
|
|
254
437
|
useEffect(() => {
|
|
@@ -420,7 +603,8 @@ function useSignAndSendTransaction() {
|
|
|
420
603
|
}
|
|
421
604
|
|
|
422
605
|
// src/index.ts
|
|
423
|
-
import { AddressType
|
|
606
|
+
import { AddressType } from "@phantom/client";
|
|
607
|
+
import { NetworkId } from "@phantom/constants";
|
|
424
608
|
export {
|
|
425
609
|
AddressType,
|
|
426
610
|
NetworkId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phantom/react-native-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Phantom Wallet SDK for React Native and Expo applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -45,8 +45,16 @@
|
|
|
45
45
|
"directory": "packages/react-native-sdk"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@phantom/
|
|
49
|
-
"@phantom/
|
|
48
|
+
"@phantom/api-key-stamper": "^0.1.3",
|
|
49
|
+
"@phantom/base64url": "^0.1.0",
|
|
50
|
+
"@phantom/client": "^0.1.6",
|
|
51
|
+
"@phantom/constants": "^0.0.2",
|
|
52
|
+
"@phantom/crypto": "^0.1.2",
|
|
53
|
+
"@phantom/embedded-provider-core": "^0.1.3",
|
|
54
|
+
"@phantom/sdk-types": "^0.1.2",
|
|
55
|
+
"@types/bs58": "^5.0.0",
|
|
56
|
+
"bs58": "^6.0.0",
|
|
57
|
+
"buffer": "^6.0.3"
|
|
50
58
|
},
|
|
51
59
|
"peerDependencies": {
|
|
52
60
|
"expo": ">=53.0.0",
|