PortalSession that it passes straight to
Portal. No login system of your own, and no server-side call to mint a
Client Session Token.
Your app owns three things the SDK deliberately does not: when a sign-in
starts, how the authorize URL is opened, and how the redirect gets back
to you. PortalAuth never opens a browser and never registers a deep-link
handler.
Client Auth requires a version of
@portal-hq/core that exports PortalAuth.
If PortalAuth is not exported by your installed version, upgrade to the latest
release.Prerequisites
- Authentication enabled for your environment, with at least one sign-in method and your Auth Environment ID to hand (see Enable authentication)
- The sign-in methods you want configured — Email magic links, Google OAuth, or Apple OAuth
- A redirect URL on your environment’s allow list (see Redirect URLs)
- A working Portal integration (see Getting Started)
Installation
There is nothing new to install.PortalAuth ships in @portal-hq/core, and
the session is stored with react-native-keychain, which
@portal-hq/keychain already requires.
Register your redirect
The OS only delivers a redirect to your app if you register its scheme natively. For a redirect URL ofmyapp://auth/callback, register the scheme and
make sure a returning redirect reaches the running app.
- iOS
- Android
Register the scheme in Registering the scheme is not enough on its own. A cold start arrives through
Without this,
Info.plist:launchOptions, which Linking.getInitialURL() reads, but the usual Client
Auth path — the browser returning to an app that is still running in the
background — needs your AppDelegate to forward the URL:Linking.addEventListener('url', …) never fires for a warm or
backgrounded app and the sign-in appears to hang.The notification is posted directly because React-RCTLinking ships only its
.m/.mm sources, so RCTLinkingManager.h is not a public header and the
class is not visible from Swift. In an Objective-C AppDelegate you can call
[RCTLinkingManager application:app openURL:url options:options] instead — it
posts the same notification.Create a PortalAuth
Create one long-livedPortalAuth and reuse it. It holds no in-flight state, so
concurrent sign-ins are safe and a sign-in survives your app being backgrounded.
isAccountAbstracted is fixed when you construct PortalAuth, and it only
takes effect on a user’s first sign-in. A returning user keeps the client they
already have.Check which methods are enabled
getMethods() reports what your environment allows, so you can render only the
buttons that will work.
autoCreateWallet is a signal for your app — nothing in the SDK or the API acts
on it. See Create or reuse the wallet.
A two-factor requirement is not visible here. It only appears when a
sign-in is completed, so your redirect handler must always be ready for it.
Start a sign-in
Google or Apple
loginWithGoogle() and loginWithApple() return an authorize URL. They do not
open a browser and do not wait for the sign-in to finish — your app opens the
URL, and the result arrives later as a deep link.
Linking.openURL hands off to the system browser, which is what the Portal
example app does and what Google’s OAuth policy requires. You can instead use a
native auth-session API such as ASWebAuthenticationSession or Android Custom
Tabs, but be careful: on Android the Custom Tab redirect also fires your
intent filter, so the session’s own result and your deep-link listener will both
deliver the same redirect. Feed it to handleRedirect() from one path only —
see Complete the sign-in.Email magic link
sendMagicLink() resolves once Portal has handed the email off for delivery. It
tells you nothing about the eventual sign-in, which arrives as a deep link when
the user opens the link.
Every call sends a real email, and sends are rate limited to 10 per address per
minute. Never retry automatically; make a resend an explicit user action.
Complete the sign-in
handleRedirect() is the single completion path for every method. Give it the
incoming URL and it either resolves a result, resolves null because the URL
was not a Client Auth redirect, or throws.
Register the listener before you open any browser, and read getInitialURL()
once per process in case the redirect is what launched your app. Keep that
latch at module scope rather than in component state: getInitialURL() keeps
returning the launch URL for the lifetime of the process, so a per-component
guard lets a remount replay a grant that has already been spent.
handleRedirect() resolves null for any URL that is not this instance’s
redirect, which makes it safe to call from a shared deep-link handler alongside
your app’s other routes.
Handle two-factor authentication
If your environment requires a second factor, a completed sign-in resolves withstatus: 'totpRequired' instead of a session. Nothing is persisted yet.
totpLink is an otpauth:// URI on a user’s first sign-in — render it as a QR
code for their authenticator app — and null once they are enrolled.
On success, verifyTotp() resolves the same shape the non-TOTP path produces,
so result.session is the PortalSession you carry on with — hand it to the
same place you would hand a session from handleRedirect():
createPortal from
Initialize Portal from the session:
Rendering the QR code is your app’s job; Portal does not ship a QR component.
Any React Native QR library works — pass
challenge.totpLink to it verbatim.Initialize Portal from the session
Pass the session ascredentials. Everything else about Portal is unchanged —
including backup, which is still required.
credentials and apiKey are mutually exclusive — passing both throws.
Create or reuse the wallet
A returning end user keeps the client and wallet they already have, so check for addresses before creating anything. Create a wallet only when the environment asks you to, viaautoCreateWallet.
Restore the session on launch
restoreSession() rebuilds the session from secure storage so a returning user
does not sign in again.
End the session
There are three ways a session ends, and they are not interchangeable.There is no server-side revoke endpoint, so both operations are local
sign-outs. The token itself stays valid until the backend expires it. See
Authentication and API Keys
for session lifetimes.
Neither operation deletes the wallet’s signing shares. The session lifecycle and
the wallet lifecycle are separate: when the user signs in again, they continue
with the same wallet, provided that wallet is otherwise still available to them.
Handle session invalidation
When the backend rejects the credential with a401, the session ended
somewhere your app cannot see and the user has to sign in again.
portal.clearSession() — you already know
about a sign-out you asked for.
Subscribe from something that lives as long as the Portal it belongs to. The
callback is not replayed, so if the only subscriber is inside a screen that has
been unmounted by the time the session ends, the event is missed and your app
carries on looking signed in.
As with a sign-out, an invalidated session does not delete the wallet’s signing
shares — only the credential is gone. Wallet state cannot be read without a
session, so treat it as unknown until the user signs in again rather than as
evidence the wallet is missing.
Handle errors
Client Auth surfaces two error types. Branch on theirreason.
PortalAuthError — a sign-in that could not be completed:
PortalCredentialError — a credential that could not be turned into a token: