@abstraxn/signer-react 1.0.15 → 2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.16] - 2026-02-23
9
+
10
+ ### Fixed
11
+
12
+ - **External wallet disconnect from extension** – When the user disconnects from an external wallet (e.g. MetaMask) via the extension ("Disconnect this site"), the app now updates `isConnected` to false immediately without requiring a page refresh. Previously the sync effect returned early when the user had explicitly connected, so state was never reset until refresh. The fix: (1) allow the sync effect to run the existing reset when wagmi reports disconnected; (2) use wagmi’s `useConnectionEffect(onDisconnect)` to clear external wallet state as soon as wagmi reports disconnect; (3) subscribe to the connector provider’s `accountsChanged` and `disconnect` events so state clears even if wagmi is slow or stale.
13
+
8
14
  ## [1.0.15] - 2026-02-20
9
15
 
10
16
  ### Added
package/README.md CHANGED
@@ -98,11 +98,107 @@ function HookConnectButton() {
98
98
 
99
99
  - `useAbstraxnWallet()` - Main wallet hook
100
100
  - `useIsConnected()` - Check connection status
101
- - `useAddress()` - Get wallet address
102
- - `useWhoami()` - Get user information
103
- - `useExternalWalletInfo()` - External wallet information
104
- - `usePrepareTransaction()` - Prepare and sign transactions
105
- - `useExportWallet()` - Export wallet
101
+ - `useAddress()` - Get wallet address (EVM)
102
+ - `useWhoami()` - Get user information (includes `solanaAddress` when available)
103
+ - `useExternalWalletInfo()` - External wallet information (EVM)
104
+ - `useExportWallet()` - Export wallet (EVM or Solana, based on current chain)
105
+ - `usePublicClient()` / `usePrepareRawTxn()` / `useSignTxn()` / `useSignAndSendTxn()` / `useWaitForTxnReceipt()` - EVM transaction flow (prepare → sign → send → confirm)
106
+ - `useSolanaConnection()` / `useSolanaPublicKey()` / `usePrepareSolanaTxn()` / `useSignSolanaTxn()` / `useSignAndSendSolanaTxn()` / `useWaitForSolanaConfirmation()` - Solana transaction flow (prepare → sign → send → confirm)
107
+
108
+ ### Example: EVM → Solana flow after social/email login
109
+
110
+ ```tsx
111
+ import {
112
+ useAbstraxnWallet,
113
+ usePublicClient,
114
+ usePrepareRawTxn,
115
+ useEstimateGas,
116
+ useGetGasPrice,
117
+ useSignAndSendTxn,
118
+ useWaitForTxnReceipt,
119
+ useSolanaConnection,
120
+ useSolanaPublicKey,
121
+ usePrepareSolanaTxn,
122
+ useSignAndSendSolanaTxn,
123
+ useWaitForSolanaConfirmation,
124
+ } from '@abstraxn/signer-react';
125
+ import { polygonAmoy } from 'viem/chains';
126
+
127
+ function CrossChainExample() {
128
+ const { isConnected } = useAbstraxnWallet();
129
+
130
+ // EVM clients & hooks
131
+ const { publicClient } = usePublicClient(
132
+ polygonAmoy,
133
+ 'https://rpc-amoy.polygon.technology'
134
+ );
135
+ const { prepareRawTxn } = usePrepareRawTxn(publicClient);
136
+ const { estimateGas } = useEstimateGas(publicClient);
137
+ const { getGasPrice } = useGetGasPrice(publicClient);
138
+ const { signAndSendTxn } = useSignAndSendTxn(publicClient);
139
+ const { waitForTxnReceipt } = useWaitForTxnReceipt(publicClient);
140
+
141
+ // Solana clients & hooks
142
+ const { connection } = useSolanaConnection('https://api.devnet.solana.com');
143
+ const { solanaAddress } = useSolanaPublicKey();
144
+ const { prepareSolanaTransfer } = usePrepareSolanaTxn(connection);
145
+ const { signAndSendSolanaTxn } = useSignAndSendSolanaTxn(connection);
146
+ const { waitForSolanaConfirmation } = useWaitForSolanaConfirmation(connection);
147
+
148
+ const handleCrossChain = async () => {
149
+ if (!isConnected) throw new Error('Please login first');
150
+
151
+ // 1) EVM tx (e.g. on Polygon Amoy)
152
+ const from = '0x...'; // current EVM address
153
+ const rawTx = await prepareRawTxn({
154
+ from,
155
+ to: '0x...',
156
+ value: '0.001',
157
+ });
158
+ const { gasLimit } = await estimateGas({
159
+ account: from,
160
+ to: rawTx.to,
161
+ data: rawTx.data,
162
+ value: rawTx.value,
163
+ });
164
+ const fees = await getGasPrice();
165
+
166
+ const evmResult = await signAndSendTxn({
167
+ from,
168
+ ...rawTx,
169
+ gas: {
170
+ gasLimit,
171
+ maxFeePerGas: fees.maxFeePerGas!,
172
+ maxPriorityFeePerGas: fees.maxPriorityFeePerGas!,
173
+ },
174
+ });
175
+
176
+ await waitForTxnReceipt({ hash: evmResult.hash, confirmations: 1 });
177
+
178
+ // 2) After EVM confirmation, send SOL
179
+ if (!solanaAddress) throw new Error('Solana wallet not provisioned');
180
+
181
+ const { transaction } = await prepareSolanaTransfer({
182
+ fromPubkey: solanaAddress,
183
+ toPubkey: 'TargetSolanaAddressHere',
184
+ amountInSol: 0.01,
185
+ });
186
+
187
+ const solanaResult = await signAndSendSolanaTxn({
188
+ transaction,
189
+ fromPubkey: solanaAddress,
190
+ });
191
+
192
+ await waitForSolanaConfirmation({ signature: solanaResult.signature });
193
+ };
194
+
195
+ return (
196
+ <button onClick={handleCrossChain}>
197
+ Run EVM → Solana Flow
198
+ </button>
199
+ );
200
+ }
201
+ ```
106
202
 
107
203
  ## 🔗 Related Packages
108
204
 
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * AbstraxnProviderInner - Core provider logic
4
4
  * This component contains all the wallet state management and operations
@@ -6,15 +6,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
6
6
  import { useEffect, useState, useRef, useCallback, useMemo, } from "react";
7
7
  import { AbstraxnWallet, AuthenticationError, } from "@abstraxn/signer-core";
8
8
  import { OnboardingUIWeb } from "../OnboardingUI";
9
+ import { useConnectionEffect, } from "wagmi";
9
10
  import { parseEther, createPublicClient, http } from "viem";
10
11
  import { ExternalWalletButtons } from "../../ExternalWalletButtons";
11
12
  import { EVM_CHAINS, SOLANA_CHAINS, getChainById, toCoreChain, } from "../../chains";
12
13
  import { AbstraxnContext } from "./context";
14
+ /** Syncs wagmi disconnect to external wallet state; only rendered when wagmi is available (under WagmiProvider). */
15
+ function WagmiConnectionEffectSync({ onDisconnect, }) {
16
+ useConnectionEffect({ onDisconnect });
17
+ return null;
18
+ }
13
19
  export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
14
20
  const { isInitialized, setIsInitialized, isConnected, setIsConnected, address, setAddress, user, setUser, whoami, setWhoami, chainId, setChainId, error, setError, loading, setLoading, resending, setResending, walletBalance, setWalletBalance, onboardingRef, otpIdRef, walletRef, googleCallbackHandledRef, twitterCallbackHandledRef, discordCallbackHandledRef, } = base;
15
21
  const externalWalletsEnabled = config.externalWallets?.enabled ?? false;
16
22
  // Keep a ref to avoid re-creating callbacks when toggling config (prevents flicker)
17
23
  const externalWalletsEnabledRef = useRef(externalWalletsEnabled);
24
+ const isExternalWalletConnectedRef = useRef(false);
18
25
  useEffect(() => {
19
26
  externalWalletsEnabledRef.current = externalWalletsEnabled;
20
27
  }, [externalWalletsEnabled]);
@@ -37,6 +44,9 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
37
44
  const [emailForOtp, setEmailForOtpState] = useState("");
38
45
  const explicitConnectionRef = useRef(false);
39
46
  const autoDisconnectHandledRef = useRef(false);
47
+ useEffect(() => {
48
+ isExternalWalletConnectedRef.current = isExternalWalletConnected;
49
+ }, [isExternalWalletConnected]);
40
50
  // Track when we last connected to prevent premature reset
41
51
  const lastConnectionTimeRef = useRef(0);
42
52
  // Refs to track previous values and prevent unnecessary updates
@@ -1715,6 +1725,28 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
1715
1725
  setLoading(false);
1716
1726
  }
