Notifications
Issue #625. Every account has an in-app notification history; the mobile app can also receive push notifications. Push only draws attention: opening a notification always re-reads the resource with the caller's current access.
Production status — 20 October 2026
Production push is enabled for all eligible opted-in recipients following explicit
user approval. /tailspend/prod/notifications/push-enabled is true; the switch
is stage-wide, with no platform or single-account rollout filter. Account/category
preferences, personal group mutes, current membership and device ownership still
control each delivery. To stop sending, set the switch to false (or delete it);
the worker refreshes configuration within one minute and history continues.
Client Release is enabled. Android 1.2.8/build 26 is available through Play internal testing, and iOS 1.2.8/build 37 is valid for internal TestFlight. Both production artifacts and completed submissions were verified; internal uploads do not mean public store approval. Preserve these builds rather than rebuilding to repair a status-only workflow failure (#731).
The Play-installed Android app passed permission/settings recovery, foreground and process-cold group taps, People routing, logout deregistration, signed-out tap gating and renewed registration after sign-in. The physical TestFlight iPhone registered through Sign in with Apple, received the labelled production push and opened the expected group, confirmed by the user and New Relic. All five labelled direct-transport receipts succeeded. These probes bypass domain history/outbox.
The subsequent real invitation, sent between the user's two accounts in the zero-expense validation group, created one recipient history entry, completed its outbox row and produced one successful production worker push receipt. Both production notification alarms remained OK. The final invitation tap confirmation and current tracking status are recorded in issue #625. Earlier dated evidence below retains its historical scope; past release holds do not describe the current rollout. Physical-device iOS crash symbolication remains explicitly deferred, separate from the completed physical push delivery check.
Enabling notifications on a phone
The app asks for OS permission after the user taps Turn on push notifications in Profile; opening the app alone does not show the prompt. After choosing Allow, registration runs on sign-in, app resume and native token changes. If permission was denied, Profile explains how to recover through Open settings. An enabled account preference does not override an OS denial. Check the account shown in Profile when locating a device: Apple and Google sign-ins can belong to different TailSpend accounts even when both are owned by the same person.
What's sent
| Event | Who hears | History text | Lock-screen text |
|---|---|---|---|
| Expense updated/deleted | Current account holders in old/current payer and split sets, except the actor | "Expense updated in |
"Expense updated" / "Expense removed" |
| Structured comment mention | Current registered group members tagged explicitly, except the actor | "Mention in |
"New mention" |
| Friend request / invitation | Recipient, if the pending request/invitation remains valid and unblocked | "New friend request" / "Invitation to |
"Friend request" / "Group invitation" |
| Recurring reminder | Current account holders paying/splitting an active upcoming slot | "Upcoming expense in |
"Upcoming expense" |
| Expense created | Account holders paying or splitting it, except whoever added it | "New expense in |
"New expense" |
| Settlement recorded | Payer and payee, except whoever recorded it | "Payment recorded in |
"Payment recorded" |
| Join request submitted | The group's owners | "Request to join |
"New join request" |
| Join request approved | The applicant, if still a member | "You've joined |
"Request approved" |
| Join request declined | The applicant | "Join request declined" (never names the group) | "Join request declined" |
Guests never receive anything. Lock-screen text has no names, labels or amounts. Recurring expenses are announced as added automatically.
How it works
flowchart LR
Change[Expense / settlement / join request] -->|same transaction| Outbox[EVENT row]
Outbox --> Stream[Table stream, INSERT + SK=EVENT]
Stream --> Worker[tailspend-<stage>-notification-worker]
Worker --> History[NOTIF rows per recipient]
Worker -->|switch on| Expo[Expo Push Service]
- Outbox. Domain writes add an
EVENTrow in the same DynamoDB transaction, so a committed change always has its event and a failed one never does (apps/services/src/notifications/outbox.ts). - Worker. Works out recipients from the current roster, so someone removed meanwhile isn't told. It writes each recipient's history entry with a conditional put. If the entry already exists, the worker has handled that recipient and skips the push, which makes retried batches safe. Per-record failures are retried 5 times with batch bisection, then go to
tailspend-<stage>-notification-worker-failures. - Push. Sent only when the switch is on, the recipient's settings allow the category, and the event is under an hour old. Requests to Expo retry throttling, server errors and network failures up to 3 times.
DeviceNotRegisteredtickets remove the token straight away. - Maintenance (
tailspend-<stage>-notification-maintenance, every 15 minutes):- Receipts. Accepted tickets are kept for a day under
RECEIPTS#<15-minute bucket>. The job reads Expo receipts for sends 15–75 minutes old and removes tokens whose receipt saysDeviceNotRegistered. A ticket means Expo accepted the message; the receipt says whether Apple or Google did. Neither proves the person saw it. - Recovery. Each
EVENTrow carries aPendingKey(sparsePendingEventsIndex) until it's dispatched. The job re-dispatches events still pending 10 minutes after creation, which covers stream records that were lost or expired after 24 hours. Dispatch is idempotent per recipient, so this can't double up.
- Receipts. Accepted tickets are kept for a day under
- Devices.
PUT /users/me/devices/{installationId}registers a random per-install id and its Expo token. Each token has one owning install (TOKEN#claim, moved transactionally), so a phone that switches accounts stops receiving the old account's notifications. Registrations last 90 days unless refreshed; 10 per account. - Account deletion removes devices, claims, settings and history.
API
| Route | Purpose |
|---|---|
GET /users/me/notifications |
History, newest first, with unreadCount (capped at 99) |
POST /users/me/notifications/read |
Mark everything read |
GET / PATCH /users/me/notification-settings |
push (master), expenses, settlements, joinRequests, people, comments, reminders, atomic groupMute |
PUT / DELETE /users/me/devices/{installationId} |
Register / remove a device |
The push data payload (PushPayloadSchema, version 1) carries only ids. The app maps them to its own routes, and ignores pushes meant for another account.
Turning push on for a stage
- Upload APNs and FCM v1 credentials to EAS (
eas credentials) and ship a native build that includesexpo-notifications. - If the Expo project has push security enabled, store its access token:
aws ssm put-parameter --type SecureString --name /tailspend/<stage>/deployment-environment/EXPO_ACCESS_TOKEN --value <token> - Switch sending on:
aws ssm put-parameter --type String --overwrite --name /tailspend/<stage>/notifications/push-enabled --value true
The worker reads both within a minute. Set the switch to anything but true (or delete it) to stop sending; history keeps working either way.
Alarms (MonitoringStack): tailspend-<stage>-notification-failures fires on any message in the failure queue; tailspend-<stage>-notification-dispatch-failures fires on 5 or more dispatch failures in an hour (worker or recovery). Also useful in the logs: notifications.push_sent, notifications.push_rejected, notifications.receipt_error and notifications.events_recovered.
Follow-up operations — 13 October 2026
Events and preferences
Expense creation, edits and deletion notify current account holders in the old/current payer and split sets, excluding the actor and guests. Deletion opens the group. Settlements retain their existing rules. Join-request submission checks blocks against every OWNER (both directions), and dispatch checks again against the current paginated roster. If any OWNER/applicant block exists, the queued submission produces neither history nor push. A missing People dependency fails closed on this path. Pending friend requests and direct invitations are checked again for withdrawals, blocks, expiry and replacement before dispatch.
Comments carry an explicit mentionedParticipantIds list (maximum 20), selected in the web/native composer. Text is never parsed for usernames. The API rejects guests and participants outside the group, deduplicates tags and commits mentions plus their outbox event in one transaction. Message edits preserve existing tags when the field is omitted; newly added tags notify once, and removed tags are filtered before sending.
Hourly recurring ticks also query upcoming active slots within 24 hours. An outbox event is written conditionally once per template/slot. Reminder dispatch rechecks active status and the scheduled time; due, paused or rescheduled slots are discarded. Reminder errors are logged independently so they cannot block expense creation.
The profile provides expense, payment, join-request, people, mention and reminder switches. Profile also provides personal per-group mutes under Group alerts. Mutes only affect push; history remains. Each group mute uses an atomic DynamoDB set operation so changes to different groups cannot overwrite one another. Clients render current accessible group names in history titles.
Desktop continues to use the shared notification history. Native desktop alerts are deferred until a separate opt-in design and delivery lifecycle are agreed; they are not part of mobile push acceptance.
Credentials and rollout
As of 13 October 2026:
- Firebase project
tailspend-pushis on the free Spark plan, with Android packagestudio.kinesis.tailspend. The public Android config isapps/mobile/google-services.json; Expo prebuild wires it into the native project. Google Analytics was not enabled. - EAS FCM v1 uses the dedicated
tailspend-eas-fcmservice account withroles/firebasecloudmessaging.admin. Its private JSON key is held in EAS credentials; the downloaded temporary copy was removed. - EAS APNs key
HKJF3T28S9belongs to Apple team88C5VNABRRand is assigned tostudio.kinesis.tailspend. - The approved Expo robot
TailSpend Push Workerhas Developer access. Its token is stored only in/tailspend/{beta,prod}/deployment-environment/EXPO_ACCESS_TOKENas SSM SecureString. Enhanced push security is account-wide and enabled; other Expo apps under this account also need authenticated push requests. expo-notificationsrequires new native binaries. App/runtime version is bumped to1.2.5; an OTA cannot install the native module or Firebase configuration.
The production Client Release hold remains in place until beta validation passes. The New Relic backend/web kill switch does not disable the mobile agent or push delivery. Push sending has its own per-stage /tailspend/<stage>/notifications/push-enabled switch, cached for one minute and defaulting to off when absent. Enable beta only for acceptance, then restore off on failure; production needs its own approval/validated rollout.
Acceptance evidence
Expo Doctor passes all 21 checks; local release-mode iOS simulator and Android APK builds pass. The first iOS transport test returned APNs BadDeviceToken. Explicit simulator sandbox registration corrected it: a subsequent Expo receipt returned status: ok, and a labelled test notification appeared in the simulator's Notification Center. Physical devices retain Expo's provisioning-profile detection.
Live monitoring also exposed repeated native-token callbacks recursively requesting another native token. Registration now passes the listener's token to Expo and ignores identical callbacks; regression tests cover recursion, rotation and a fresh subscription. Push taps wait for the root navigator to mount and record a notification.open action containing only the event type. The rebuilt simulator app is installed. New Relic reports only two Expo token requests since its fresh launch, confirming the repeated-request loop stopped. The resumed transport/lifecycle results are recorded below. The matching iOS 1.2.1 dSYM uploaded successfully with app/image UUID 1DDDC07E-2186-3B0B-A0E0-A38161C7C604. A temporary local-only SDK crash probe produced a native SIGABRT; the diagnostic source was removed and the regular simulator app rebuilt/reinstalled. New Relic received occurrence 8BB395BC-96E3-493E-B723-66B83955B1F8 with the matching image UUID; its symbolicated flag was true but all 24 NerdGraph frames remained raw addresses. New Relic documents that simulator crashes cannot be symbolicated. This is a supported-platform limitation, so simulator named-frame resolution is not claimed. On 16 October the user explicitly deferred physical-device symbolication proof; that limitation no longer blocks the simulator-scoped release. The user-approved EAS keystore signed Android beta 1.2.1 (local validation build 21), installed successfully in the new TailSpend_Push_Validation Google Play emulator. Beta sign-in and OS notification opt-in were subsequently completed on 1.2.3. Both earlier emulator apps had different signing certificates and their saved data was preserved. The downloaded EAS keystore, credentials JSON and all temporary password copies were removed after signing. A separate release-mode Android SDK native-crash probe reached New Relic with named SDK and TailSpend activity frames; the ordinary app was restored afterward. Neither provider acceptance nor these isolated transport tests complete the domain-flow acceptance gate.
Use the user-approved Google Play Android emulator and iOS simulator. A compiled app or accepted Expo ticket is not delivery proof. Record foreground display, background delivery, cold-start tap, signed-out tap, missing/deleted resources, denied OS permission, account switching, reinstall/logout and account deletion. Check the Expo receipt and verify that taps only open resources belonging to the signed-in recipient. Reinstall tests should use a disposable simulator/emulator, preserving the user's existing device data.
Watch worker failure queue depth, dispatch/rejection log alarms, receipt failures and the recovery sweep. Tokens belong only in credentials/device storage and must not appear in logs, Git or screenshots. The stream may redeliver events; stable event IDs, conditional history writes and tap deduplication avoid duplicate history/navigation, while provider display remains best effort.
People read cost
GET /people/balances retains its 50-group cap and one-minute client/API cache. Rosters are fully paginated, including participants after the first 100. Only this calculation requests DynamoDB ReturnConsumedCapacity=TOTAL; parallel reads are scoped to the request with AsyncLocalStorage. New Relic records peopleBalanceDurationMs, peopleBalanceReadRequests, peopleBalanceReadUnits, group counts and partial status. Read requests count SDK commands, not individual network retry attempts.
The stage dashboard includes latency percentiles and average read units/commands. Measure with real beta People traffic after deploying this change; unit tests prove accounting, not production cost. Compare uncached requests at representative group/expense counts before deciding on precomputed settle-up plans.
Housekeeping
The referenced 17 September WIP on main: a35ae9f stash is absent from the main checkout. The three currently present stashes have different branches/subjects and were preserved. No unrelated stash was dropped.
Resumed simulator checks — 13 October 2026
The signed-in iOS 1.2.1 simulator displayed authenticated generic test pushes in the foreground, background and after termination. Foreground/background/cold-start taps opened notification history, with notification.open telemetry observed. A fabricated missing expense rendered Expense not found.; a payload naming a different recipient was ignored. These direct Expo transport probes intentionally bypass the domain outbox and do not add history.
Android's Google authentication completed, but native browser completion and Expo Router each tried to exchange the same one-use OAuth code. New Relic showed paired HTTP 200/400 token requests. The corrected auth flow deduplicates concurrent and late callbacks, validates persisted PKCE state, waits for startup bootstrap and discards completions superseded by logout/new sign-in. Final native version is 1.2.3; rebuild and repeat native acceptance before rollout. All 78 mobile tests pass, including six OAuth concurrency/recovery regression tests; type and lint checks pass.
A real beta iOS People request measured six reads, three read units and 73.88 ms for one group. A real two-group web request measured 11 reads, 5.5 units and 159.68 ms (partial=false). The deployed integration baseline measured six reads, three units and 18.04 ms. Keep the current minute cache and group cap; collect larger-group/expense samples before precomputing balances. Unit tests are not used as cost evidence.
Android lifecycle and auth recovery — 13 October 2026
On the dedicated TailSpend_Push_Validation emulator, EAS-signed beta 1.2.3/build 22 recovered from denied OS notification permission and registered with beta. Generic authenticated test pushes displayed with the TailSpend notification icon in the foreground and background; taps opened notification history. Replacing the same signed APK preserved app data and left no running TailSpend process; the next push launched a new process and opened history. Logout removed the Android device row, and tapping an already delivered notification while signed out did not open private data. The foreground, background, reinstalled cold-start and queued signed-out pushes all returned successful Expo receipts. Four Android notification.open actions reached New Relic. These direct transport tests bypass domain event generation, preference eligibility and history writes.
The iOS 1.2.3 simulator detected denied OS permission, opened its notification settings and recovered after permission was restored. A subsequent generic push displayed in Notification Center, opened history and returned a successful receipt. The personal test-group mute persisted in beta storage; after restoring it the muted-group set was empty.
Returning from Android's logout callback exposed a Maximum update depth exceeded crash: the old auth gate removed the root navigator during a route transition. Version 1.2.4 keeps the navigator mounted and uses Expo Stack.Protected for authenticated screens. Sign-in is a route, while public invite, help, About and OAuth callback screens remain reachable. A new Maestro flow covers API session expiry, signed-out private links and callback recovery; its fixture returns explicit API 401s while retaining the test session. The fixture regression passes. Repeat fresh native sign-in/logout on the rebuilt binaries before shipping.
Final 1.2.5 release-mode simulator/emulator binaries compile and install. Fresh Android 1.2.4 sign-in reached the beta dashboard; New Relic recorded exactly one token HTTP 200 and no token-request error, confirming the one-use code fix. Fresh Android 1.2.5 sign-in also reached the dashboard. The callback recovery button now targets the public /sign-in route explicitly. The correctly ad-hoc signed iOS 1.2.5 simulator restored its beta session and repeated foreground banner/display/tap plus process-terminated cold-start display/tap; both Expo receipts succeeded. The earlier unsigned local simulator build lacked keychain entitlements and was replaced with the correctly signed validation build; no signing-policy change is shipped. Temporary Android signing keys/password files were removed after every build.
Final Android 1.2.5 logout and callback recovery returned to the sign-in screen without the prior navigator crash. Fresh sign-in was repeated afterward. Both stage push switches were independently rechecked absent/default-off, and Client Release remains disabled_manually.
Final Android 1.2.5 missing-expense push rendered Expense not found. A subsequent FRIEND_REQUESTED test naming a fabricated different recipient was ignored, leaving the app on the prior expense screen.
Refreshed acceptance — 16 October 2026
PR #678 is merged and its native auth recovery flow passed CI. The fresh native
candidate is 1.2.6, using current compatible SDK 57 patches. EAS Build now accepts
preview-simulator, inheriting the beta API, preview environment and OTA channel,
and checks Expo Doctor before queueing builds. Native projects and local
credentials are excluded from the EAS archive. Local archive inspection confirmed
that generated mobile native projects and private key/environment files are absent.
The live beta integration checks passed expense creation/edit/deletion, structured mentions and deduplication, actor exclusion, muted-history retention, join-request history, preferences and token ownership transfer. Account deletion was exercised only with disposable identities; its live check verifies removal of device rows and token claims. The six People/invitation tests passed as well. These checks exercise real API writes and stream processing, without treating history or provider tickets as device-display proof.
The user explicitly deferred iOS physical-device native symbolication proof. Simulator crash capture and matching dSYM upload remain recorded; named simulator frames are not claimed. Production Client Release and production push remain held until fresh native candidate and remaining device lifecycle acceptance pass.
Real-worker validation on the installed beta 1.2.5 apps sent a join-request alert to both platforms. Android's tap opened the correct People screen and native approval admitted the disposable applicant. A subsequent expense-created alert opened the correct synthetic £1 expense on Android. Turning off that group's alerts in native history retained the update history and produced no new Expo tickets. Restoring alerts produced four tickets for an edit and mention across the two devices. Disabling the expense category also retained history without new tickets; the original preferences were restored. iOS display passed; the user also confirmed opening Notifications and then the validation group. Device Hub ignored automated direct push taps, so a fresh 1.2.6 direct-tap check remains required. Only synthetic test data was used.
Beta sending was enabled for this acceptance run and remains enabled for the beta pilot. Production sending remains off and Client Release remains disabled. The synthetic expense and disposable actor were deleted after validation; both native devices retain their original account settings and existing groups.
All 10 selected real-worker Expo tickets returned successful receipts, with zero errors or pending receipts. Receipt success records provider handoff; Android UI and the user's iOS navigation confirmation supply separate device evidence.
Notification presentation
Web and mobile history cards use event-specific icons and labels for expenses, payments, mentions, recurring reminders, invitations and join decisions. Each available destination is named in the bottom-right action (for example, “Open expense” or “Open group”); declined or incomplete entries remain readable without a dead navigation action. Group alert settings live on the main Profile screen on mobile and web. Notification history contains the event list. The mobile dashboard header keeps Notifications and Profile in padded 44-point controls. People remains available from Profile and the invitation banner.
Native candidate acceptance — 18 October 2026
PR #714 merged as bab5857 and its Main Pipeline passed, including production
backend/web deployment and checks. EAS beta builds for that commit completed:
Android f27d5bb8-fefa-40d2-bda7-eec4a2e00970 and iOS simulator
10cb57be-4556-4d94-9b56-9c8b134526de. Both installed as version 1.2.7 over
the existing test apps without clearing data. The Android APK contains only
ARM32/ARM64, with Hermes and React Native libraries, and uses the beta API.
Android passed twenty process-cold starts before sign-in recovery and another twenty with the signed-in session. iOS passed five process-cold starts and retained its recovered session. Both devices registered app version 1.2.7 in beta. iOS Profile includes notification categories and Group alerts; history shows type icons and explicit destination actions. History's Open group and Review requests actions opened the validation group and its People screen. Labelled background and process-terminated cold-start pushes opened the intended group/People destinations on iOS. Android foreground and background push taps opened People and the validation group. Android had a running process before its second tap, so that tap is not counted as cold-start proof. All five direct Expo tickets returned successful receipts; the foreground iOS banner was observed but its tap was not verified. These transport probes do not generate domain events or prove the full acceptance matrix.
Concurrent startup exposed transient DynamoDB TransactionConflict
cancellations during device registration. Registration now retries only
contention cancellations, with jitter and a maximum of three attempts. Each
attempt rereads the installation and token owner and rebuilds the atomic
transaction, retaining ownership conditions and account-device limits.
Exhausted contention returns HTTP 409; conditional/validation failures are not
retried. The app retries unsuccessful registration on its next normal trigger.
The rapid launch tests also exercised the existing weighted API rate limit;
limits remain unchanged. Keep Client Release held and production push off
until deployed recovery and the remaining beta acceptance are recorded.
Deployed registration recovery — 18 October 2026
PR #715 deployed as baddf25 to dev, beta and production. Three simultaneous
Android/iOS startup cycles passed on the installed 1.2.7 apps; both returned
to their signed-in dashboards with the original groups. During this check,
beta logged eleven registration responses, all HTTP 200, and no API 5xx.
This confirms the deployed registration recovery check, not every future
contention case. The final production integration gate still failed due to
runner connection timeouts and a developer role test reusing mutated state
on retry. Its repair is separate from the Hermes/ARM startup acceptance;
Client Release and production push remain held until all remaining gates pass.
18 October 1.2.7 domain acceptance follow-up
Both EAS 1.2.7 beta apps scanned a synthetic receipt (Tea £3 + Sandwich £5),
read the £8 total and saved the expense. New Relic recorded successful
receipt.scan and expense.create actions for both platforms. The user
approved deleting both OCR expenses; the original validation expense remains.
A disposable authenticated beta actor generated a join request, expense, payment and successive expense edits through the public API. Android opened People from the real join-request push and the exact expense from the background expense push. Current-device Expo receipts succeeded, the outbox cleared and replaying a delivered expense event twice added no duplicate history. Actor exclusion held; only its approval notification went to the actor. A temporary personal group mute retained the next edit in history and produced no new current-device push tickets; the original alert preference was restored. Six current-device receipts succeeded. Production notification alarms were OK. These checks do not claim the full remaining failure/lifecycle matrix or production push delivery.
Android process-reclamation tests reproduced a cold-start failure: tapping an
expense update opened the dashboard, with no notification.open action. The
response subscription now listens before reading the SDK cache and re-reads
on app resume, navigator changes and session restoration. It waits for the
authenticated protected routes, retaining recipient validation and tap
deduplication. Regression tests cover a native response buffered after the
first read and navigation becoming ready later. At this stage beta runtime
validation and direct iOS taps were pending; the 19 October results below
supersede that status. Physical-device iOS native symbolication remains
explicitly deferred by the user.
19 October native push-tap recovery
Preview update f4564e5 published successfully for both 1.2.7 platforms, and
both New Relic source maps uploaded. Runtime telemetry identifies Android
update 01a15154-c20b-78db-9269-9bdf4725f856 and iOS update
01a15154-c20b-772a-b44c-7453e46f0a7b. The updated iOS app opened the exact
synthetic expense from a real expense-update alert after its process was
terminated. Android still opened Dashboard after process reclamation, despite
selecting the individual alert and retaining the saved session. The JavaScript
readiness change alone is therefore not accepted as the Android fix.
The scenario matches Expo #49254: FCM auto-displays a notification while the process is dead, but Android can lose its launch extras when the app's task survives. No JavaScript response listener can recover an intent payload the native layer never receives.
Version 1.2.8 adds an Android-only config plugin and messaging-service subclass. For visible messages carrying Expo markers, the service temporarily removes FCM's notification-enable flags before Firebase decides whether to auto-display. It restores the notification block before handing the message to Expo, retaining title, channel, sound, data and the SDK's foreground-handler semantics. Expo then uses its notification forwarder rather than the affected launcher intent. Direct FCM and Expo data-only messages keep their original handling. The manifest removes Expo's default FCM entry point and registers exactly one private TailSpend service; APNs, credentials and backend delivery are unchanged. Expo and Firebase remain precompiled dependencies, and Hermes V1/ARM support remains unchanged.
The Android CI gate runs three Robolectric tests against Firebase's actual dispatcher: visible Expo messages, stock direct-FCM handling and data-only messages. All three native tests, release Kotlin/manifest compilation, 93 mobile tests, lint, TypeScript and workflow checks pass. The focused provider, maintenance and dispatcher suites pass all 34 tests, including bounded retry, invalid-token cleanup and recipient eligibility.
A fresh locally EAS-signed 1.2.8 / Android version code 25 beta APK was
installed over the existing app, preserving authentication and data. Its
signing certificate matches 1.2.7; its packaged service and beta API were
verified, and its native libraries contain only ARM32/ARM64. The first build
needed the same Java memory settings already used by CI; the retry succeeded.
APK SHA-256:
491e08e3bbd6adf3ec3651411298e3a13e8315ef463dd0f06557f232d2eee78f.
Real worker delivery and OS taps passed in the foreground and after background
process reclamation while the Android task remained. The first cold tap opened
the exact synthetic expense; a second cold tap opened a newly created payment
with a different destination, ruling out a stale expense intent. New Relic
recorded two EXPENSE_UPDATED opens and one SETTLEMENT_RECORDED open for the
1.2.8 embedded update 995a259f-a1db-48c4-a12b-72a4303ff2e3. The updated iOS
1.2.7 beta recorded the successful cold expense tap. Six recent receipts for
the two beta devices were ok, with no errors; ten audited outbox rows were
complete. This verifies the Android native workaround; an OTA alone cannot
install this service into 1.2.7.
Operational acceptance also covered a live partial-batch replay: only the
intentionally unparseable synthetic record failed, while valid duplicate
records before and after it completed. A simulated interrupted outbox state
was recovered by the deployed maintenance sweep (eventsRecovered: 1), with
pending state cleared and the existing history row unchanged. This is scoped
fault simulation, not a claim of a forced provider outage or killed worker.
The labelled synthetic domain expense, both test payments, disposable account and its anonymized participant were removed after acceptance. The original validation expense is preserved; the group has one participant and zero balances. Profile, identity, notification and Cognito cleanup was verified. The two earlier OCR fixtures are also gone and alert preferences are restored.
PR #718 merged as f2a14e5 after all PR checks passed, including the required
native push-handoff tests. Production Client Release and push remain held
pending Main deployment gates,
the remaining current-candidate lifecycle/initial-fetch checks, and production
native artifact/submission validation. Physical-device iOS symbolication
remains explicitly deferred. Release native before OTA; queueing a store
submission is not store approval.
Dashboard Initial-Fetch Recovery Follow-Up
Release review confirmed that an initial profile/group query failure fell
through to the empty-group and zero-balance UI. The dashboard now distinguishes
that error from a successful empty response, offers Retry, and preserves cached
groups/balances with a refresh-failure notice. Four rendered-screen regression
cases pass, and fixture-backed native flows cover group/profile 503 recovery.
No beta fault injection, API-limit change or real-user data reset is involved.
Current-source native-flow execution remains pending; rendered tests alone are
not claimed as device acceptance. See docs/mobile-release.md for test scope.
Final 1.2.8 Lifecycle Check — 19 October
Main at a9d73f2 passed all dev/beta/prod gates. EAS beta builds
52d41f40-ba0f-448d-adf8-c232d3184703 (Android) and
a1e0aafb-9cec-4652-9d2e-e9344eea8049 (iOS simulator) completed from that
commit and were installed over saved apps. Both registered version 1.2.8
with beta. Signing compatibility, the beta API in both bundles, the Android
push service and ARM32/64-only libraries were verified.
On Android, loss of Wi-Fi/mobile data retained cached groups and balances with the refresh-failure notice. Reconnecting and retrying restored the ordinary dashboard; both network settings were restored. A labelled direct transport alert tapped while signed out did not expose the protected group; signing in afterward resumed the buffered destination and retained the original group/expense. This transport probe does not generate domain history.
Logout exposed an acceptance failure: the session cleared but its exact device row remained, and scoped access logs contained no device DELETE. Cleanup had depended on the in-memory successful token for this process. The follow-up instead removes an existing saved installation, stops new registration work and drains started API writes before deletion. It caches the installation ID before the write, avoiding another storage lookup on the ordinary logout path, and never creates a new ID just to log out. Generations keep an old cleanup from removing a newly signed-in account's registration. The Profile action awaits the browser logout before routing to Sign In, so the ordinary return cannot leave the app on the missing-sign-in-parameters recovery screen.
Six asynchronous lifecycle regressions cover persisted-only cleanup, pending and failed API writes, late token callbacks, invalid/missing IDs and a new account overtaking cleanup. This code still needs the reproduced logout/device row check to pass on beta before production Client Release/push is restored. The Mac lock currently prevents the remaining iOS UI checks; its fresh beta startup/registration succeeded, but iOS permission/tap acceptance is not inferred from that. Physical-device iOS symbolication remains explicitly deferred.