@dubsdotapp/expo 0.2.10 → 0.2.12

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
@@ -455,7 +455,8 @@ var DubsClient = class {
455
455
  // src/storage.ts
456
456
  var STORAGE_KEYS = {
457
457
  MWA_AUTH_TOKEN: "dubs_mwa_auth_token",
458
- JWT_TOKEN: "dubs_jwt_token"
458
+ JWT_TOKEN: "dubs_jwt_token",
459
+ PHANTOM_SESSION: "dubs_phantom_session"
459
460
  };
460
461
  function createSecureStoreStorage() {
461
462
  let SecureStore = null;
@@ -493,6 +494,7 @@ import { Connection } from "@solana/web3.js";
493
494
 
494
495
  // src/managed-wallet.tsx
495
496
  import { createContext, useContext, useState, useEffect, useRef, useCallback } from "react";
497
+ import { Platform } from "react-native";
496
498
 
497
499
  // src/wallet/mwa-adapter.ts
498
500
  import { PublicKey } from "@solana/web3.js";
@@ -592,6 +594,388 @@ var MwaWalletAdapter = class {
592
594
  }
593
595
  };
594
596
 
597
+ // src/wallet/phantom-deeplink/phantom-deeplink-adapter.ts
598
+ import { PublicKey as PublicKey2, Transaction as Transaction2 } from "@solana/web3.js";
599
+ import bs582 from "bs58";
600
+
601
+ // src/wallet/phantom-deeplink/crypto.ts
602
+ import nacl from "tweetnacl";
603
+ import bs58 from "bs58";
604
+ function generateKeyPair() {
605
+ const kp = nacl.box.keyPair();
606
+ return { publicKey: kp.publicKey, secretKey: kp.secretKey };
607
+ }
608
+ function deriveSharedSecret(ourSecretKey, theirPublicKey) {
609
+ return nacl.box.before(theirPublicKey, ourSecretKey);
610
+ }
611
+ function encryptPayload(payload, sharedSecret) {
612
+ const nonce = nacl.randomBytes(24);
613
+ const message = new TextEncoder().encode(JSON.stringify(payload));
614
+ const encrypted = nacl.box.after(message, nonce, sharedSecret);
615
+ if (!encrypted) throw new Error("Encryption failed");
616
+ return {
617
+ nonce: bs58.encode(nonce),
618
+ ciphertext: bs58.encode(encrypted)
619
+ };
620
+ }
621
+ function decryptPayload(ciphertextBase58, nonceBase58, sharedSecret) {
622
+ const ciphertext = bs58.decode(ciphertextBase58);
623
+ const nonce = bs58.decode(nonceBase58);
624
+ const decrypted = nacl.box.open.after(ciphertext, nonce, sharedSecret);
625
+ if (!decrypted) throw new Error("Decryption failed \u2014 invalid shared secret or corrupted data");
626
+ return JSON.parse(new TextDecoder().decode(decrypted));
627
+ }
628
+
629
+ // src/wallet/phantom-deeplink/deeplink-handler.ts
630
+ import { Linking } from "react-native";
631
+ var TAG = "[Dubs:DeeplinkHandler]";
632
+ var DeeplinkHandler = class {
633
+ constructor(redirectBase) {
634
+ this.pending = /* @__PURE__ */ new Map();
635
+ this.subscription = null;
636
+ this.redirectBase = redirectBase.replace(/\/+$/, "");
637
+ console.log(TAG, "Created with redirectBase:", this.redirectBase);
638
+ }
639
+ /** Start listening for incoming deeplinks. Call once on adapter init. */
640
+ start() {
641
+ if (this.subscription) {
642
+ console.log(TAG, "Already listening, skipping start()");
643
+ return;
644
+ }
645
+ console.log(TAG, "Starting URL listener");
646
+ this.subscription = Linking.addEventListener("url", ({ url }) => {
647
+ console.log(TAG, "Received URL event:", url);
648
+ this.handleUrl(url);
649
+ });
650
+ Linking.getInitialURL().then((url) => {
651
+ if (url) {
652
+ console.log(TAG, "Cold-start URL found:", url);
653
+ this.handleUrl(url);
654
+ } else {
655
+ console.log(TAG, "No cold-start URL");
656
+ }
657
+ });
658
+ }
659
+ /** Stop listening and reject all pending requests. */
660
+ destroy() {
661
+ console.log(TAG, "Destroying \u2014 pending requests:", this.pending.size);
662
+ this.subscription?.remove();
663
+ this.subscription = null;
664
+ for (const [id, req] of this.pending) {
665
+ clearTimeout(req.timer);
666
+ req.reject(new Error("DeeplinkHandler destroyed"));
667
+ this.pending.delete(id);
668
+ }
669
+ }
670
+ /**
671
+ * Open a Phantom deeplink and wait for the redirect response.
672
+ * @param url The full Phantom deeplink URL to open
673
+ * @param requestId Unique ID embedded in the redirect_link param
674
+ * @param timeoutMs How long to wait before rejecting (default 120s)
675
+ */
676
+ async send(url, requestId, timeoutMs = 12e4) {
677
+ console.log(TAG, `send() requestId=${requestId} timeout=${timeoutMs}ms`);
678
+ console.log(TAG, "Opening URL:", url.substring(0, 120) + (url.length > 120 ? "..." : ""));
679
+ return new Promise((resolve, reject) => {
680
+ const timer = setTimeout(() => {
681
+ console.log(TAG, `Timeout reached for requestId=${requestId}`);
682
+ this.pending.delete(requestId);
683
+ reject(new Error(`Phantom deeplink timed out after ${timeoutMs / 1e3}s`));
684
+ }, timeoutMs);
685
+ this.pending.set(requestId, { resolve, reject, timer });
686
+ Linking.openURL(url).catch((err) => {
687
+ console.log(TAG, `Failed to open URL: ${err.message}`);
688
+ this.pending.delete(requestId);
689
+ clearTimeout(timer);
690
+ reject(new Error(`Failed to open Phantom: ${err.message}`));
691
+ });
692
+ });
693
+ }
694
+ handleUrl(url) {
695
+ if (!url.startsWith(this.redirectBase)) {
696
+ console.log(TAG, "Ignoring URL (not our redirect base):", url.substring(0, 80));
697
+ return;
698
+ }
699
+ console.log(TAG, "Processing redirect URL:", url.substring(0, 120) + (url.length > 120 ? "..." : ""));
700
+ const parsed = new URL(url);
701
+ const params = {};
702
+ parsed.searchParams.forEach((value, key) => {
703
+ params[key] = value;
704
+ });
705
+ const paramKeys = Object.keys(params);
706
+ console.log(TAG, "Parsed params keys:", paramKeys.join(", "));
707
+ if (params.errorCode) {
708
+ const errorMessage = params.errorMessage ? decodeURIComponent(params.errorMessage) : `Phantom error code: ${params.errorCode}`;
709
+ console.log(TAG, `Phantom returned error: code=${params.errorCode} message="${errorMessage}"`);
710
+ const requestId2 = this.extractRequestId(url);
711
+ if (requestId2 && this.pending.has(requestId2)) {
712
+ console.log(TAG, `Routing error to requestId=${requestId2}`);
713
+ const req = this.pending.get(requestId2);
714
+ clearTimeout(req.timer);
715
+ this.pending.delete(requestId2);
716
+ req.reject(new Error(errorMessage));
717
+ return;
718
+ }
719
+ console.log(TAG, `Cannot route error to specific request, rejecting all ${this.pending.size} pending`);
720
+ for (const [id, req] of this.pending) {
721
+ clearTimeout(req.timer);
722
+ req.reject(new Error(errorMessage));
723
+ this.pending.delete(id);
724
+ }
725
+ return;
726
+ }
727
+ const requestId = this.extractRequestId(url);
728
+ console.log(TAG, `Extracted requestId=${requestId}, pending requests: [${[...this.pending.keys()].join(", ")}]`);
729
+ if (requestId && this.pending.has(requestId)) {
730
+ console.log(TAG, `Resolving requestId=${requestId}`);
731
+ const req = this.pending.get(requestId);
732
+ clearTimeout(req.timer);
733
+ this.pending.delete(requestId);
734
+ req.resolve({ params });
735
+ return;
736
+ }
737
+ const first = this.pending.entries().next().value;
738
+ if (first) {
739
+ const [id, req] = first;
740
+ console.log(TAG, `Fallback: resolving first pending requestId=${id}`);
741
+ clearTimeout(req.timer);
742
+ this.pending.delete(id);
743
+ req.resolve({ params });
744
+ } else {
745
+ console.log(TAG, "No pending requests to resolve \u2014 redirect may have arrived late");
746
+ }
747
+ }
748
+ /** Try to extract a request ID from the URL path segment after the redirect base. */
749
+ extractRequestId(url) {
750
+ const afterBase = url.slice(this.redirectBase.length);
751
+ const match = afterBase.match(/^\/?([a-zA-Z0-9_-]+)/);
752
+ return match?.[1] ?? null;
753
+ }
754
+ };
755
+
756
+ // src/wallet/phantom-deeplink/phantom-deeplink-adapter.ts
757
+ var TAG2 = "[Dubs:PhantomAdapter]";
758
+ var requestCounter = 0;
759
+ function nextRequestId() {
760
+ return `req${Date.now()}_${++requestCounter}`;
761
+ }
762
+ var PhantomDeeplinkAdapter = class {
763
+ constructor(config) {
764
+ this._publicKey = null;
765
+ this._connected = false;
766
+ this._dappKeyPair = null;
767
+ this._sharedSecret = null;
768
+ this._sessionToken = null;
769
+ this._phantomPublicKey = null;
770
+ console.log(TAG2, "Creating adapter with config:", {
771
+ redirectUri: config.redirectUri,
772
+ appUrl: config.appUrl,
773
+ cluster: config.cluster,
774
+ timeout: config.timeout
775
+ });
776
+ this.config = config;
777
+ this.timeout = config.timeout ?? 12e4;
778
+ this.handler = new DeeplinkHandler(config.redirectUri);
779
+ this.handler.start();
780
+ console.log(TAG2, "Adapter created and deeplink listener started");
781
+ }
782
+ get publicKey() {
783
+ return this._publicKey;
784
+ }
785
+ get connected() {
786
+ return this._connected;
787
+ }
788
+ /**
789
+ * Restore a previously saved session without opening Phantom.
790
+ * Call this before connect() if you have persisted session state.
791
+ */
792
+ restoreSession(saved) {
793
+ console.log(TAG2, "Restoring session for wallet:", saved.walletPublicKey);
794
+ this._dappKeyPair = {
795
+ publicKey: bs582.decode(saved.dappPublicKey),
796
+ secretKey: bs582.decode(saved.dappSecretKey)
797
+ };
798
+ this._phantomPublicKey = bs582.decode(saved.phantomPublicKey);
799
+ this._sharedSecret = bs582.decode(saved.sharedSecret);
800
+ this._sessionToken = saved.sessionToken;
801
+ this._publicKey = new PublicKey2(saved.walletPublicKey);
802
+ this._connected = true;
803
+ console.log(TAG2, "Session restored successfully \u2014 connected:", this._publicKey.toBase58());
804
+ }
805
+ /** Serialize the current session for persistence. Returns null if not connected. */
806
+ getSession() {
807
+ if (!this._connected || !this._dappKeyPair || !this._phantomPublicKey || !this._sharedSecret || !this._sessionToken || !this._publicKey) {
808
+ return null;
809
+ }
810
+ return {
811
+ dappPublicKey: bs582.encode(this._dappKeyPair.publicKey),
812
+ dappSecretKey: bs582.encode(this._dappKeyPair.secretKey),
813
+ phantomPublicKey: bs582.encode(this._phantomPublicKey),
814
+ sharedSecret: bs582.encode(this._sharedSecret),
815
+ sessionToken: this._sessionToken,
816
+ walletPublicKey: this._publicKey.toBase58()
817
+ };
818
+ }
819
+ async connect() {
820
+ console.log(TAG2, "connect() \u2014 generating x25519 keypair");
821
+ this._dappKeyPair = generateKeyPair();
822
+ const dappPubBase58 = bs582.encode(this._dappKeyPair.publicKey);
823
+ console.log(TAG2, "Dapp public key:", dappPubBase58);
824
+ const requestId = nextRequestId();
825
+ const redirectLink = `${this.config.redirectUri}/${requestId}`;
826
+ console.log(TAG2, `connect() requestId=${requestId} redirectLink=${redirectLink}`);
827
+ const appUrl = this.config.appUrl || this.config.redirectUri;
828
+ console.log(TAG2, "Using app_url:", appUrl);
829
+ const params = new URLSearchParams({
830
+ dapp_encryption_public_key: dappPubBase58,
831
+ cluster: this.config.cluster || "mainnet-beta",
832
+ redirect_link: redirectLink,
833
+ app_url: appUrl
834
+ });
835
+ const url = `https://phantom.app/ul/v1/connect?${params.toString()}`;
836
+ console.log(TAG2, "Opening Phantom connect deeplink...");
837
+ const response = await this.handler.send(url, requestId, this.timeout);
838
+ console.log(TAG2, "Received connect response, param keys:", Object.keys(response.params).join(", "));
839
+ const phantomPubBase58 = response.params.phantom_encryption_public_key;
840
+ if (!phantomPubBase58) {
841
+ console.log(TAG2, "ERROR: No phantom_encryption_public_key in response");
842
+ throw new Error("Phantom did not return an encryption public key");
843
+ }
844
+ console.log(TAG2, "Phantom public key:", phantomPubBase58);
845
+ this._phantomPublicKey = bs582.decode(phantomPubBase58);
846
+ this._sharedSecret = deriveSharedSecret(
847
+ this._dappKeyPair.secretKey,
848
+ this._phantomPublicKey
849
+ );
850
+ console.log(TAG2, "Shared secret derived, decrypting response...");
851
+ const data = decryptPayload(
852
+ response.params.data,
853
+ response.params.nonce,
854
+ this._sharedSecret
855
+ );
856
+ console.log(TAG2, "Decrypted connect data \u2014 public_key:", data.public_key, "session length:", data.session?.length);
857
+ this._sessionToken = data.session;
858
+ this._publicKey = new PublicKey2(data.public_key);
859
+ this._connected = true;
860
+ console.log(TAG2, "Connected! Wallet:", this._publicKey.toBase58());
861
+ this.config.onSessionChange?.(this.getSession());
862
+ }
863
+ disconnect() {
864
+ console.log(TAG2, "disconnect() \u2014 clearing state, was connected:", this._connected, "wallet:", this._publicKey?.toBase58());
865
+ this._publicKey = null;
866
+ this._connected = false;
867
+ this._dappKeyPair = null;
868
+ this._sharedSecret = null;
869
+ this._sessionToken = null;
870
+ this._phantomPublicKey = null;
871
+ this.config.onSessionChange?.(null);
872
+ console.log(TAG2, "Disconnected");
873
+ }
874
+ async signTransaction(transaction) {
875
+ this.assertConnected();
876
+ console.log(TAG2, "signTransaction() \u2014 serializing transaction");
877
+ const serializedTx = bs582.encode(
878
+ transaction.serialize({ requireAllSignatures: false, verifySignatures: false })
879
+ );
880
+ console.log(TAG2, "Transaction serialized, length:", serializedTx.length);
881
+ const { nonce, ciphertext } = encryptPayload(
882
+ { transaction: serializedTx, session: this._sessionToken },
883
+ this._sharedSecret
884
+ );
885
+ const requestId = nextRequestId();
886
+ const redirectLink = `${this.config.redirectUri}/${requestId}`;
887
+ console.log(TAG2, `signTransaction() requestId=${requestId}`);
888
+ const params = new URLSearchParams({
889
+ dapp_encryption_public_key: bs582.encode(this._dappKeyPair.publicKey),
890
+ nonce,
891
+ payload: ciphertext,
892
+ redirect_link: redirectLink
893
+ });
894
+ const url = `https://phantom.app/ul/v1/signTransaction?${params.toString()}`;
895
+ console.log(TAG2, "Opening Phantom signTransaction deeplink...");
896
+ const response = await this.handler.send(url, requestId, this.timeout);
897
+ console.log(TAG2, "Received signTransaction response");
898
+ const data = decryptPayload(
899
+ response.params.data,
900
+ response.params.nonce,
901
+ this._sharedSecret
902
+ );
903
+ console.log(TAG2, "Decrypted signed transaction, length:", data.transaction?.length);
904
+ return Transaction2.from(bs582.decode(data.transaction));
905
+ }
906
+ async signAndSendTransaction(transaction) {
907
+ this.assertConnected();
908
+ console.log(TAG2, "signAndSendTransaction() \u2014 serializing transaction");
909
+ const serializedTx = bs582.encode(
910
+ transaction.serialize({ requireAllSignatures: false, verifySignatures: false })
911
+ );
912
+ console.log(TAG2, "Transaction serialized, length:", serializedTx.length);
913
+ const { nonce, ciphertext } = encryptPayload(
914
+ { transaction: serializedTx, session: this._sessionToken },
915
+ this._sharedSecret
916
+ );
917
+ const requestId = nextRequestId();
918
+ const redirectLink = `${this.config.redirectUri}/${requestId}`;
919
+ console.log(TAG2, `signAndSendTransaction() requestId=${requestId}`);
920
+ const params = new URLSearchParams({
921
+ dapp_encryption_public_key: bs582.encode(this._dappKeyPair.publicKey),
922
+ nonce,
923
+ payload: ciphertext,
924
+ redirect_link: redirectLink
925
+ });
926
+ const url = `https://phantom.app/ul/v1/signAndSendTransaction?${params.toString()}`;
927
+ console.log(TAG2, "Opening Phantom signAndSendTransaction deeplink...");
928
+ const response = await this.handler.send(url, requestId, this.timeout);
929
+ console.log(TAG2, "Received signAndSendTransaction response");
930
+ const data = decryptPayload(
931
+ response.params.data,
932
+ response.params.nonce,
933
+ this._sharedSecret
934
+ );
935
+ console.log(TAG2, "Transaction sent! Signature:", data.signature);
936
+ return data.signature;
937
+ }
938
+ async signMessage(message) {
939
+ this.assertConnected();
940
+ console.log(TAG2, "signMessage() \u2014 message length:", message.length);
941
+ const { nonce, ciphertext } = encryptPayload(
942
+ { message: bs582.encode(message), session: this._sessionToken },
943
+ this._sharedSecret
944
+ );
945
+ const requestId = nextRequestId();
946
+ const redirectLink = `${this.config.redirectUri}/${requestId}`;
947
+ console.log(TAG2, `signMessage() requestId=${requestId}`);
948
+ const params = new URLSearchParams({
949
+ dapp_encryption_public_key: bs582.encode(this._dappKeyPair.publicKey),
950
+ nonce,
951
+ payload: ciphertext,
952
+ redirect_link: redirectLink
953
+ });
954
+ const url = `https://phantom.app/ul/v1/signMessage?${params.toString()}`;
955
+ console.log(TAG2, "Opening Phantom signMessage deeplink...");
956
+ const response = await this.handler.send(url, requestId, this.timeout);
957
+ console.log(TAG2, "Received signMessage response");
958
+ const data = decryptPayload(
959
+ response.params.data,
960
+ response.params.nonce,
961
+ this._sharedSecret
962
+ );
963
+ console.log(TAG2, "Message signed, signature:", data.signature?.substring(0, 20) + "...");
964
+ return bs582.decode(data.signature);
965
+ }
966
+ /** Remove the Linking event listener. Call when the adapter is no longer needed. */
967
+ destroy() {
968
+ console.log(TAG2, "destroy() \u2014 cleaning up");
969
+ this.handler.destroy();
970
+ }
971
+ assertConnected() {
972
+ if (!this._connected || !this._sharedSecret || !this._sessionToken) {
973
+ console.log(TAG2, "assertConnected FAILED \u2014 connected:", this._connected, "hasSecret:", !!this._sharedSecret, "hasSession:", !!this._sessionToken);
974
+ throw new Error("Wallet not connected");
975
+ }
976
+ }
977
+ };
978
+
595
979
  // src/ui/ConnectWalletScreen.tsx
596
980
  import {
597
981
  View,
@@ -770,6 +1154,7 @@ var styles = StyleSheet.create({
770
1154
 
771
1155
  // src/managed-wallet.tsx
772
1156
  import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
1157
+ var TAG3 = "[Dubs:ManagedWallet]";
773
1158
  var DisconnectContext = createContext(null);
774
1159
  function useDisconnect() {
775
1160
  return useContext(DisconnectContext);
@@ -782,85 +1167,168 @@ function ManagedWalletProvider({
782
1167
  accentColor,
783
1168
  appIcon,
784
1169
  tagline,
1170
+ redirectUri,
1171
+ appUrl,
785
1172
  children
786
1173
  }) {
787
1174
  const [connected, setConnected] = useState(false);
788
1175
  const [connecting, setConnecting] = useState(false);
789
1176
  const [isReady, setIsReady] = useState(false);
790
1177
  const [error, setError] = useState(null);
1178
+ const usePhantom = Platform.OS === "ios" && !!redirectUri;
1179
+ console.log(TAG3, `Platform: ${Platform.OS}, redirectUri: ${redirectUri ? "provided" : "not set"}, usePhantom: ${usePhantom}`);
791
1180
  const adapterRef = useRef(null);
792
1181
  const transactRef = useRef(null);
793
1182
  if (!adapterRef.current) {
794
- adapterRef.current = new MwaWalletAdapter({
795
- transact: (...args) => {
796
- if (!transactRef.current) {
797
- throw new Error(
798
- "@dubsdotapp/expo: @solana-mobile/mobile-wallet-adapter-protocol-web3js is required. Install it with: npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js"
799
- );
1183
+ if (usePhantom) {
1184
+ console.log(TAG3, "Creating PhantomDeeplinkAdapter");
1185
+ adapterRef.current = new PhantomDeeplinkAdapter({
1186
+ redirectUri,
1187
+ appUrl,
1188
+ cluster,
1189
+ onSessionChange: (session) => {
1190
+ if (session) {
1191
+ console.log(TAG3, "Phantom session changed \u2014 saving to storage, wallet:", session.walletPublicKey);
1192
+ storage.setItem(STORAGE_KEYS.PHANTOM_SESSION, JSON.stringify(session)).catch((err) => {
1193
+ console.log(TAG3, "Failed to save Phantom session:", err);
1194
+ });
1195
+ } else {
1196
+ console.log(TAG3, "Phantom session cleared \u2014 removing from storage");
1197
+ storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch((err) => {
1198
+ console.log(TAG3, "Failed to delete Phantom session:", err);
1199
+ });
1200
+ }
800
1201
  }
801
- return transactRef.current(...args);
802
- },
803
- appIdentity: { name: appName },
804
- cluster,
805
- onAuthTokenChange: (token) => {
806
- if (token) {
807
- storage.setItem(STORAGE_KEYS.MWA_AUTH_TOKEN, token).catch(() => {
808
- });
809
- } else {
810
- storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {
811
- });
1202
+ });
1203
+ } else {
1204
+ console.log(TAG3, "Creating MwaWalletAdapter");
1205
+ adapterRef.current = new MwaWalletAdapter({
1206
+ transact: (...args) => {
1207
+ if (!transactRef.current) {
1208
+ throw new Error(
1209
+ "@dubsdotapp/expo: @solana-mobile/mobile-wallet-adapter-protocol-web3js is required. Install it with: npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js"
1210
+ );
1211
+ }
1212
+ return transactRef.current(...args);
1213
+ },
1214
+ appIdentity: { name: appName },
1215
+ cluster,
1216
+ onAuthTokenChange: (token) => {
1217
+ if (token) {
1218
+ storage.setItem(STORAGE_KEYS.MWA_AUTH_TOKEN, token).catch(() => {
1219
+ });
1220
+ } else {
1221
+ storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {
1222
+ });
1223
+ }
812
1224
  }
813
- }
814
- });
1225
+ });
1226
+ }
815
1227
  }
816
1228
  const adapter = adapterRef.current;
817
1229
  useEffect(() => {
818
1230
  let cancelled = false;
819
1231
  (async () => {
820
- try {
821
- const mwa = await import("@solana-mobile/mobile-wallet-adapter-protocol-web3js");
822
- if (cancelled) return;
823
- transactRef.current = mwa.transact;
824
- } catch {
825
- }
826
- try {
827
- const savedToken = await storage.getItem(STORAGE_KEYS.MWA_AUTH_TOKEN);
828
- if (savedToken && !cancelled) {
829
- adapter.setAuthToken(savedToken);
830
- await adapter.connect();
831
- if (!cancelled) setConnected(true);
1232
+ if (usePhantom) {
1233
+ console.log(TAG3, "Phantom path \u2014 checking for saved session...");
1234
+ try {
1235
+ const savedJson = await storage.getItem(STORAGE_KEYS.PHANTOM_SESSION);
1236
+ if (savedJson && !cancelled) {
1237
+ console.log(TAG3, "Found saved Phantom session, restoring...");
1238
+ const saved = JSON.parse(savedJson);
1239
+ adapter.restoreSession(saved);
1240
+ if (!cancelled) {
1241
+ console.log(TAG3, "Session restored, marking connected");
1242
+ setConnected(true);
1243
+ }
1244
+ } else {
1245
+ console.log(TAG3, "No saved Phantom session found");
1246
+ }
1247
+ } catch (err) {
1248
+ console.log(TAG3, "Failed to restore Phantom session:", err instanceof Error ? err.message : err);
1249
+ } finally {
1250
+ if (!cancelled) {
1251
+ console.log(TAG3, "Phantom init complete, marking ready");
1252
+ setIsReady(true);
1253
+ }
1254
+ }
1255
+ } else {
1256
+ console.log(TAG3, "MWA path \u2014 dynamic-importing transact...");
1257
+ try {
1258
+ const mwa = await import("@solana-mobile/mobile-wallet-adapter-protocol-web3js");
1259
+ if (cancelled) return;
1260
+ transactRef.current = mwa.transact;
1261
+ console.log(TAG3, "MWA transact loaded");
1262
+ } catch {
1263
+ console.log(TAG3, "MWA not installed \u2014 transact will throw on use");
1264
+ }
1265
+ try {
1266
+ const savedToken = await storage.getItem(STORAGE_KEYS.MWA_AUTH_TOKEN);
1267
+ if (savedToken && !cancelled) {
1268
+ console.log(TAG3, "Found saved MWA auth token, reconnecting...");
1269
+ adapter.setAuthToken(savedToken);
1270
+ await adapter.connect();
1271
+ if (!cancelled) {
1272
+ console.log(TAG3, "MWA reconnected from saved token");
1273
+ setConnected(true);
1274
+ }
1275
+ } else {
1276
+ console.log(TAG3, "No saved MWA auth token");
1277
+ }
1278
+ } catch (err) {
1279
+ console.log(TAG3, "MWA silent reconnect failed:", err instanceof Error ? err.message : err);
1280
+ } finally {
1281
+ if (!cancelled) {
1282
+ console.log(TAG3, "MWA init complete, marking ready");
1283
+ setIsReady(true);
1284
+ }
832
1285
  }
833
- } catch {
834
- } finally {
835
- if (!cancelled) setIsReady(true);
836
1286
  }
837
1287
  })();
838
1288
  return () => {
839
1289
  cancelled = true;
840
1290
  };
841
- }, [adapter, storage]);
1291
+ }, [adapter, storage, usePhantom]);
842
1292
  const handleConnect = useCallback(async () => {
1293
+ console.log(TAG3, "handleConnect() \u2014 user tapped connect");
843
1294
  setConnecting(true);
844
1295
  setError(null);
845
1296
  try {
846
1297
  await adapter.connect();
1298
+ console.log(TAG3, "handleConnect() \u2014 success, wallet:", adapter.publicKey?.toBase58());
847
1299
  setConnected(true);
848
1300
  } catch (err) {
849
1301
  const message = err instanceof Error ? err.message : "Connection failed";
1302
+ console.log(TAG3, "handleConnect() \u2014 failed:", message);
850
1303
  setError(message);
851
1304
  } finally {
852
1305
  setConnecting(false);
853
1306
  }
854
1307
  }, [adapter]);
855
1308
  const disconnect = useCallback(async () => {
856
- adapter.disconnect();
1309
+ console.log(TAG3, "disconnect() \u2014 clearing all state");
1310
+ adapter.disconnect?.();
857
1311
  await storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {
858
1312
  });
1313
+ await storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch(() => {
1314
+ });
859
1315
  await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {
860
1316
  });
861
1317
  setConnected(false);
1318
+ console.log(TAG3, "disconnect() \u2014 done");
862
1319
  }, [adapter, storage]);
863
- if (!isReady) return null;
1320
+ useEffect(() => {
1321
+ return () => {
1322
+ if (usePhantom && adapter && "destroy" in adapter) {
1323
+ console.log(TAG3, "Unmounting \u2014 destroying Phantom adapter");
1324
+ adapter.destroy();
1325
+ }
1326
+ };
1327
+ }, [adapter, usePhantom]);
1328
+ if (!isReady) {
1329
+ console.log(TAG3, "Not ready yet \u2014 rendering null");
1330
+ return null;
1331
+ }
864
1332
  if (!connected) {
865
1333
  if (renderConnectScreen === false) {
866
1334
  return null;
@@ -879,6 +1347,7 @@ function ManagedWalletProvider({
879
1347
  }
880
1348
  return /* @__PURE__ */ jsx2(ConnectWalletScreen, { ...connectProps });
881
1349
  }
1350
+ console.log(TAG3, "Rendering children \u2014 connected with wallet:", adapter.publicKey?.toBase58());
882
1351
  return /* @__PURE__ */ jsx2(DisconnectContext.Provider, { value: disconnect, children: children(adapter) });
883
1352
  }
884
1353
 
@@ -893,7 +1362,7 @@ import {
893
1362
  StyleSheet as StyleSheet2,
894
1363
  Keyboard,
895
1364
  KeyboardAvoidingView,
896
- Platform,
1365
+ Platform as Platform2,
897
1366
  Image as Image2,
898
1367
  Animated,
899
1368
  ScrollView
@@ -1007,7 +1476,7 @@ function useNetworkGames(params) {
1007
1476
  import { useState as useState6, useCallback as useCallback6 } from "react";
1008
1477
 
1009
1478
  // src/utils/transaction.ts
1010
- import { Transaction as Transaction2 } from "@solana/web3.js";
1479
+ import { Transaction as Transaction3 } from "@solana/web3.js";
1011
1480
  async function signAndSendBase64Transaction(base64Tx, wallet) {
1012
1481
  if (!wallet.publicKey) throw new Error("Wallet not connected");
1013
1482
  const binaryStr = atob(base64Tx);
@@ -1015,7 +1484,7 @@ async function signAndSendBase64Transaction(base64Tx, wallet) {
1015
1484
  for (let i = 0; i < binaryStr.length; i++) {
1016
1485
  bytes[i] = binaryStr.charCodeAt(i);
1017
1486
  }
1018
- const transaction = Transaction2.from(bytes);
1487
+ const transaction = Transaction3.from(bytes);
1019
1488
  if (wallet.signAndSendTransaction) {
1020
1489
  return wallet.signAndSendTransaction(transaction);
1021
1490
  }
@@ -1256,7 +1725,7 @@ function useCreateCustomGame() {
1256
1725
 
1257
1726
  // src/hooks/useAuth.ts
1258
1727
  import { useState as useState10, useCallback as useCallback10, useRef as useRef2, useContext as useContext2 } from "react";
1259
- import bs58 from "bs58";
1728
+ import bs583 from "bs58";
1260
1729
 
1261
1730
  // src/auth-context.ts
1262
1731
  import { createContext as createContext2 } from "react";
@@ -1294,7 +1763,7 @@ function useAuth() {
1294
1763
  setStatus("signing");
1295
1764
  const messageBytes = new TextEncoder().encode(message);
1296
1765
  const signatureBytes = await wallet.signMessage(messageBytes);
1297
- const signature = bs58.encode(signatureBytes);
1766
+ const signature = bs583.encode(signatureBytes);
1298
1767
  setStatus("verifying");
1299
1768
  const result = await client.authenticate({ walletAddress, signature, nonce });
1300
1769
  if (result.needsRegistration) {
@@ -1795,7 +2264,7 @@ function DefaultRegistrationScreen({
1795
2264
  KeyboardAvoidingView,
1796
2265
  {
1797
2266
  style: [s.container, { backgroundColor: t.background }],
1798
- behavior: Platform.OS === "ios" ? "padding" : void 0,
2267
+ behavior: Platform2.OS === "ios" ? "padding" : void 0,
1799
2268
  children: /* @__PURE__ */ jsx3(
1800
2269
  ScrollView,
1801
2270
  {
@@ -1896,7 +2365,9 @@ function DubsProvider({
1896
2365
  renderLoading,
1897
2366
  renderError,
1898
2367
  renderRegistration,
1899
- managed = true
2368
+ managed = true,
2369
+ redirectUri,
2370
+ appUrl
1900
2371
  }) {
1901
2372
  const config = NETWORK_CONFIG[network];
1902
2373
  const baseUrl = baseUrlOverride || config.baseUrl;
@@ -1946,6 +2417,8 @@ function DubsProvider({
1946
2417
  accentColor: uiConfig.accentColor,
1947
2418
  appIcon: uiConfig.appIcon,
1948
2419
  tagline: uiConfig.tagline,
2420
+ redirectUri,
2421
+ appUrl,
1949
2422
  children: (adapter) => /* @__PURE__ */ jsx4(
1950
2423
  ManagedInner,
1951
2424
  {
@@ -2765,7 +3238,7 @@ import {
2765
3238
  Animated as Animated2,
2766
3239
  StyleSheet as StyleSheet10,
2767
3240
  KeyboardAvoidingView as KeyboardAvoidingView2,
2768
- Platform as Platform2
3241
+ Platform as Platform3
2769
3242
  } from "react-native";
2770
3243
  import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2771
3244
  var STATUS_LABELS2 = {
@@ -2877,7 +3350,7 @@ function CreateCustomGameSheet({
2877
3350
  KeyboardAvoidingView2,
2878
3351
  {
2879
3352
  style: styles9.keyboardView,
2880
- behavior: Platform2.OS === "ios" ? "padding" : void 0,
3353
+ behavior: Platform3.OS === "ios" ? "padding" : void 0,
2881
3354
  children: /* @__PURE__ */ jsx12(View10, { style: styles9.sheetPositioner, children: /* @__PURE__ */ jsxs10(View10, { style: [styles9.sheet, { backgroundColor: t.background }], children: [
2882
3355
  /* @__PURE__ */ jsx12(View10, { style: styles9.handleRow, children: /* @__PURE__ */ jsx12(View10, { style: [styles9.handle, { backgroundColor: t.textMuted }] }) }),
2883
3356
  /* @__PURE__ */ jsxs10(View10, { style: styles9.header, children: [
@@ -3129,6 +3602,7 @@ export {
3129
3602
  LivePoolsCard,
3130
3603
  MwaWalletAdapter,
3131
3604
  NETWORK_CONFIG,
3605
+ PhantomDeeplinkAdapter,
3132
3606
  PickWinnerCard,
3133
3607
  PlayersCard,
3134
3608
  SOLANA_PROGRAM_ERRORS,