1717
1727
  }, []);
1728
+ // Sign Solana transaction via API (returns signed transaction)
1729
+ const signSolanaTransactionViaAPI = useCallback(async (unsignedTransaction, fromAddress) => {
1730
+ if (!walletRef.current)
1731
+ throw new Error("Wallet not initialized");
1732
+ setLoading(true);
1733
+ setError(null);
1734
+ try {
1735
+ const walletAny = walletRef.current;
1736
+ if (typeof walletAny.signSolanaTransactionViaAPI !== "function") {
1737
+ throw new Error("Solana transaction signing is not supported by this wallet");
1738
+ }
1739
+ return await walletAny.signSolanaTransactionViaAPI(unsignedTransaction, fromAddress);
1740
+ }
1741
+ catch (err) {
1742
+ const error = err instanceof Error ? err : new Error("Failed to sign Solana transaction");
1743
+ setError(error);
1744
+ throw error;
1745
+ }
1746
+ finally {
1747
+ setLoading(false);
1748
+ }
1749
+ }, []);
1718
1750
  // Sign typed data / raw payload via API (same flow as signTransactionViaAPI, payload: from, unsignedTransaction, encoding, hashFunction)
1719
1751
  const signTypedTxViaAPI = useCallback(async (fromAddress, unsignedTransaction, encoding, hashFunction) => {
1720
1752
  if (!walletRef.current)
@@ -1984,12 +2016,6 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
1984
2016
  isUpdatingRef.current = false;
1985
2017
  return;
1986
2018
  }
1987
- // If we're not connected but explicitConnectionRef is true, it means we're trying to reconnect
1988
- // Don't reset state in this case - wait for the connection to complete
1989
- if (!wagmiAccount.isConnected && explicitConnectionRef.current) {
1990
- // Connection is in progress, don't interfere
1991
- return;
1992
- }
1993
2019
  }
