Skip to content

fix(android): let the authorization response survive a task reset (#1094) - #1137

Closed
dparcerisa wants to merge 1 commit into
FormidableLabs:mainfrom
dparcerisa:fix/android-survive-task-reset
Closed

fix(android): let the authorization response survive a task reset (#1094)#1137
dparcerisa wants to merge 1 commit into
FormidableLabs:mainfrom
dparcerisa:fix/android-survive-task-reset

Conversation

@dparcerisa

Copy link
Copy Markdown

Draft, because the last section is a real design question I would rather have answered before polishing this.

Fixes the Data intent is null failure in #1094: start an authorization, leave the app to read a one-time code from a mail app, come back from the launcher icon, and the login dies. Anyone whose provider emails a code hits it.

Two causes, and neither fix works alone

before:  AuthorizationManagementActivity   <- holds the pending request
         MainActivity
(return to the app from the launcher)
after:   MainActivity

AppAuth: No stored state - unable to handle response
  1. AppAuth keeps the pending request inside AuthorizationManagementActivity, which lives in the caller's task. When the host app's launcher activity is launchMode="singleTask"what React Native recommends for deep linking — re-entering from the icon clears the task above the root and destroys it. Even a perfectly delivered response has nothing to match against.
  2. authorizeWithConfiguration delivers via startActivityForResult(authIntent, 52), which is bound to the living activity instance and cannot cross tasks. Once the activity is gone the result arrives as a null Intent.

What this PR changes

Only the delivery half. The response now comes back through a PendingIntent addressed to the launcher activity — the component only, no MAIN/LAUNCHER categories, since those are what trigger the reset — and is completed from onNewIntent, which the module declared but left empty. Result handling is factored into a shared method, so the activity-result path is untouched.

The other half is on the host app: give AuthorizationManagementActivity its own taskAffinity plus singleInstance, which keeps the launcher activity's launchMode alone. Happy to document that in the README if this direction is accepted.

Measured, on an Android 15 emulator

Attempt Outcome
singleTop on the launcher activity (the workaround in #1094) Worse — error gone, but the promise never settles, so the login silently hangs
android:alwaysRetainTaskState="true" No effect; does not override singleTask clear-top
This change alone AppAuth still has no stored state to match
This change plus the host-app task change Works — verified twice in a row

Also checked that deep links and universal links still resume a single launcher-activity instance rather than spawning a second.

The open question

This changes how the response arrives for every Android consumer. It could instead sit behind a config flag (androidUsePendingIntentDelivery or similar), at the cost of a JS-side option and two code paths to maintain.

I lean unconditional — the PendingIntent path is strictly more robust and onNewIntent is already wired through ActivityEventListener, so RN apps route it without extra work — but it is your call, and I will rework it either way. There are no tests in this area yet; tell me the shape you want and I will add them.

Closes the "Data intent is null" failure reported in FormidableLabs#1094: start an
authorization, leave the app (to read a one-time code from a mail app, for
instance), come back from the launcher icon, and the flow dies.

Two things combine to cause it. AppAuth keeps the pending request inside
AuthorizationManagementActivity, which lives in the caller's task; when the host
app's launcher activity is launchMode="singleTask" — what React Native
recommends for deep linking — re-entering from the icon clears the task above
the root and destroys it, and AppAuth then logs "No stored state - unable to
handle response". Separately, the response is delivered with
startActivityForResult, which is bound to the living activity instance and
cannot cross tasks, so it arrives as a null Intent.

This change addresses the delivery half: the response now comes back through a
PendingIntent addressed to the launcher activity, completed from onNewIntent
(declared but empty until now). The result handling is factored into a shared
method so the activity-result path is unchanged.

Host apps also need AuthorizationManagementActivity to survive, which they can
do by giving it its own taskAffinity and singleInstance without touching their
launcher activity's launchMode. Both halves are needed; neither works alone.

Measured on an Android 15 emulator. The workaround suggested in FormidableLabs#1094 —
switching the launcher activity to singleTop — was also tried and is worse: the
error disappears but the promise never settles, so the login silently hangs.
android:alwaysRetainTaskState="true" has no effect.
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

@dparcerisa is attempting to deploy a commit to the Nearform Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 69b3d4f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dparcerisa

Copy link
Copy Markdown
Author

Closing this, and I want to be straight about why: we shipped it, QA broke it, and it is worse than what it replaces. Two findings, both from real devices rather than emulators.

1. It trades a catchable error for silence.

Delivering the response through a PendingIntent means the app must launch itself from the background. Android grants that only briefly after the app loses focus, and OEM skins cut it shorter. On an Oppo A74 (Android 14, ColorOS) with default settings:

  • A slow provider — Spotify, which mails a one-time code — never completes. The mail round trip outlasts the allowance, the launch is refused, and the promise never settles: no resolve, no reject, nothing to catch.
  • A fast provider (Google) completes fine, getting in under the wire.
  • Enabling ColorOS "auto-launch" makes the slow one work, which confirms the mechanism.

So the change does not fix the bug on the devices most likely to hit it; it converts Data intent is null into an unobservable hang. An error you can catch is strictly better than one you cannot.

2. The host-app half I recommended breaks retrying.

Marking AuthorizationManagementActivity as singleInstance in a separate task does keep it alive across a task reset — but a second authorization can then never create a fresh instance. Android reuses the stale one, AppAuth reads the resume as a cancellation, and the browser never opens. Abandoning a login and trying again — ordinary behaviour — is left broken. No emulator showed this; it took a deliberate retry test.

What we did instead, in case it is useful to anyone landing here from #1094: we left the library alone and handled it in JS. Remember that a flow is in the air, clear it on every settled path, and if either Data intent is null arrives or the app returns to the front with the flow still unsettled, offer to resume. One tap reopens the browser; the provider session is usually still alive, so it completes immediately. It does not hide the interruption, but it needs no permission the manufacturer can withhold and it does not break retrying.

Sorry for the noise. I would rather withdraw this than leave a proposal standing that I now know regresses real devices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant