TMA · React Native Team Brief

Everything that requires an App Store / Play Store release — and nothing that doesn't. Seven tasks · core locked ≤ 40h — plus a new email deep-link add-on folded into RN‑6 (~2.5h → ≈43h all-in). Sprint A (R1): RN‑4 + RN‑5 in ~2 working days. Sprint B (R2): RN‑1/2/3/6/7 in ~3–4 working days. File-level instructions live in the companion Sprint Spec (open beside this page).
v2.4 · 14 Jul 2026 · Scope: native iOS + Android (React Native) only · Cut from the TMA Analytics Master Implementation Brief v2.7
New in v2.4: route-map link added; the email→revenue UTM capture is flagged as Nic / no-release (master Task 27), not RN scope. v2.3: email deep-link add-on folded into RN‑6 (email links open the app to the right screen — direct + deferred after install; uses AppsFlyer OneLink, already owned). v2.2: AppsFlyer RN‑5/6/7 · code audit · Sprint Spec & Code Touchpoints (day plans, files, functions, hard hour caps). Decision locked: Amplitude is the client event destination for funnels. v2.1: RN‑4 early. v2.0: Paywalls · Retention Map · push/review.
Prepared for Aga's review before it is sent to the RN team · Verification by TMA's analytics desk (§9)
Sprint Spec · files & hours → Open brief + sprint side-by-side. Caps: R1 ≤15h · R2 ≤25h · all ≤40h.
🔍 Code-verified (14 Jul 2026) — MovementAthlete-RN, no code changes in this pass

These are facts from the current binary plumbing — they explain Meta Ads Manager screenshots (ATE True Status Rate unavailable, ATE parameter volume out-of-range, app currently ineligible for iOS 14+ attribution) and the Google-lead / screen-path gaps.

⏱️ Locked hours (code-audited) — not open-ended ranges

These replace vague “2–3 week” quotes. Detail: day plans, file/function lists → sprint-spec.html. Hard caps are the hour ceiling per task.

TaskWhatReleaseEstHard cap
RN‑5ATE / ATT + AppsFlyer always-initR12.5h3h
RN‑4RevenueCat Paywalls (Offerings already exist)R110h12h
RN‑1Canonical id — Amplitude + AF + RC attrsR24h5h
RN‑6UTM persist + Google stitchR23.5h4h
RN‑6 ➕Deep-link add-on — email OneLink opens the right screen (direct + deferred)R22.5h3h
RN‑7Screen tracker (one listener)R22h3h
RN‑3Intercom anonymous + push tokenR22h3h
RN‑2Amplitude catalog events (extend existing services)R28h10h
Sprint A · R1 — ~2 working days13h15h
Sprint B · R2 — ~3–4 working days (core)22h25h
All native scope (core)35h40h
All-in incl. deep-link add-on37.5h43h

Overage only with a written change request naming extra files and why — see Scope rules. Store review wait does not count toward the hour caps.

▶️ Before you start — a ~15-minute confirmation, then a firm quote

You built TMA's RevenueCat and AppsFlyer integrations, so you already know this app's auth and subscription plumbing — that's the whole reason you're the team for this, and the reason the estimate above is tight rather than a wide external-team range.

Please do a quick pass over the auth, workout-lifecycle and paywall code before you begin, then answer these three questions and give a firm per-task quote:

  1. Does the app render the current RevenueCat Offering, or are product IDs hardcoded? This decides how much of RN‑4 is already done — if it fetches the current Offering, remote price testing may work almost immediately.
  2. What RevenueCat SDK version is the app on? The Paywalls SDK (RN‑4) needs a recent version; an SDK bump may be a prerequisite.
  3. Is a push SDK + deep-link routing already present — and does the AppsFlyer OneLink deep-link callback do more than console.log? (this also sizes the RN‑6 email deep-link add-on) Sizes RN‑3 — if push is already wired, it's the small end of the range.
  4. Confirm the fire-points for RN‑2's events are where this brief assumes — clean central startWorkout/completeWorkout functions and tidy auth hooks. If the workout lifecycle is more scattered than expected, flag it now, not mid-build, and re-quote.

The estimate holds if the code is where you expect it. The two things that move it are the workout lifecycle being more spread out than assumed, and how much paywall rework RN‑4 needs — this pass catches both up front instead of them becoming surprises.

