anza-xyz / wallet-adapter

Modular TypeScript wallet adapters and components for Solana applications.
https://anza-xyz.github.io/wallet-adapter/
Apache License 2.0
1.59k stars 960 forks source link

Phantom's in-app browser not opening on iOS #814

Open umangveerma opened 1 year ago

umangveerma commented 1 year ago

Describe the bug Trying to connect Phantom wallet in safari browser on mobile, and Phantom's in-app browser is not opening. I'm required to click Solflare from the list of wallets (which is not installed), redirected to Solflare's website and on trying again after coming back to the initial page Phantom as an option works fine.

To Reproduce Steps to reproduce the behavior:

  1. Go to https://solpayments.vercel.app on an iPhone
  2. Click on any product, click on 'add to bag', then click on 'checkout' from sidebar
  3. Click on 'connect wallet' button on the payment page, choose Phantom as an option and nothing will happen on multiple tries
  4. Then click on Solflare, you'll redirected to their website if wallet not installed, come back to the initial payment page at domain (checkout.candypay.fun)
  5. Try Phantom again and it will work now, but it wasn't working earlier

Note - If Phantom works for you in the first try and the payment page opens up in it's in-app browser, please come back to the payment page in your browser and try clicking on Phantom again and it will not work in the second try, which is unexpected behaviour and not great for UX in case user wasn't able to connect wallet in first try for some reason.

Screenshots https://github.com/solana-labs/wallet-adapter/assets/91828247/4385eb0b-658a-411a-834e-622a561911d9

Smartphone

mcintyre94 commented 1 year ago

Okay I've done some digging

On your link, the behaviour I'm seeing is:

Obviously there are a lot of things that could be causing this, so I've done some tests to try to narrow it down a bit

First, here's a codesandbox with two buttons that deeplink into Phantom/Solflare respectively using the same JS as wallet-adapter: https://zfn6gj.csb.app/ . This works correctly, nothing stops me going phantom - back - phantom - back - phantom. So it's probably not a wallet issue, or a limitation of iOS redirects/universal links

Second I modified our starter/example app to add Phantom + Solflare adapters. When I open this with iOS I find:

I think that the behaviour I'm seeing on your site is consistent with that when using the dialog/modal button and selecting Phantom/Solflare.

Without seeing your code it's hard to know exactly what would get the behaviour that you expect, or if there's a bug here.

But speculating a bit, here's what I think is probably happening:

But even if it did call setWalletName, that wouldn't trigger anything because it's already set to that (per the localstorage line). This is why it works when you choose Solflare, it changes walletName and then when you choose Phantom again it works.

But this should only be an issue if there is already a localstorage key containing a previously selected wallet, and you select the same one again.

For apps like yours where storing the wallet name doesn't make sense (since you're not using autoconnect or displaying the connected wallet in the UI), it might make sense for us to add an option to WalletProvider to disable localstorage?

You can verify this by using Safari to debug your page, if you delete the walletName localstorage key and refresh it'll work correctly. If you click Phantom/Solflare, then back, you'll see walletName is set in localstorage. And whichever one that was won't work, but if you delete the localstorage key and refresh then it will.

letehaha commented 9 months ago

I also faced that problem.

if I use a dialog/modal button, select Phantom, it redirects correctly. But if I go back, the connect and multi-buttons show the Phantom logo, tapping the dialog/modal again does not redirect. Same with Solflare. But using the connect or multi-button correctly redirects

@mcintyre94 thanks for your dive into it, it really looks logical and makes it clear! I tested it a bit, and it seems to be a valid point about walletName in LocalStorage. So if there's no value initially, any wallet will work. Then it will work only if you switch between wallets. But there's also a thing that if you manually delete the value, it won't work anyway, only after the refresh, as you said. Buut is that really expected behavior?

For your understanding that's how I'm using it:

// app.ts
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";

export default function app() {
 // ...

  const network = WalletAdapterNetwork.Mainnet;
  const wallets = useMemo(
    () => [
      new PhantomWalletAdapter(),
      new SolflareWalletAdapter(),
      new TorusWalletAdapter(),
      new MathWalletAdapter(),
      new Coin98WalletAdapter(),
      new CloverWalletAdapter(),
      new HuobiWalletAdapter(),
      new CoinbaseWalletAdapter(),
      new BitKeepWalletAdapter(),
      new NekoWalletAdapter(),
      new TrustWalletAdapter(),
      new NightlyWalletAdapter(),
      new SalmonWalletAdapter(),
      new FoxWalletWalletAdapter(),
    ],
    [network]
  );

  return (
    // ...
    <ConnectionProvider>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>
          {/* ... */}
        </WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  )
}

And that's my ConnectWallet component that triggers the modal.

// connect wallet button
import { useWalletModal } from "@solana/wallet-adapter-react-ui";

export const ConnectWallet = () => {
  // ...
  const { setVisible, visible } = useWalletModal();

  return (
    // ...
    <button
      onClick={() => {
        setVisible(true);
      }}
      type="button"
    >
      {connecting ? t`Connecting...` : t`Connect wallet`}
    </button>
  )
}

@mcintyre94 let me know please if that info helps you identify the problem.

Tbh I'm not sure I can ask you for anything, since seems like you're not the part of team. If I'm correct, please let me know, so I'll understand if I need to address it to someone else or try to fix myself :)

yanmarinichinterexy commented 6 months ago

Any updates on this?

cbuckley-code commented 6 months ago

I'm stuck here too... My code looks exactly the same as above (with just PhantoWallet). This code works great on a laptop with the Chrome extension but on my iphone it throws my app into the Phantom Wallet app and the modal overlay never triggers off. Here's a link https://deadco.info/sphere (don't worry there's no transactions going on ...) I'm just trying to connect to get the public key so I can send it to my server for a claim. i want to launch this during the Dead and Company shows in Las Vegas.