1994
2020
  // Check if external wallet is connected
1995
2021
  if (wagmiAccount.isConnected && wagmiAccount.address) {
@@ -2483,6 +2509,95 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
2483
2509
  setLoading(false);
2484
2510
  }
2485
2511
  }, [externalWalletsEnabled, wagmiDisconnect, wagmiAccount?.connector]);
2512
+ // When wagmi reports disconnect (e.g. user disconnected in extension), clear external wallet state immediately
2513
+ const handleWagmiDisconnect = useCallback(() => {
2514
+ if (!externalWalletsEnabledRef.current)
2515
+ return;
2516
+ setIsExternalWalletConnected(false);
2517
+ setExternalWalletAddress(null);
2518
+ setExternalWalletChainId(null);
2519
+ setConnectionType(null);
2520
+ try {
2521
+ localStorage.removeItem("abstraxn_connection_type");
2522
+ }
2523
+ catch (e) {
2524
+ // Ignore
2525
+ }
2526
+ explicitConnectionRef.current = false;
2527
+ lastConnectionTimeRef.current = 0;
2528
+ lastAddressRef.current = null;
2529
+ lastChainIdRef.current = null;
2530
+ if (!walletRef.current?.isConnected) {
2531
+ setIsConnected(false);
2532
+ setAddress(null);
2533
+ setChainId(null);
2534
+ }
2535
+ }, [
2536
+ setIsExternalWalletConnected,
2537
+ setExternalWalletAddress,
2538
+ setExternalWalletChainId,
2539
+ setConnectionType,
2540
+ setIsConnected,
2541
+ setAddress,
2542
+ setChainId,
2543
+ ]);
2544
+ // When user disconnects in the extension, provider may emit before wagmi updates; listen so we clear state even if wagmi is stale
2545
+ const providerListenerCleanupRef = useRef(null);
2546
+ useEffect(() => {
2547
+ if (!externalWalletsEnabled ||
2548
+ !isExternalWalletConnected ||
2549
+ !wagmiAccount?.connector) {
2550
+ return;
2551
+ }
2552
+ const connector = wagmiAccount.connector;
2553
+ let mounted = true;
2554
+ const setup = async () => {
2555
+ try {
2556
+ const getProvider = connector.getProvider ?? connector.getEthereumProvider;
2557
+ if (typeof getProvider !== "function")
2558
+ return;
2559
+ const p = await getProvider();
2560
+ if (!mounted || !p)
2561
+ return;
2562
+ const onDisconnectFromProvider = () => {
2563
+ if (!isExternalWalletConnectedRef.current)
2564
+ return;
2565
+ handleWagmiDisconnect();
2566
+ };
2567
+ const onAccountsChanged = (accounts) => {
2568
+ const list = Array.isArray(accounts) ? accounts : [];
2569
+ if (list.length === 0)
2570
+ onDisconnectFromProvider();
2571
+ };
2572
+ p.on?.("accountsChanged", onAccountsChanged);
2573
+ p.on?.("disconnect", onDisconnectFromProvider);
2574
+ return () => {
2575
+ p.removeListener?.("accountsChanged", onAccountsChanged);
2576
+ p.removeListener?.("disconnect", onDisconnectFromProvider);
2577
+ };
2578
+ }
2579
+ catch {
2580
+ // Connector may not support getProvider (e.g. some injected)
2581
+ return undefined;
2582
+ }
2583
+ };
2584
+ setup().then((cleanup) => {
2585
+ if (mounted)
2586
+ providerListenerCleanupRef.current = cleanup ?? null;
2587
+ });
2588
+ return () => {
2589
+ mounted = false;
2590
+ const cleanup = providerListenerCleanupRef.current;
2591
+ providerListenerCleanupRef.current = null;
2592
+ if (typeof cleanup === "function")
2593
+ cleanup();
2594
+ };
2595
+ }, [
2596
+ externalWalletsEnabled,
2597
+ isExternalWalletConnected,
2598
+ wagmiAccount?.connector,
2599
+ handleWagmiDisconnect,
2600
+ ]);
2486
2601
  // Helper function to get network name from chain ID
2487
2602
  const getNetworkName = useCallback((chainId) => {
2488
2603
  const chainNames = {
@@ -2762,6 +2877,7 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
2762
2877
  signTransaction,
2763
2878
  sendTransaction,
2764
2879
  signTransactionViaAPI,
2880
+ signSolanaTransactionViaAPI,
2765
2881
  signTypedTxViaAPI,
2766
2882
  signAndSendTransaction,
2767
2883
  loginWithOTP,
@@ -3351,6 +3467,6 @@ export function AbstraxnProviderInner({ config, children, base, wagmi, }) {
3351
3467
  console.warn("Failed to update ExternalWalletButtons:", error);
3352
3468
  }
3353
3469
  }, [value, externalWalletsEnabled]);
3354
- return (_jsx(AbstraxnContext.Provider, { value: value, children: children }));
3470
+ return (_jsxs(AbstraxnContext.Provider, { value: value, children: [wagmi ? (_jsx(WagmiConnectionEffectSync, { onDisconnect: handleWagmiDisconnect })) : null, children] }));
3355
3471
  }
3356
3472
  //# sourceMappingURL=AbstraxnProviderInner.js.map