Capacitor React (iOS) Redirect fails after login (blank screen)

Hi,

I added Capacitor to my React app following the guide Ionic & Capacitor (React).

I encountered a problem that seems to be the same as the one described in this topic, which does not seem to be offering a solution…

In Xcode console however, the logs just say:

⚡️  To Native ->  Browser open 97869726
⚡️  TO JS undefined

And the webview is not closed automatically. If I close it manually and try to login again, I just see a blank screen.

By the way, my config switches from Web to Capacitor whenever necessary. The Web config works fine…

package.json

{
	"dependencies": {
		"@auth0/auth0-react": "^2.2.4",
		"@capacitor/android": "^6.1.1",
		"@capacitor/app": "^6.0.0",
		"@capacitor/browser": "^6.0.1",
		"@capacitor/core": "^6.1.1",
		"@capacitor/ios": "^6.1.1"
	},
	"devDependencies": {
		"@capacitor/cli": "^6.1.1",
	},
	"proxy": "http://localhost:4000",
	"engines": {
		"node": "20.x",
		"yarn": "1.x"
	}
}

src/contexts/auth/auth.js

import React from 'react'
// React Router hook for routing.
import { useNavigate } from 'react-router-dom'

import { Auth0Provider } from '@auth0/auth0-react'

import { Capacitor } from '@capacitor/core'

import { domain, clientId, audience, redirectUri } from './authConfig'

const Auth0ProviderWithNavigate = ({ children }) => {
	const isNative = Capacitor.isNativePlatform()

	const navigate = useNavigate()

	const onRedirectCallback = (appState) => {
		navigate(appState?.returnTo || window.location.pathname)
	}

	// if (!(domain && clientId && redirectUri)) {
	// 	return null
	// }

	return (
		<Auth0Provider
			domain={domain}
			clientId={clientId}
			useRefreshTokens={isNative ? true : undefined}
			useRefreshTokensFallback={isNative ? false : undefined}
			authorizationParams={{ audience, redirect_uri: redirectUri }}
			onRedirectCallback={!isNative && onRedirectCallback}
		>
			{children}
		</Auth0Provider>
	)
}

export default Auth0ProviderWithNavigate

src/contexts/auth/authConfig.js

import { Capacitor } from '@capacitor/core'

const isNative = Capacitor.isNativePlatform()

const PACKAGE_ID = 'REDACTED'

export const domain = process.env.REACT_APP_AUTH0_DOMAIN

export const redirectUri = `${
	isNative ? `${PACKAGE_ID}://${domain}/capacitor/${PACKAGE_ID}` : process.env.REACT_APP_HOST_URL
}/callback`

const authConfig = {
	domain,
	clientId: process.env.REACT_APP_AUTH0_CLIENT_ID,
	audience: process.env.REACT_APP_AUTH0_AUDIENCE,
	redirectUri,
}

export const { clientId, audience } = authConfig

src/index.js

import React, { Suspense } from 'react'
// Replaces ReactDOM.createRoot.
import { createRoot } from 'react-dom/client'

// Import i18n (needs to be bundled).
import './contexts/i18n/i18n'
import { Provider } from 'react-redux'
import store from './store/store'
import { BrowserRouter as Router } from 'react-router-dom'
import Auth0Provider from './contexts/auth/auth'
import CableProvider from './contexts/cable/cable'
import L10n from './contexts/l10n/l10n'
import Theme from './contexts/theme/theme'
import { App as AntApp } from 'antd'

import LoadingView from './components/loading/LoadingView'
import App from './components/app/App'

import reportWebVitals from './reportWebVitals'

// As of React 18.
const root = createRoot(document.getElementById('root'))

root.render(
	// Trigger React’s Suspense if translations are still being loaded by the `i18next-http-backend`.
	<Suspense fallback={<LoadingView />}>
		<Provider store={store}>
			<Router>
				<Auth0Provider>
					<CableProvider>
						<Theme>
							<L10n>
								<AntApp>
									<App />
								</AntApp>
							</L10n>
						</Theme>
					</CableProvider>
				</Auth0Provider>
			</Router>
		</Provider>
	</Suspense>,
)

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

src/components/app/App.jsx

// React Effect hook.
import React, { useEffect } from 'react'
// Auth0 hook for authentication.
import { useAuth0 } from '@auth0/auth0-react'

import { Capacitor } from '@capacitor/core'
import { App as CapApp } from '@capacitor/app'
import { Browser } from '@capacitor/browser'

import { Routes, Route, Navigate } from 'react-router-dom'

import AuthenticationGuard from '../shared/navigation/AuthenticationGuard'

import LoadingView from '../loading/LoadingView'

const App = () => {
	const { handleRedirectCallback, isLoading, isAuthenticated, user } = useAuth0()

	const isNative = Capacitor.isNativePlatform()

	const shouldHandleRedirectCallback = isNative
	useEffect(() => {
		if (shouldHandleRedirectCallback) return

		// Handle the `appUrlOpen` event and call `handleRedirectCallback`.
		CapApp.addListener('appUrlOpen', async ({ url }) => {
			if (url.includes('state') && (url.includes('code') || url.includes('error'))) {
				await handleRedirectCallback(url)
			}
			// No-op on Android.
			await Browser.close()
		})
	}, [shouldHandleRedirectCallback, handleRedirectCallback])

	if (isLoading) {
		// Loading auth...
		return <LoadingView />
	}

	return (
		<div>
			<Routes>
			</Routes>
		</div>
	)
}

export default App

src/utils/auth.js

import { Capacitor } from '@capacitor/core'
import { Browser } from '@capacitor/browser'

import { redirectUri } from '../contexts/auth/authConfig'

const isNative = Capacitor.isNativePlatform()

const signInCap = async (loginWithRedirect, opts) => {
	await loginWithRedirect({
		async openUrl(url) {
			await Browser.open({ url, windowName: '_self' })
		},
		...opts,
	})
}

const signInWeb = async (loginWithRedirect, opts) =>
	await loginWithRedirect({
		appState: { returnTo: '/' },
		...opts,
	})

export const signIn = (loginWithRedirect, opts = {}) =>
	isNative ? signInCap(loginWithRedirect, opts) : signInWeb(loginWithRedirect, opts)

export const signUp = (loginWithRedirect) =>
	signIn(loginWithRedirect, { authorizationParams: { screen_hint: 'signup' } })

const signOutCap = async (logout) => {
	await logout({
		logoutParams: { returnTo: redirectUri },
		async openUrl(url) {
			await Browser.open({ url, windowName: '_self' })
		},
	})
}

const signOutWeb = (logout) =>
	logout({
		logoutParams: { returnTo: window.location.origin },
	})

export const signOut = (logout) => (isNative ? signOutCap(logout) : signOutWeb(logout))

ios/App/App/Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>Editor</string>
            <key>CFBundleURLName</key>
            <string>REDACTED</string>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>REDACTED</string>
            </array>
        </dict>
    </array>
  </dict>
</plist>