// pages/_app.js
import React from 'react';
import { WalletModalProvider, WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';

// Import the CSS for the wallet adapter
require('@solana/wallet-adapter-react-ui/styles.css');

const MyApp = ({ Component, pageProps }) => {
    const endpoint = "mainnet"; // This can be adjusted to your preferred network
    const wallets = [new PhantomWalletAdapter()]; // Add other wallets like Sollet, Ledger, etc.

    return (
        <ConnectionProvider endpoint={endpoint}>
            <WalletProvider wallets={wallets} autoConnect>
                <WalletModalProvider>
                    <Component {...pageProps} />
                </WalletModalProvider>
            </WalletProvider>
        </ConnectionProvider>
    );
};

export default MyApp;

Source for the component is here https://github.com/DeadCoSol/deadco-website/blob/main/nextjs-deadco/components/ClaimForm.js

cbuckley-code commented 6 months ago

Okay I've done some digging

On your link, the behaviour I'm seeing is:

  • If I click Phantom on first visit, it works correctly. If I then tap back, then Phantom doesn't work again
  • Same with Solflare
  • The "last visited" seems to be sticky across tabs, for example if I open Phantom, go back, close tab, open a new one at the same URL, Phantom still doesn't work

Obviously there are a lot of things that could be causing this, so I've done some tests to try to narrow it down a bit

First, here's a codesandbox with two buttons that deeplink into Phantom/Solflare respectively using the same JS as wallet-adapter: https://zfn6gj.csb.app/ . This works correctly, nothing stops me going phantom - back - phantom - back - phantom. So it's probably not a wallet issue, or a limitation of iOS redirects/universal links

Second I modified our starter/example app to add Phantom + Solflare adapters. When I open this with iOS I find:

  • if I use a multi-button, select Phantom, it redirects correctly. If I go back, the connect and multi-buttons shows the Phantom logo, tapping it again redirects correctly. Same with Solflare
  • if I use a dialog/modal button, select Phantom, it redirects correctly. But if I go back, the connect and multi-buttons show the Phantom logo, tapping the dialog/modal again does not redirect. Same with Solflare. But using the connect or multi-button correctly redirects

I think that the behaviour I'm seeing on your site is consistent with that when using the dialog/modal button and selecting Phantom/Solflare.

Without seeing your code it's hard to know exactly what would get the behaviour that you expect, or if there's a bug here.

But speculating a bit, here's what I think is probably happening:

if (walletName === nextWalletName) {
  return;
}

But even if it did call setWalletName, that wouldn't trigger anything because it's already set to that (per the localstorage line). This is why it works when you choose Solflare, it changes walletName and then when you choose Phantom again it works.

But this should only be an issue if there is already a localstorage key containing a previously selected wallet, and you select the same one again.

For apps like yours where storing the wallet name doesn't make sense (since you're not using autoconnect or displaying the connected wallet in the UI), it might make sense for us to add an option to WalletProvider to disable localstorage?

You can verify this by using Safari to debug your page, if you delete the walletName localstorage key and refresh it'll work correctly. If you click Phantom/Solflare, then back, you'll see walletName is set in localstorage. And whichever one that was won't work, but if you delete the localstorage key and refresh then it will.

I can't get the example to run. If I do an npm install it complains about 'workspace:' not being set I see it's used in the package.json but do I need to set it for each package?

dogebonker commented 4 months ago

Faced the same issue, which complicated stuff in my dapp. Read through the whole tread and the localStorage thing is true. Basically, Phantom acts different from Solflare (only tested those two for now), and in a scenario where both wallets are in a locked state, Solflare would show a popup, library would write the value of Solflare to walletName key or whatever you provided as a property localStorageKey to <WalletProvider />, but if you never unlocked Solflare wallet and just closed the popup it will erase the walletName key from local storage, while in Phantom case even if you closed the modal popup without actually unlocking the wallet the key in local storage stays set to Phantom, which is why you are unable to press the button for the second time.

dogebonker commented 4 months ago

Have to update you on my last comment, this is not the case, I implemented a fix in my dapp which removes walletName after pressing the button on iOS but when I came back to the browser's tab my button was still not responding to my clicks.

dogebonker commented 4 months ago

Ok, so I actually found out the issue, it's here:

        if (this._readyState !== WalletReadyState.Unsupported) {
            if (isIosAndRedirectable()) {
                // when in iOS (not webview), set Phantom as loadable instead of checking for install
                this._readyState = WalletReadyState.Loadable;
                this.emit('readyStateChange', this._readyState);
            } else {
                scopePollingDetectionStrategy(() => {
                    if (window.phantom?.solana?.isPhantom || window.solana?.isPhantom) {
                        this._readyState = WalletReadyState.Installed;
                        this.emit('readyStateChange', this._readyState);
                        return true;
                    }
                    return false;
                });
            }
        }

What this does is basically it sets the connecting state of Phantom to true and, as far as I understood, never sets it to false afterwards, so here is what I did to make it work on iOS:

walletsToCheck.forEach(({ name, globalObject }) => {
    if (localStorageWallet === name && globalObject === undefined) {
      localStorage.removeItem('walletName')
      const wallet = wallets.find((wallet) => wallet.adapter.name === name)
      if (wallet) {
        wallet.adapter.disconnect()
      }
    }
  })

in Phantom case:

{ name: 'Phantom', globalObject: window.phantom }

What's sad is that it still doesn't work for Android as Phantom only has the isIosAndRedirectable check, which in our case is false and we fallback to Phantom button doing nothing.