@dubsdotapp/expo 0.2.22 → 0.2.24

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
@@ -456,7 +456,8 @@ var DubsClient = class {
456
456
  var STORAGE_KEYS = {
457
457
  MWA_AUTH_TOKEN: "dubs_mwa_auth_token",
458
458
  JWT_TOKEN: "dubs_jwt_token",
459
- PHANTOM_SESSION: "dubs_phantom_session"
459
+ PHANTOM_SESSION: "dubs_phantom_session",
460
+ PHANTOM_CONNECT_IN_FLIGHT: "dubs_phantom_connect_in_flight"
460
461
  };
461
462
  function createSecureStoreStorage() {
462
463
  let SecureStore = null;
@@ -494,7 +495,7 @@ import { Connection as Connection2 } from "@solana/web3.js";
494
495
 
495
496
  // src/managed-wallet.tsx
496
497
  import { createContext, useContext, useState, useEffect, useRef, useCallback } from "react";
497
- import { Platform as Platform2 } from "react-native";
498
+ import { Platform } from "react-native";
498
499
 
499
500
  // src/wallet/mwa-adapter.ts
500
501
  import { PublicKey } from "@solana/web3.js";
@@ -633,6 +634,7 @@ var DeeplinkHandler = class {
633
634
  constructor(redirectBase) {
634
635
  this.pending = /* @__PURE__ */ new Map();
635
636
  this.subscription = null;
637
+ this._coldStartUrl = null;
636
638
  this.redirectBase = redirectBase.replace(/\/+$/, "");
637
639
  console.log(TAG, "Created with redirectBase:", this.redirectBase);
638
640
  }
@@ -742,9 +744,16 @@ var DeeplinkHandler = class {
742
744
  this.pending.delete(id);
743
745
  req.resolve({ params });
744
746
  } else {
745
- console.log(TAG, "No pending requests to resolve \u2014 redirect may have arrived late");
747
+ console.log(TAG, "No pending requests to resolve \u2014 stashing as cold-start URL");
748
+ this._coldStartUrl = url;
746
749
  }
747
750
  }
751
+ /** Return and clear the stashed cold-start URL (if any). */
752
+ getColdStartUrl() {
753
+ const url = this._coldStartUrl;
754
+ this._coldStartUrl = null;
755
+ return url;
756
+ }
748
757
  /** Extract the request ID from the dubs_rid query parameter. */
749
758
  extractRequestId(url) {
750
759
  try {
@@ -836,6 +845,18 @@ var PhantomDeeplinkAdapter = class {
836
845
  redirect_link: redirectLink,
837
846
  app_url: appUrl
838
847
  });
848
+ if (this.config.storage) {
849
+ console.log(TAG2, "Saving in-flight connect state to storage");
850
+ await this.config.storage.setItem(
851
+ STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT,
852
+ JSON.stringify({
853
+ dappPublicKey: dappPubBase58,
854
+ dappSecretKey: bs582.encode(this._dappKeyPair.secretKey),
855
+ requestId,
856
+ createdAt: Date.now()
857
+ })
858
+ );
859
+ }
839
860
  const url = `https://phantom.app/ul/v1/connect?${params.toString()}`;
840
861
  console.log(TAG2, "Opening Phantom connect deeplink...");
841
862
  const response = await this.handler.send(url, requestId, this.timeout);
@@ -863,6 +884,8 @@ var PhantomDeeplinkAdapter = class {
863
884
  this._connected = true;
864
885
  console.log(TAG2, "Connected! Wallet:", this._publicKey.toBase58());
865
886
  this.config.onSessionChange?.(this.getSession());
887
+ this.config.storage?.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
888
+ });
866
889
  }
867
890
  disconnect() {
868
891
  console.log(TAG2, "disconnect() \u2014 clearing state, was connected:", this._connected, "wallet:", this._publicKey?.toBase58());
@@ -875,6 +898,73 @@ var PhantomDeeplinkAdapter = class {
875
898
  this.config.onSessionChange?.(null);
876
899
  console.log(TAG2, "Disconnected");
877
900
  }
901
+ /**
902
+ * Return and clear the stashed cold-start URL from the deeplink handler.
903
+ * Used by ManagedWalletProvider to detect an in-flight connect on cold start.
904
+ */
905
+ consumeColdStartUrl() {
906
+ return this.handler.getColdStartUrl();
907
+ }
908
+ /**
909
+ * Complete a connect flow that was interrupted by the OS killing the app (Android).
910
+ * Loads the saved in-flight keypair from storage, decrypts the Phantom response
911
+ * from the cold-start URL, and restores the session.
912
+ */
913
+ async completeConnectFromColdStart(url) {
914
+ if (!this.config.storage) {
915
+ throw new Error("Cannot recover cold-start connect: no storage configured");
916
+ }
917
+ console.log(TAG2, "completeConnectFromColdStart() \u2014 attempting recovery");
918
+ const raw = await this.config.storage.getItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT);
919
+ if (!raw) {
920
+ throw new Error("No in-flight connect state found in storage");
921
+ }
922
+ const inFlight = JSON.parse(raw);
923
+ const age = Date.now() - inFlight.createdAt;
924
+ if (age > 12e4) {
925
+ console.log(TAG2, `In-flight state expired (${Math.round(age / 1e3)}s old), clearing`);
926
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
927
+ });
928
+ throw new Error("In-flight connect state expired");
929
+ }
930
+ console.log(TAG2, `In-flight state age: ${Math.round(age / 1e3)}s, requestId: ${inFlight.requestId}`);
931
+ this._dappKeyPair = {
932
+ publicKey: bs582.decode(inFlight.dappPublicKey),
933
+ secretKey: bs582.decode(inFlight.dappSecretKey)
934
+ };
935
+ const parsed = new URL(url);
936
+ const params = {};
937
+ parsed.searchParams.forEach((value, key) => {
938
+ params[key] = value;
939
+ });
940
+ if (params.errorCode) {
941
+ const errorMessage = params.errorMessage ? decodeURIComponent(params.errorMessage) : `Phantom error code: ${params.errorCode}`;
942
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
943
+ });
944
+ throw new Error(errorMessage);
945
+ }
946
+ const phantomPubBase58 = params.phantom_encryption_public_key;
947
+ if (!phantomPubBase58) {
948
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
949
+ });
950
+ throw new Error("Phantom did not return an encryption public key");
951
+ }
952
+ console.log(TAG2, "Cold-start: Phantom public key:", phantomPubBase58);
953
+ this._phantomPublicKey = bs582.decode(phantomPubBase58);
954
+ this._sharedSecret = deriveSharedSecret(
955
+ this._dappKeyPair.secretKey,
956
+ this._phantomPublicKey
957
+ );
958
+ const data = decryptPayload(params.data, params.nonce, this._sharedSecret);
959
+ console.log(TAG2, "Cold-start: Decrypted connect data \u2014 public_key:", data.public_key);
960
+ this._sessionToken = data.session;
961
+ this._publicKey = new PublicKey2(data.public_key);
962
+ this._connected = true;
963
+ console.log(TAG2, "Cold-start recovery complete! Wallet:", this._publicKey.toBase58());
964
+ this.config.onSessionChange?.(this.getSession());
965
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
966
+ });
967
+ }
878
968
  async signTransaction(transaction) {
879
969
  this.assertConnected();
880
970
  console.log(TAG2, "signTransaction() \u2014 serializing transaction");
@@ -963,53 +1053,6 @@ var PhantomDeeplinkAdapter = class {
963
1053
  }
964
1054
  };
965
1055
 
966
- // src/utils/device.ts
967
- import { Platform } from "react-native";
968
- function isSolanaSeeker() {
969
- try {
970
- const Device = __require("expo-device");
971
- return Device.brand?.toLowerCase() === "solanamobile";
972
- } catch {
973
- return false;
974
- }
975
- }
976
- async function getDeviceInfo() {
977
- try {
978
- const Device = __require("expo-device");
979
- return {
980
- platform: Platform.OS,
981
- modelName: Device.modelName,
982
- brand: Device.brand,
983
- manufacturer: Device.manufacturer,
984
- osName: Device.osName,
985
- osVersion: Device.osVersion,
986
- deviceType: Device.deviceType,
987
- deviceName: Device.deviceName,
988
- totalMemory: Device.totalMemory,
989
- modelId: Device.modelId,
990
- designName: Device.designName,
991
- productName: Device.productName,
992
- isDevice: Device.isDevice
993
- };
994
- } catch {
995
- return {
996
- platform: Platform.OS,
997
- modelName: null,
998
- brand: null,
999
- manufacturer: null,
1000
- osName: null,
1001
- osVersion: null,
1002
- deviceType: null,
1003
- deviceName: null,
1004
- totalMemory: null,
1005
- modelId: null,
1006
- designName: null,
1007
- productName: null,
1008
- isDevice: null
1009
- };
1010
- }
1011
- }
1012
-
1013
1056
  // src/ui/ConnectWalletScreen.tsx
1014
1057
  import {
1015
1058
  View,
@@ -1197,6 +1240,7 @@ function getOrCreatePhantomAdapter(config) {
1197
1240
  redirectUri: config.redirectUri,
1198
1241
  appUrl: config.appUrl,
1199
1242
  cluster: config.cluster,
1243
+ storage: config.storage,
1200
1244
  onSessionChange: (session) => {
1201
1245
  if (session) {
1202
1246
  console.log(TAG3, "Phantom session changed \u2014 saving to storage, wallet:", session.walletPublicKey);
@@ -1234,9 +1278,8 @@ function ManagedWalletProvider({
1234
1278
  const [connecting, setConnecting] = useState(false);
1235
1279
  const [isReady, setIsReady] = useState(false);
1236
1280
  const [error, setError] = useState(null);
1237
- const seeker = Platform2.OS === "android" && isSolanaSeeker();
1238
- const usePhantom = !seeker && !!redirectUri;
1239
- console.log(TAG3, `Platform: ${Platform2.OS}, seeker: ${seeker}, redirectUri: ${redirectUri ? "provided" : "not set"}, usePhantom: ${usePhantom}`);
1281
+ const usePhantom = Platform.OS === "ios" && !!redirectUri;
1282
+ console.log(TAG3, `Platform: ${Platform.OS}, redirectUri: ${redirectUri ? "provided" : "not set"}, usePhantom: ${usePhantom}`);
1240
1283
  const adapterRef = useRef(null);
1241
1284
  const transactRef = useRef(null);
1242
1285
  if (!adapterRef.current) {
@@ -1286,6 +1329,21 @@ function ManagedWalletProvider({
1286
1329
  }
1287
1330
  return;
1288
1331
  }
1332
+ const coldStartUrl = phantom.consumeColdStartUrl();
1333
+ if (coldStartUrl) {
1334
+ try {
1335
+ console.log(TAG3, "Cold-start URL detected, attempting recovery");
1336
+ await phantom.completeConnectFromColdStart(coldStartUrl);
1337
+ if (!cancelled) {
1338
+ console.log(TAG3, "Cold-start recovery succeeded");
1339
+ setConnected(true);
1340
+ setIsReady(true);
1341
+ }
1342
+ return;
1343
+ } catch (err) {
1344
+ console.log(TAG3, "Cold-start recovery failed:", err instanceof Error ? err.message : err);
1345
+ }
1346
+ }
1289
1347
  try {
1290
1348
  const savedSession = await storage.getItem(STORAGE_KEYS.PHANTOM_SESSION);
1291
1349
  if (savedSession && !cancelled) {
@@ -1377,6 +1435,8 @@ function ManagedWalletProvider({
1377
1435
  });
1378
1436
  await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {
1379
1437
  });
1438
+ await storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
1439
+ });
1380
1440
  setConnected(false);
1381
1441
  console.log(TAG3, "disconnect() \u2014 done");
1382
1442
  }, [adapter, storage, usePhantom]);
@@ -1792,6 +1852,53 @@ import bs583 from "bs58";
1792
1852
  import { createContext as createContext2 } from "react";
1793
1853
  var AuthContext = createContext2(null);
1794
1854
 
1855
+ // src/utils/device.ts
1856
+ import { Platform as Platform2 } from "react-native";
1857
+ function isSolanaSeeker() {
1858
+ try {
1859
+ const Device = __require("expo-device");
1860
+ return Device.brand?.toLowerCase() === "solanamobile";
1861
+ } catch {
1862
+ return false;
1863
+ }
1864
+ }
1865
+ async function getDeviceInfo() {
1866
+ try {
1867
+ const Device = __require("expo-device");
1868
+ return {
1869
+ platform: Platform2.OS,
1870
+ modelName: Device.modelName,
1871
+ brand: Device.brand,
1872
+ manufacturer: Device.manufacturer,
1873
+ osName: Device.osName,
1874
+ osVersion: Device.osVersion,
1875
+ deviceType: Device.deviceType,
1876
+ deviceName: Device.deviceName,
1877
+ totalMemory: Device.totalMemory,
1878
+ modelId: Device.modelId,
1879
+ designName: Device.designName,
1880
+ productName: Device.productName,
1881
+ isDevice: Device.isDevice
1882
+ };
1883
+ } catch {
1884
+ return {
1885
+ platform: Platform2.OS,
1886
+ modelName: null,
1887
+ brand: null,
1888
+ manufacturer: null,
1889
+ osName: null,
1890
+ osVersion: null,
1891
+ deviceType: null,
1892
+ deviceName: null,
1893
+ totalMemory: null,
1894
+ modelId: null,
1895
+ designName: null,
1896
+ productName: null,
1897
+ isDevice: null
1898
+ };
1899
+ }
1900
+ }
1901
+
1795
1902
  // src/hooks/useAuth.ts
1796
1903
  function useAuth() {
1797
1904
  const sharedAuth = useContext2(AuthContext);