0 · Context — what this is and why it matters

TMA is a subscription fitness product. Users arrive through a marketing quiz and a web checkout (both internal, not your scope), then train in the native iOS/Android app (your scope). The company is instrumenting the full journey — ad → quiz lead → checkout → trial start → paying subscriber → retention (D1/D7/D30) — so every step becomes visible and the ad platforms can optimise toward buyers instead of form-fillers.

Almost all of that wiring is dashboard connectors, ad-console settings, GA4 and backend work, and it is being done internally with no release cycle. Two things, however, live inside the native app binary and therefore need a store submission:

A third, smaller item (RN‑3) lets TMA reach the ~760 people/week who install from Google ads and never sign up, via a free push notification — which needs the app to register anonymous installs with Intercom on first launch.

You are not designing anything — you are firing a list that already exists

TMA's full event taxonomy is already designed and named in Amplitude; the events you need for RN‑2 are simply switched off. Your job is to fire an existing, named list at the right moments and to set the same id everywhere. Every event, with a "fire when" and priority, is in the companion Event Catalog & Wiring Spec (TMA-NATIVE-EVENT-CATALOG.html) — §6 lists the exact events for this brief.

1 · Your scope — and, just as important, what is NOT your scope

✅ In scope (this brief)
🚫 Explicitly NOT your scope — do not build or re-build these (they are owned internally, no release)
📦 How it ships — two releases (updated v2.2)

Release 1 — now: RN‑4 + RN‑5. Paywall leak + Meta/iOS App Install attribution both need a binary now. RN‑5 is small and attribution-critical (Ads Manager shows App installs as Eligible with a Quality issue on ATE volume).

Release 2 — with the signup/onboarding redesign: RN‑1 + RN‑2 + RN‑3 + RN‑6 + RN‑7. If redesign slips, RN‑1 + RN‑6 (identity + Google stitch) can join Release 1 — they sit on the revenue / email-sequence critical path.

2 · The retention map — the retention work that needs a store release (your scope)

TMA's biggest revenue number is churn, not acquisition — but only part of "improving retention" is a native-app problem. The rows below are the pieces that genuinely need a binary: your work. (Subscription-curve dashboards, failed-card dunning and funnel-stage retargeting are handled internally with no release — out of your scope, see §1.)

Retention capabilityNeeds a release?OwnerNotes
Behavioural D1 / D7 / D30 retention — do they actually train? 🔴 R2
redesign
You — RN‑2 Only in-app workout events answer this. Subscription data tells you they still pay; it can't tell you they still use it — which is the leading indicator of the churn that follows.
Fixing the paywall leak — iterate design & price 🔴 yes — onceYou — RN‑4 Paywall → card entry is ~15–20% today. After RN‑4, every future fix is remote. This is the release that stops needing releases.
Push re-engagement (incl. the never-signed-up pool) 🔴 R2
redesign
You — RN‑3 + RN‑2 Token registration (RN‑3) makes it possible; push open/permission events (RN‑2) make it measurable.
The read

These rows are the things that are impossible without a binary — your work in this brief. RN‑4 is the one with compounding value — it converts an unlimited number of future paywall/pricing releases into dashboard changes.

3 · Why the work is sequenced this way — read this, it's the logic

A fair worry: "if TMA is rebuilding the signup/onboarding/paywall flow, won't the analytics get redone too — so isn't this wasted?" The answer is no, and the reason is the split below.

Kind of workBuild whenExamples
Screen-INDEPENDENT
survives any redesign
Now — this briefCanonical identity (RN‑1); auth + workout lifecycle events (RN‑2); anonymous-install registration (RN‑3). These fire off the SDK layer and the RevenueCat callback — not off any specific screen. A workout is a workout whether the paywall is old or new.
Screen-DEPENDENT
tied to a specific UI
Later — into the NEW flowOnboarding- and paywall-screen events (mini_onboard_*, assessment screens, paywall-screen views). Don't instrument screens that are about to be deleted — wire the new ones as they're built.

And why identity/retention goes in before the redesign, not after: the flow being rebuilt is the exact flow that is unmeasurable today. If it's redesigned before it's measured, there's no baseline — no way to prove the new flow converts better (or catch it converting worse). The screen-independent measurement goes in now, establishes a baseline over ~2–3 weeks, and the redesign is then measured against it.

