@dubsdotapp/expo 0.2.22 → 0.2.23

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.d.mts CHANGED
@@ -430,6 +430,7 @@ declare const STORAGE_KEYS: {
430
430
  readonly MWA_AUTH_TOKEN: "dubs_mwa_auth_token";
431
431
  readonly JWT_TOKEN: "dubs_jwt_token";
432
432
  readonly PHANTOM_SESSION: "dubs_phantom_session";
433
+ readonly PHANTOM_CONNECT_IN_FLIGHT: "dubs_phantom_connect_in_flight";
433
434
  };
434
435
  /**
435
436
  * Creates a TokenStorage backed by expo-secure-store.
@@ -622,6 +623,8 @@ interface PhantomDeeplinkAdapterConfig {
622
623
  timeout?: number;
623
624
  /** Called when the Phantom session changes (save/clear for persistence) */
624
625
  onSessionChange?: (session: PhantomSession | null) => void;
626
+ /** Storage for persisting in-flight connect state (cold-start recovery on Android) */
627
+ storage?: TokenStorage;
625
628
  }
626
629
  /**
627
630
  * Phantom wallet adapter using deeplinks (works on iOS and Android).
@@ -649,6 +652,17 @@ declare class PhantomDeeplinkAdapter implements WalletAdapter {
649
652
  getSession(): PhantomSession | null;
650
653
  connect(): Promise<void>;
651
654
  disconnect(): void;
655
+ /**
656
+ * Return and clear the stashed cold-start URL from the deeplink handler.
657
+ * Used by ManagedWalletProvider to detect an in-flight connect on cold start.
658
+ */
659
+ consumeColdStartUrl(): string | null;
660
+ /**
661
+ * Complete a connect flow that was interrupted by the OS killing the app (Android).
662
+ * Loads the saved in-flight keypair from storage, decrypts the Phantom response
663
+ * from the cold-start URL, and restores the session.
664
+ */
665
+ completeConnectFromColdStart(url: string): Promise<void>;
652
666
  signTransaction(transaction: Transaction): Promise<Transaction>;
653
667
  signMessage(message: Uint8Array): Promise<Uint8Array>;
654
668
  /** Remove the Linking event listener. Call when the adapter is no longer needed. */
package/dist/index.d.ts CHANGED
@@ -430,6 +430,7 @@ declare const STORAGE_KEYS: {
430
430
  readonly MWA_AUTH_TOKEN: "dubs_mwa_auth_token";
431
431
  readonly JWT_TOKEN: "dubs_jwt_token";
432
432
  readonly PHANTOM_SESSION: "dubs_phantom_session";
433
+ readonly PHANTOM_CONNECT_IN_FLIGHT: "dubs_phantom_connect_in_flight";
433
434
  };
434
435
  /**
435
436
  * Creates a TokenStorage backed by expo-secure-store.
@@ -622,6 +623,8 @@ interface PhantomDeeplinkAdapterConfig {
622
623
  timeout?: number;
623
624
  /** Called when the Phantom session changes (save/clear for persistence) */
624
625
  onSessionChange?: (session: PhantomSession | null) => void;
626
+ /** Storage for persisting in-flight connect state (cold-start recovery on Android) */
627
+ storage?: TokenStorage;
625
628
  }
626
629
  /**
627
630
  * Phantom wallet adapter using deeplinks (works on iOS and Android).
@@ -649,6 +652,17 @@ declare class PhantomDeeplinkAdapter implements WalletAdapter {
649
652
  getSession(): PhantomSession | null;
650
653
  connect(): Promise<void>;
651
654
  disconnect(): void;
655
+ /**
656
+ * Return and clear the stashed cold-start URL from the deeplink handler.
657
+ * Used by ManagedWalletProvider to detect an in-flight connect on cold start.
658
+ */
659
+ consumeColdStartUrl(): string | null;
660
+ /**
661
+ * Complete a connect flow that was interrupted by the OS killing the app (Android).
662
+ * Loads the saved in-flight keypair from storage, decrypts the Phantom response
663
+ * from the cold-start URL, and restores the session.
664
+ */
665
+ completeConnectFromColdStart(url: string): Promise<void>;
652
666
  signTransaction(transaction: Transaction): Promise<Transaction>;
653
667
  signMessage(message: Uint8Array): Promise<Uint8Array>;
654
668
  /** Remove the Linking event listener. Call when the adapter is no longer needed. */
