Skip to content

feat(notifications): resolve taps to native intents instead of web sessions - #22

Merged
Aalv3 merged 1 commit into
fix/auth-failure-classification-20260830from
feat/native-notification-intents-20260909
Sep 9, 2026
Merged

Aalv3 merged 1 commit into
fix/auth-failure-classification-20260830from
feat/native-notification-intents-20260909

Conversation

@Aalv3

@Aalv3 Aalv3 commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Implements the founder-approved native notification intent model and removes the abandoned authenticated-WebView experiment.

The architectural fix

DiscourseUtils.endpointForSiteNotification is lossy: a granted_badge becomes /badges/:id/basic?username=:u and badge_name is discarded. Every earlier attempt at badge routing started from that URL, so by the time classification ran, the information a native screen needs was already gone — which is why each attempt ended at a web page. notificationIntent reads the payload first.

Four kinds: native, badge, staff_external, unavailable.

Type matrix

Class Types Destination
A 1–11, 13, 14, 15, 17, 18, 20, 24, 25, 27, 28, 34, 36, 801, 802 Topic
A 800 MemberProfile
A 21 with a topic Topic
A 21 without a topic MemberProfile — see note
B 12 granted_badge BadgeEarned (new)
Staff 37, 38 /admin external, staff only
C 16, 19, 22, 23, 26, 29, 30, 31, 32, unknown, absent bounded unavailable

Note on 21: it resolves to /u/:me/activity/approval-given, which the pre-existing profile pattern already matched. A valid native destination exists, so it is class A and deliberately untouched — I did not reclassify existing behavior to fit the new model. A test pins this.

BadgeEarned

Renders badge_name only, from the payload, with Close. No network request — asserted by a test that fails the render if fetch is called.

badge_title is a boolean in Discourse (may the badge be worn as a title), not descriptive text — the repo's own existing fixture had badge_title: false, confirming it. It is never rendered. No badge description exists in the payload and none is fetched.

Removed experiment surface

js/webViewSession.js (deleted) · js/__tests__/webViewSession.test.js (deleted) · first_party_web disposition + FIRST_PARTY_WEB_PATHS + isFirstPartyWebPath · _openFirstPartyWeb · destination bootstrap prop/wiring · web_session diagnostic stages.

Strict WebView guard restoredWebViewComponent.js and WebViewScreen.js reverted to their pre-experiment state, so canonical pages without a native route no longer fall through to an unauthenticated Discourse session. The WebView screen itself is untouched where legitimately used.

notificationRouting.test.js was the experiment's own suite and is removed; its still-valid URL-classifier boundary tests are salvaged into memberRouteBoundaries.test.js.

Tests — 781/781 across 96 suites

notificationIntent.test.js (56 tests) proves: granted_badge → badge intent; badge_name survives from payload; the URL form would have lost it; badge_title never rendered as text; malformed badge payloads fall to bounded state; BadgeEarned renders the name, makes no network request, and closes via both header and action; all 25 topic types unchanged; MemberProfile unchanged; staff /admin staff-only; members denied it; every class-C type bounded; unknown/absent bounded; unauthenticated resolves nothing; off-origin unavailable; destinationPresentation no longer emits web; the experiment files are gone; no intent path can reach a WebView; the strict guard is restored; read-marking still precedes resolution and the tap site no longer builds a URL.

Gates

format/lint clean · Jest 781/781 · verify:ota, verify:ios-auth, verify:release-readiness, verify:backend, validate:ota all PASS.

Per the new CI policy this is authenticated-surface JS work the logged-out Detox suite cannot exercise. It does touch js/Discourse.js, so the classifier will select the full suite until PR #21 lands and its auth-shell path list is reconsidered.

OTA

JS-only, runtime an-ios-android-1.0.0-native-2 unchanged. Not published — awaiting separate approval.

…ssions

A notification tap now resolves to an intent computed from the notification
payload, before any URL is built: native, badge, staff_external, or
unavailable.

The payload ordering is the whole point. endpointForSiteNotification is lossy -
a granted_badge becomes /badges/:id/basic and badge_name is discarded - so
every earlier attempt at badge routing started from a URL that had already
thrown away what a native screen needs, and each one ended at a web page.

granted_badge opens a small native BadgeEarned screen rendering badge_name from
the payload. It makes no network request, so it cannot stall or fail.
badge_title is a boolean in Discourse - whether the badge may be worn as a
title - and is never rendered as descriptive text; the payload carries no
description and V1 does not fetch one.

Topic and MemberProfile routing are untouched. Type 21 without a topic resolves
to /u/:me/activity/approval-given, which the profile pattern already matched;
that is a valid native destination and is left exactly as it was. Staff /admin
keeps its external handoff as an explicit documented exception - members get
the bounded state, never the handoff. Everything else - group_message_summary,
liked_consolidated, membership_request_accepted, chat, unknown types - reaches
one explicit unavailable state. No silent no-op.

Removes the abandoned authenticated-WebView experiment: js/webViewSession.js,
the first_party_web disposition and its allowlist, the destination bootstrap
wiring, _openFirstPartyWeb, and the web_session diagnostic stages. The strict
WebView navigation guard, relaxed to admit the bootstrap, is restored. The
WebView screen itself is untouched where it is legitimately used.

The OTP contract was eventually correct - the request succeeded and the
confirmation form rendered - but Finish Login failed with "Missing, invalid or
expired token", and the architecture was wrong regardless: a member already
authenticated in the app should never perform a second web authentication to
read a notification.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
@Aalv3
Aalv3 merged commit cf588cd into fix/auth-failure-classification-20260830 Sep 9, 2026
2 of 4 checks passed
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