4 · Task RN‑1 (T4) — One canonical user identity across all SDKs

RN‑1 · T4 · THE KEYSTONE
Set one shared user id everywhere, on login and signup
🔴 store releaseEst 4h · Cap 5h · iOS + Android
App code — React Native, both platforms
Every server-side integration attaches events to a user id. If RevenueCat, Amplitude and AppsFlyer each hold a different id, a person's ad-click, trial and subscription land on three different ghosts and the journey can't be stitched. Everything downstream depends on this — including the connectors TMA is configuring internally, which only attribute correctly once these ids and attributes are set.

What to do

  1. Pick the canonical id: the backend user id — the same id the web quiz sets as the GA4 user_id. One id, used in every SDK. Confirm the exact field with TMA before you wire it.
  2. On login and on signup, set it everywhere:
    Purchases.logIn(userId)            // RevenueCat
    amplitude.setUserId(userId)        // Amplitude
    setCustomerUserId(userId)          // AppsFlyer
  3. Set the RevenueCat subscriber attributes so the server-side connectors can attach correctly:
    Purchases.setAttributes({
      '$amplitudeUserId': userId,
      '$appsflyerId': await AppsFlyer.getAppsFlyerUID()
    })
    ($appsflyerId is already present on active profiles — this keeps it correct going forward and adds the Amplitude join.)
  4. Handle logout / account switching so ids never leak between users: call Purchases.logOut(), reset the Amplitude user id, and clear the AppsFlyer customer id on logout.
Done when: one test purchase appears under the same user id in RevenueCat, Amplitude and AppsFlyer. (TMA's desk verifies this against the three live APIs and reports the ids it sees.)

5 · Task RN‑2 (T7) — Native product & lifecycle events

RN‑2 · T7 · RETENTION CORE
Fire the already-named auth & workout events — plus push & review measurement
🔴 store releaseEst 8h · Cap 10h · iOS + Android
App code — React Native, both platforms
Without in-app behaviour events, D1/D7/D30 retention and activation can't be read from real usage — and retention is the number that decides both break-even and the company's valuation. The events already exist and are named; they simply don't fire.

What to do

  1. Initialise and identify the native Amplitude SDK on both platforms, using the RN‑1 canonical id (so every event joins to the same person as their subscription).
  2. Fire the already-named events from the Event Catalog at the right moments. Start with the retention coreauth and workout lifecycle (the exact list is in §6). You are not designing events; the names, properties and "fire when" are specified in TMA-NATIVE-EVENT-CATALOG.html.
  3. Keep the locked names exactly. Amplitude event names are permanent — no renaming without a migration plan. Use the catalog's names verbatim (do not invent variants).
  4. Add the push + review events (new in v2.0 — these don't exist in the catalog yet and are needed to make re-engagement measurable):
    • push_permission_requested / push_permission_granted / push_permission_deniedthe grant rate literally is the size of the reachable pool (see RN‑3); we currently can't see it.
    • push_received / push_opened — carry a campaign/message id so a send can be tied to a return visit. Without these, every push we send is unmeasurable.
    • app_opened with an open source (push · deep link · organic) — this is the denominator for D1/D7/D30 and tells us whether push actually brings people back.
  5. Add the native in-app review prompt + fire in_app_review_prompted. Trigger it on a genuine win (e.g. a completed workout / a level-up), never on launch. Ratings drive App-Store ranking → free installs. It's a small add on a release we're already shipping.
  6. DEFER the onboarding-screen and paywall-screen events (mini_onboard_*, assessment and paywall-screen views) until the signup redesign ships — those screens are being rebuilt (see §3). They are not part of this release. Exception: keep firing the existing checkout events named in RN‑4 so the paywall funnel survives the Paywalls-SDK swap.
Done when: workout + auth events arrive from real devices carrying the canonical id, a clean D1/D7/D30 retention curve can be drawn, and a test push produces a push_opened tied to its campaign id. (TMA's desk confirms via the Amplitude API.)

6 · Task RN‑3 (T12) — Intercom anonymous-install registration + push token

RN‑3 · T12 · FREE REACH
Register anonymous installs with Intercom & send the device push token on first launch
🔴 store releaseEst 2h · Cap 3h · iOS + Android
App code — React Native, both platforms (Intercom SDK)
Roughly 760 people/week install from Google ads and never sign up, so TMA has no email for them. A push notification is the only free way to reach them — worth exactly one push, then write-off. It needs a push token, and today the app only registers people with Intercom after signup, so anonymous installs are unreachable. This closes that gap.

What to do (the RN code piece)

  1. On first launch, before signup, register the install as an anonymous Intercom contact:
    Intercom.loginUnidentifiedUser()
  2. Once notification permission is granted, send the device token to Intercom:
    Intercom.sendTokenToIntercom(token)
    With Expo, the @intercom/intercom-react-native config plugin wires the native side automatically — confirm it's present.
  3. When the user later signs up, transition the anonymous contact to the identified user (Intercom.loginUserWithUserAttributes with the canonical id) so the two don't double-count.
  4. Prime the permission ask — don't burn it on cold launch (new in v2.0). Show a soft in-app pre-prompt explaining the value, and only fire the OS permission dialog after the user has had a win (e.g. finishing their first workout). Asking on first launch, before any value is shown, gets denied — and a denial is permanent (the user must go to Settings to undo it). The grant rate is the size of this entire pool; if you ask badly, RN‑3 delivers nothing. Fire the push_permission_* events from RN‑2 so we can see the grant rate. Exact placement is a product call — propose one and TMA will confirm.
Two things TMA handles / must decide — flag, don't block on them
  • Push credentials are uploaded by TMA, one-time, in the Intercom console (iOS APNs .p8 key; Android Firebase service-account JSON) — not your hours. Your code just needs the token registered.
  • Billing: Intercom bills on contacts — pulling every anonymous install in can push TMA up a pricing tier. TMA will confirm this before enabling; if they ask you to gate it behind a flag, keep it toggleable.
  • Permission timing: asking for notification permission at a bad moment (first launch, before any value is shown) kills the grant rate. If the ask can sit slightly later in the flow, note it — but final placement is a product call, not a blocker for this task.
Done when: a freshly-installed device that has not signed up appears as a push-reachable contact in Intercom, and a test push delivers to it. (TMA's desk confirms the reachable-contact count via the Intercom API.)

7 · Task RN‑4 — RevenueCat Paywalls SDK (the one that ends the release cycle)

RN‑4 · NEW IN v2.0 · STRATEGIC
Render the paywall from RevenueCat, so paywall & pricing changes never need a release again
🔴 store releaseonce, everEst 10h · Cap 12h · iOS + Android
▶ SHIP IN RELEASE 1 with RN‑5. Don't wait for the redesign. Paywall + Meta/iOS ATE both need a binary now. RN‑4 has no dependency on RN‑1/2/3/6/7.
App code — React Native, both platforms · RevenueCat SDK + RevenueCatUI (Paywalls)
The business problem: only ~15–20% of people who see TMA's paywall go on to enter a card. That is the single biggest leak in the funnel. Fixing it means iterating — testing prices, headlines, layouts, trial framing — and today every one of those tests requires a full App Store / Play Store release, because the paywall is hand-coded in the app. At a 1–2 week release-and-review cycle per test, that leak can never realistically be closed.

The fix: RevenueCat can serve the paywall remotely — its Paywalls product is server-driven UI, and its Experiments product assigns A/B variants server-side. Once the app renders the paywall from RevenueCat's remote config instead of its own hardcoded screen, TMA changes prices, copy, layout and runs A/B tests from a dashboard, with no build, no review, and no waiting for users to update. This is a one-time integration that permanently removes the release cycle from TMA's most important conversion surface.
Two things to tell us in the confirm-pass (they change the hours):
  1. Does the app already fetch and render the current Offering from the RevenueCat SDK, or are the product IDs hardcoded in the paywall screen? This is the precondition for all remote price testing. If Offerings are already used, a chunk of RN‑4 is effectively done and remote price experiments could work almost immediately.
  2. What RevenueCat SDK version is the app on? Paywalls needs a reasonably recent SDK — an SDK bump may be a prerequisite and should be quoted in.

What to do

  1. Upgrade the RevenueCat SDK to a version that supports Paywalls, on both platforms.
  2. Add RevenueCatUI (the Paywalls SDK) and present the paywall from the remote Paywall configuration attached to the current Offering — replacing the hand-coded paywall screen as the thing users see.
  3. Always render the current Offering for the customer (never hardcoded product ids). This is what lets RevenueCat's Experiments assign a variant server-side and have the app simply show it — no build required.
  4. Keep the existing checkout events firing from the new paywall — subscription_checkout_screen_opened, payment_initiated, subscription_checkout_confirmed, begin_checkout — so the paywall funnel is continuous across the swap and we don't lose historical comparability. This matters: don't let the SDK swap silently orphan the funnel.
  5. Verify the loop end-to-end: change something in the RevenueCat dashboard → confirm it appears on a device without a new build. Then run a two-variant Experiment and confirm devices are assigned to different variants.
  6. Fallback: configure a fallback paywall / offering so customers on older app versions (who won't have the Paywalls SDK) still get a working paywall.
Done when: (1) a paywall design/copy change made in the RevenueCat dashboard appears on a real device with no new build; (2) a price change made by swapping the Offering appears the same way; (3) a two-variant Experiment splits traffic and both variants render; and (4) the checkout events still fire from the new paywall.
Why this is worth more than its hours. Every other task in this brief makes something visible. This one makes something changeable. After RN‑4, TMA stops needing you for paywall and pricing work entirely — the marketing team iterates the highest-leverage screen in the product from a dashboard, indefinitely.

7b · Task RN‑5 — iOS ATE / ATT + AppsFlyer always-init NEW · R1

RN‑5 · META ATTRIBUTION BLOCKER
Always initialise AppsFlyer; mirror ATT into ATE; stop gating the SDK on grant-only
🔴 store releaseships with RN‑4Est 2.5h · Cap 3h · iOS (+Android verify)
App code — app/_layout.tsx, services/AppsFlyerService.ts, services/FacebookService.ts
Why Meta shows zero usable App Install attribution: Events Manager reports no iOS 14.5 ATE True Status Rate, Advertiser Tracking Enabled parameter volume out-of-range on App installs, and the app currently ineligible for Meta's iOS 14+ attribution. Code audit: ATE is not hardcoded to false — it is never set, and AppsFlyer is not started at all unless ATT is granted. AppsFlyer normally stamps ATE from ATT automatically once the SDK is running; skipping init on deny starves Meta of ATE=false volume and drop-ships grant users without timeToWaitForATTUserAuthorization.

What to do

  1. Always call initializeAppsFlyer() on iOS and Android — remove the “ATT not granted → skip AppsFlyer” branch in _layout.tsx.
  2. Pass timeToWaitForATTUserAuthorization (e.g. 10–60s) into initSdk so the first launch waits for the ATT answer before the AppsFlyer launch/install ping.
  3. Still request ATT via expo-tracking-transparency (already present). On resolve: if using Facebook SDK for any app events / Audience Network path, call Settings.setAdvertiserTrackingEnabled(granted) for iOS < 17 compatibility. AppsFlyer MMP → Meta does not need a separate ATE setter when the SDK is live; the bug is the skip-on-deny gate.
  4. Do not hardcode ATE=false as a permanent default. ATE must follow ATT: granted → true; denied/restricted → false; send both populations.
  5. Confirm Meta Events Manager: ATE True Status Rate populates; App installs Quality issue on ATE volume clears after enough traffic; a known Meta install appears as attributed in AppsFlyer Raw Data.
Done when: AppsFlyer initialises for grant and deny; Meta receives events with ATE true and false; Quality issue on App installs ATE volume is gone (or trending down after volume); a Meta test install attributes in AppsFlyer.

7c · Task RN‑6 — UTM / install attribution persist + stitch NEW · R2

RN‑6 · GOOGLE + PAID SOURCE STITCH
Persist install media_source / UTM at install time; stitch to user_id + email on register
🔴 store releaseEst 3.5h · Cap 4h  +  deep-link add-on 2.5h / cap 3h · iOS + Android
App code — AppsFlyerService + auth register/login hooks · builds on RN‑1 (does not replace it)
Today conversion data is logged to console / optionally a one-shot Firebase install_attribution for Facebook only — Google and other paid sources are discarded, nothing is stored for the later signup, and there is no AppsFlyer setCustomerUserId. You cannot answer “which leads came from Google install” in a spreadsheet tied to email / welcome sequence.

What to do

  1. On onInstallConversionData (and deep-link / re-engagement if present), persist the full payload locally: at minimum media_source, campaign, adset, ad, af_status, af_channel, and any utm_* / partner params. Do this for all non-organic sources, not only Facebook.
  2. On register / login (with RN‑1):
    appsFlyer.setCustomerUserId(userId)
    appsFlyer.setAdditionalData({ media_source, campaign, …, email })
    Purchases.setAttributes({
      '$appsflyerId': afUid,
      media_source, campaign, utm_source, utm_medium, utm_campaign
    })
    // also Firebase setUserId + setUserProperty for the same UTM fields
  3. POST the stitch to TMA backend (endpoint TMA provides): { user_id, email, appsflyer_id, media_source, campaign, utm_* } so marketing can export Google installs → welcome email sequence. Native work stops at a reliable payload; the spreadsheet / Intercom segment is internal.
  4. Verify paid UTM attribution: install from a Google Ads / AppsFlyer OneLink test link → conversion data shows expected media_source → after register, AF user, RC subscriber attributes, and backend all show the same stitch. (Dashboard partner config is TMA-owned; flag if GCD is empty despite a correct test link.)
Done when: a Google-test install + register produces one row-equivalent stitch (utm/media_source + user_id + email + appsflyer_id) visible in AF + RC attributes + TMA backend export.
➕ Deep-link add-on (email → app) — NEW 14 Jul · folds into RN‑6

Why it's here, not a new task: RN‑6 is already in the AppsFlyer OneLink + DeepLinkService code and rides the same store release. Today TMA's marketing emails can only link to the App/Play Store — a dead end for users who already have the app, and a lost journey for those who don't. This makes an email link open the app to the right screen, and carry a brand-new user through install to that screen (deferred deep linking). It uses OneLink, which TMA already owns — no new tool.

App code — AppsFlyerService (OneLink listener) + existing DeepLinkService / router · iOS Associated Domains entitlement · Android App-Links intent filters

What to do (app-code only)

  1. Wire the OneLink deep-link listener to the router. Today the AppsFlyer deep-link callback only console.logs (see §10). Route on deep_link_value (+ sub-params) through the existing DeepLinkService to the mapped screen — direct (app installed) and deferred (first launch after install: read the deep-link payload from onInstallConversionData when is_first_launch is true).
  2. Register the domains for the OS handshake: add the OneLink subdomain AND the email click-tracking domain (Customer.io / ActiveCampaign wrap links) to the iOS Associated Domains entitlement (applinks:) and the Android App-Links intent filters. The ESP wraps links for click tracking, which breaks a plain Universal Link unless BOTH domains are registered — this is why the add-on needs a binary.
  3. Implement the route map TMA provides (the route map ↗) — a small deep_link_value → screen table (e.g. workout, resume_assessment, upgrade, update_payment, today). Unknown / empty value → app home; never crash on an unmapped link.
NOT your hours (TMA-side, no release): confirming OneLink is on the AppsFlyer plan · building the OneLink template · the Customer.io / ActiveCampaign ESP integration setup · handing you the exact OneLink + ESP domains and the route-map table. You receive those and wire the app.
Done when (add-on): a test OneLink email link, tapped with the app installed, opens the mapped screen (not the store, not app home); and a fresh install from that link lands on the mapped screen after first launch (deferred). Verified against TMA's OneLink test link.

7d · Task RN‑7 — Screen-level tracking NEW · R2

RN‑7 · APP OPEN → PAGE PATH
Fire screen / route events so “8 app opens” become 8 named screens
🔴 store releaseEst 2h · Cap 3h · iOS + Android
App code — Expo Router navigation listener + AppsFlyerService / AnalyticsService.logScreenView
AppsFlyer app_open / session counts have no screen URL or route. Firebase logScreenView exists but is unused outside examples. Funnel collapses to trial / subscribe / purchase with no Install → Purchase screen path.

What to do

  1. Central navigation listener (Expo Router) on every route change: fire both
    analytics().logScreenView({ screen_name, screen_class })
    appsFlyer.logEvent('af_content_view', { af_content_id: route, af_content_type: 'screen', screen_name: route })
    Use the route path as the stable id (e.g. /(app)/(tabs)/home, /(app)/pages/upgradePremium).
  2. Include durable screens (home, workout, skills, settings, paywall/upgrade, auth). Skip throwaway redesign-bound mini-onboarding pages per §3 — those wait for the new flow.
  3. Optional but useful: attach last screen_name as AF / RC additional data so purchase context shows last screen before checkout.
Done when: a device session that opens Home → Workout → Upgrade produces three distinct screen events in Firebase DebugView and AppsFlyer (not just one app_open).

8 · The exact events for RN‑2 — from the catalog

These are the screen-independent events to wire in this release. Full properties and "fire when" for each are in TMA-NATIVE-EVENT-CATALOG.html (groups referenced below). Do not rename.

🔑 Auth & identity — P0 (anchors RN‑1)

auth_session_started · auth_login_succeeded · auth_login_failed · auth_register_succeeded · auth_register_failed · auth_logout · auth_forgot_password_requested · auth_mode_switched

🏋️ Workout lifecycle — P1 (retention core — the priority)

workout_started · workout_session_started · workout_prep_opened · workout_completed · workout_session_completed · workout_session_feedback · rpe_submitted · workout_recap_share · workout_player_leave_to_plan · workout_cantdo_pain_chain

🔔 Push, session & review — NEW in v2.0 (not yet in the catalog — add them)

push_permission_requested · push_permission_granted · push_permission_denied · push_received · push_opened (+ campaign/message id) · app_opened (with open source: push / deep link / organic) · in_app_review_prompted

These are what make re-engagement measurable and give D1/D7/D30 a denominator. Propose exact property names in the confirm-pass and TMA will lock them into the catalog before you build.

💳 Keep firing from the new paywall (RN‑4) — don't orphan the funnel

subscription_checkout_screen_opened · begin_checkout · payment_initiated · subscription_checkout_confirmed

⏸️ Deferred to the redesign — do NOT wire in this release

Onboarding & assessment screens (mini_onboard_*, advanced_assessment_*, injury_profile_saved, placement_feedback_submitted, training_disclaimer_acknowledged) and paywall-screen views — these are screen-dependent and wait for the new flow. Today-screen / settings depth events (today_*, settings_*, tab_*) are P2 and out of scope for this release unless TMA asks.

Naming convention (locked — applies to everything you fire)

RevenueCat events use the rc_ prefix (server-side, not yours) · AppsFlyer uses af_ (connector, not yours) · the app's own client events use their catalog names verbatim. Amplitude event names are permanent — no renaming without a migration plan.

9 · Acceptance, deliverables & how sign-off works

How each task is signed off — you don't self-certify

You wire the code; TMA's analytics desk verifies against the live APIs (RevenueCat / Amplitude / AppsFlyer / Intercom) and reports the real numbers. Pass → next task. Fail → you get told exactly what's wrong (wrong id, missing attribute, event not arriving). No "looks fine to me".

TaskVerified byPass condition
RN‑1RC + Amplitude + AppsFlyer APIsOne test purchase shows the same user id in all three systems.
RN‑2Amplitude APIWorkout + auth events arrive from real devices with the canonical id; a D1/D7/D30 curve is drawable; a test push yields a push_opened tied to its campaign id.
RN‑3Intercom APIA never-signed-up install is a push-reachable contact; a test push delivers.
RN‑4RevenueCat dashboard + a real deviceA paywall change made in the dashboard appears on a device with NO new build; an Experiment splits traffic across two variants; checkout events still fire.
RN‑5Meta Events Manager + AppsFlyerATE True Status Rate available; ATE volume Quality issue cleared/trending; Meta test install attributes in AF with ATE matching ATT.
RN‑6AF + RC attributes + TMA backendGoogle-test install → register produces media_source/utm + user_id + email + appsflyer_id stitch.
RN‑7Firebase DebugView + AppsFlyerMulti-screen session shows distinct screen events, not only app_open.

Deliverables (sprint-shaped)

Read overview here · code file list on the other tab → sprint-spec.html

Each sprint closes only when the items below are handed over. Hours claimed must be ≤ hard cap (or an approved written CR).

SprintCalendarShipMust hand overCap
A · R1 2 working days
Day1 AM: RN‑5 · rest: RN‑4
RN‑4 + RN‑5 on TestFlight / internal track
  • PR mapped to RN‑4 / RN‑5
  • ATT grant + deny both init AppsFlyer (logs)
  • Paywall dashboard change visible without rebuild
  • Hours ≤ 15h
15h
B · R2 3–4 working days
RN‑1→6→7→3→2
RN‑1 + RN‑2 + RN‑3 + RN‑6 + RN‑7
  • Same user id in Amplitude + AF + RC
  • Google-test stitch payload (utm + user_id + email)
  • 3 distinct screen events in one session
  • Anonymous Intercom push-reachable contact
  • Catalog auth/workout events in Amplitude
  • Email OneLink opens the mapped screen (incl. deferred after install)
  • One-page “what fires where” + hours ≤ 25h core (≤ 28h incl. deep-link add-on)
25h
+3h add-on

Access you'll need

The one-sentence summary

Set one id everywhere (RN‑1), stitch paid install UTMs to email (RN‑6), fire catalog auth/workout + push/review events (RN‑2), register anonymous installs with Intercom (RN‑3), track screens not just opens (RN‑7), make email links open the app to the right screen (RN‑6 deep-link add-on), ship remote paywalls (RN‑4), and fix iOS ATE/ATT so Meta can attribute App Installs (RN‑5) — across two releases: RN‑4+RN‑5 first; RN‑1/2/3/6/7 with the redesign.

10 · Compatibility audit — this brief vs MovementAthlete-RN (v2.2)

Feasibility check of Aga's original asks against the live RN repo. Flag mismatches before the RN team quotes; do not silently invent a second stack.

Brief assumptionRepo realityVerdict
Native Amplitude SDK to fire RN‑2 catalog events No Amplitude dependency today. Client events currently go to Firebase. Amplitude already receives AF/RC connector data. LOCKED Add Amplitude SDK. Funnels & stitching live in Amplitude. Dual-write Firebase OK during transition; do not invent a second funnel taxonomy.
RN‑1: set id on RC + Amplitude + AppsFlyer RC logIn(user.id) already on login; AF setCustomerUserId missing; Firebase setUserId exists but is not called from auth screens; RC $appsflyerId / $amplitudeUserId attributes not set. feasible RN‑1 remains correct; partially done for RC only.
Money events via RC → AF / Meta / Amplitude connectors (not native) Matches TMA direction; native already fires some Firebase purchase helpers but RC is source of truth for store purchases. correct Keep money events out of RN hours.
RN‑4 Paywalls SDK / RevenueCatUI react-native-purchases ^9.9.0 present; no react-native-purchases-ui. Offerings are already fetched (Purchases.getOfferings / RevenueCatOffersService) — remote price path is partly ready; remote paywall UI still needs the UI package. feasible Confirm-pass question still stands; hours lean lower if Offerings stay as-is.
RN‑3 Intercom anonymous + push token Intercom only loginUserWithUserAttributes after auth; no loginUnidentifiedUser. Push (Firebase Messaging) + deep links already present → RN‑3 hours lean small. feasible
RN‑2 catalog names (auth_login_succeeded, workout_started, …) Existing Firebase names differ (signed_in_app, workout_started mixed, etc.). Many analytics services already fire Firebase events. align Either rename to catalog (breaks history) or map in Amplitude / allow-list both — TMA must lock one mapping before build.
UTM on web checkout / GTM Web checkout out of RN scope. Store apps do not run GTM; use AF + Firebase + RC (and Meta/Google partners). correct RN‑6 covers native stitch; web success-page metadata stays internal.
AppsFlyer auto-stamps Meta ATE True only if SDK runs. Current grant-only gate breaks that guarantee → RN‑5. gap Brief now includes RN‑5; original v2.1 omitted this.
Other tracking gaps worth noting (do not expand scope unless TMA asks)

TMA React Native Team Brief v2.4 · 14 Jul 2026 · Companion sprint/code page: sprint-spec.html · Core hour ceiling 40h + deep-link add-on ~2.5h (≈43h all-in) · RN‑5/6/7 + Amplitude decision locked · v2.3 email deep-link add-on folded into RN‑6 · Companion event catalog: TMA-NATIVE-EVENT-CATALOG.html.