package/dist/index.js CHANGED
@@ -522,7 +522,8 @@ var DubsClient = class {
522
522
  var STORAGE_KEYS = {
523
523
  MWA_AUTH_TOKEN: "dubs_mwa_auth_token",
524
524
  JWT_TOKEN: "dubs_jwt_token",
525
- PHANTOM_SESSION: "dubs_phantom_session"
525
+ PHANTOM_SESSION: "dubs_phantom_session",
526
+ PHANTOM_CONNECT_IN_FLIGHT: "dubs_phantom_connect_in_flight"
526
527
  };
527
528
  function createSecureStoreStorage() {
528
529
  let SecureStore = null;
@@ -699,6 +700,7 @@ var DeeplinkHandler = class {
699
700
  constructor(redirectBase) {
700
701
  this.pending = /* @__PURE__ */ new Map();
701
702
  this.subscription = null;
703
+ this._coldStartUrl = null;
702
704
  this.redirectBase = redirectBase.replace(/\/+$/, "");
703
705
  console.log(TAG, "Created with redirectBase:", this.redirectBase);
704
706
  }
@@ -808,9 +810,16 @@ var DeeplinkHandler = class {
808
810
  this.pending.delete(id);
809
811
  req.resolve({ params });
810
812
  } else {
811
- console.log(TAG, "No pending requests to resolve \u2014 redirect may have arrived late");
813
+ console.log(TAG, "No pending requests to resolve \u2014 stashing as cold-start URL");
814
+ this._coldStartUrl = url;
812
815
  }
813
816
  }
817
+ /** Return and clear the stashed cold-start URL (if any). */
818
+ getColdStartUrl() {
819
+ const url = this._coldStartUrl;
820
+ this._coldStartUrl = null;
821
+ return url;
822
+ }
814
823
  /** Extract the request ID from the dubs_rid query parameter. */
815
824
  extractRequestId(url) {
816
825
  try {
@@ -902,6 +911,18 @@ var PhantomDeeplinkAdapter = class {
902
911
  redirect_link: redirectLink,
903
912
  app_url: appUrl
904
913
  });
914
+ if (this.config.storage) {
915
+ console.log(TAG2, "Saving in-flight connect state to storage");
916
+ await this.config.storage.setItem(
917
+ STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT,
918
+ JSON.stringify({
919
+ dappPublicKey: dappPubBase58,
920
+ dappSecretKey: import_bs582.default.encode(this._dappKeyPair.secretKey),
921
+ requestId,
922
+ createdAt: Date.now()
923
+ })
924
+ );
925
+ }
905
926
  const url = `https://phantom.app/ul/v1/connect?${params.toString()}`;
906
927
  console.log(TAG2, "Opening Phantom connect deeplink...");
907
928
  const response = await this.handler.send(url, requestId, this.timeout);
@@ -929,6 +950,8 @@ var PhantomDeeplinkAdapter = class {
929
950
  this._connected = true;
930
951
  console.log(TAG2, "Connected! Wallet:", this._publicKey.toBase58());
931
952
  this.config.onSessionChange?.(this.getSession());
953
+ this.config.storage?.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
954
+ });
932
955
  }
933
956
  disconnect() {
934
957
  console.log(TAG2, "disconnect() \u2014 clearing state, was connected:", this._connected, "wallet:", this._publicKey?.toBase58());
@@ -941,6 +964,73 @@ var PhantomDeeplinkAdapter = class {
941
964
  this.config.onSessionChange?.(null);
942
965
  console.log(TAG2, "Disconnected");
943
966
  }
967
+ /**
968
+ * Return and clear the stashed cold-start URL from the deeplink handler.
969
+ * Used by ManagedWalletProvider to detect an in-flight connect on cold start.
970
+ */
971
+ consumeColdStartUrl() {
972
+ return this.handler.getColdStartUrl();
973
+ }
974
+ /**
975
+ * Complete a connect flow that was interrupted by the OS killing the app (Android).
976
+ * Loads the saved in-flight keypair from storage, decrypts the Phantom response
977
+ * from the cold-start URL, and restores the session.
978
+ */
979
+ async completeConnectFromColdStart(url) {
980
+ if (!this.config.storage) {
981
+ throw new Error("Cannot recover cold-start connect: no storage configured");
982
+ }
983
+ console.log(TAG2, "completeConnectFromColdStart() \u2014 attempting recovery");
984
+ const raw = await this.config.storage.getItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT);
985
+ if (!raw) {
986
+ throw new Error("No in-flight connect state found in storage");
987
+ }
988
+ const inFlight = JSON.parse(raw);
989
+ const age = Date.now() - inFlight.createdAt;
990
+ if (age > 12e4) {
991
+ console.log(TAG2, `In-flight state expired (${Math.round(age / 1e3)}s old), clearing`);
992
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
993
+ });
994
+ throw new Error("In-flight connect state expired");
995
+ }
996
+ console.log(TAG2, `In-flight state age: ${Math.round(age / 1e3)}s, requestId: ${inFlight.requestId}`);
997
+ this._dappKeyPair = {
998
+ publicKey: import_bs582.default.decode(inFlight.dappPublicKey),
999
+ secretKey: import_bs582.default.decode(inFlight.dappSecretKey)
1000
+ };
1001
+ const parsed = new URL(url);
1002
+ const params = {};
1003
+ parsed.searchParams.forEach((value, key) => {
1004
+ params[key] = value;
1005
+ });
1006
+ if (params.errorCode) {
1007
+ const errorMessage = params.errorMessage ? decodeURIComponent(params.errorMessage) : `Phantom error code: ${params.errorCode}`;
1008
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
1009
+ });
1010
+ throw new Error(errorMessage);
1011
+ }
1012
+ const phantomPubBase58 = params.phantom_encryption_public_key;
1013
+ if (!phantomPubBase58) {
1014
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
1015
+ });
1016
+ throw new Error("Phantom did not return an encryption public key");
1017
+ }
1018
+ console.log(TAG2, "Cold-start: Phantom public key:", phantomPubBase58);
1019
+ this._phantomPublicKey = import_bs582.default.decode(phantomPubBase58);
1020
+ this._sharedSecret = deriveSharedSecret(
1021
+ this._dappKeyPair.secretKey,
1022
+ this._phantomPublicKey
1023
+ );
1024
+ const data = decryptPayload(params.data, params.nonce, this._sharedSecret);
1025
+ console.log(TAG2, "Cold-start: Decrypted connect data \u2014 public_key:", data.public_key);
1026
+ this._sessionToken = data.session;
1027
+ this._publicKey = new import_web32.PublicKey(data.public_key);
1028
+ this._connected = true;
1029
+ console.log(TAG2, "Cold-start recovery complete! Wallet:", this._publicKey.toBase58());
1030
+ this.config.onSessionChange?.(this.getSession());
1031
+ await this.config.storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
1032
+ });
1033
+ }
944
1034
  async signTransaction(transaction) {
945
1035
  this.assertConnected();
946
1036
  console.log(TAG2, "signTransaction() \u2014 serializing transaction");
@@ -1256,6 +1346,7 @@ function getOrCreatePhantomAdapter(config) {
1256
1346
  redirectUri: config.redirectUri,
1257
1347
  appUrl: config.appUrl,
1258
1348
  cluster: config.cluster,
1349
+ storage: config.storage,
1259
1350
  onSessionChange: (session) => {
1260
1351
  if (session) {
1261
1352
  console.log(TAG3, "Phantom session changed \u2014 saving to storage, wallet:", session.walletPublicKey);
@@ -1345,6 +1436,21 @@ function ManagedWalletProvider({
1345
1436
  }
1346
1437
  return;
1347
1438
  }
1439
+ const coldStartUrl = phantom.consumeColdStartUrl();
1440
+ if (coldStartUrl) {
1441
+ try {
1442
+ console.log(TAG3, "Cold-start URL detected, attempting recovery");
1443
+ await phantom.completeConnectFromColdStart(coldStartUrl);
1444
+ if (!cancelled) {
1445
+ console.log(TAG3, "Cold-start recovery succeeded");
1446
+ setConnected(true);
1447
+ setIsReady(true);
1448
+ }
1449
+ return;
1450
+ } catch (err) {
1451
+ console.log(TAG3, "Cold-start recovery failed:", err instanceof Error ? err.message : err);
1452
+ }
1453
+ }
1348
1454
  try {
1349
1455
  const savedSession = await storage.getItem(STORAGE_KEYS.PHANTOM_SESSION);
1350
1456
  if (savedSession && !cancelled) {
@@ -1436,6 +1542,8 @@ function ManagedWalletProvider({
1436
1542
  });
1437
1543
  await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {
1438
1544
  });
1545
+ await storage.deleteItem(STORAGE_KEYS.PHANTOM_CONNECT_IN_FLIGHT).catch(() => {
1546
+ });
1439
1547
  setConnected(false);
1440
1548
  console.log(TAG3, "disconnect() \u2014 done");
1441
1549
  }, [adapter, storage, usePhantom]);