MovementAthlete-RN, no code changes in this passThese 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.
false in code. There is no call to AppsFlyer or Facebook setAdvertiserTrackingEnabled / equivalent anywhere. Worse: iOS skips AppsFlyer init entirely unless ATT = granted (app/_layout.tsx → initializeAppsFlyer only on grant). Denied / undetermined users never send AppsFlyer launches with ATE=false, so Meta never gets a usable ATE distribution. Fix = RN‑5.services/AppsFlyerService.ts calls initSdk without timeToWaitForATTUserAuthorization, never calls setCustomerUserId, never logs custom/screen events, and only mirrors Facebook (not Google) install attribution into Firebase once.media_source / UTM + AppsFlyer UID + email into one place. RC logIn(user.id) exists; AF customer id + RC $appsflyerId / UTM attributes do not. Partial overlap with RN‑1; UTM persist / stitch = RN‑6.logScreenView but it is only used in examples/hooks — not wired to navigation. Trial/subscribe/purchase visibility exists; Install → Purchase page path does not. Fix = RN‑7.package.json — client analytics go to Firebase Analytics. Amplitude is expected to ingest via AppsFlyer + RevenueCat connectors (see §10). Do not invent a second event taxonomy without an explicit decision.These replace vague “2–3 week” quotes. Detail: day plans, file/function lists → sprint-spec.html. Hard caps are the hour ceiling per task.
| Task | What | Release | Est | Hard cap |
|---|---|---|---|---|
| RN‑5 | ATE / ATT + AppsFlyer always-init | R1 | 2.5h | 3h |
| RN‑4 | RevenueCat Paywalls (Offerings already exist) | R1 | 10h | 12h |
| RN‑1 | Canonical id — Amplitude + AF + RC attrs | R2 | 4h | 5h |
| RN‑6 | UTM persist + Google stitch | R2 | 3.5h | 4h |
| RN‑6 ➕ | Deep-link add-on — email OneLink opens the right screen (direct + deferred) | R2 | 2.5h | 3h |
| RN‑7 | Screen tracker (one listener) | R2 | 2h | 3h |
| RN‑3 | Intercom anonymous + push token | R2 | 2h | 3h |
| RN‑2 | Amplitude catalog events (extend existing services) | R2 | 8h | 10h |
| Sprint A · R1 — ~2 working days | 13h | 15h | ||
| Sprint B · R2 — ~3–4 working days (core) | 22h | 25h | ||
| All native scope (core) | 35h | 40h | ||
| All-in incl. deep-link add-on | 37.5h | 43h | ||
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.
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:
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.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.
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.
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.
af_start_trial, af_purchase, af_subscribe, rc_*). These are delivered server-side by the RevenueCat → AppsFlyer / Meta / Amplitude connectors, which TMA configures in the RevenueCat dashboard. Your RN‑1 (+ RN‑6 attribute) work is precisely what makes those connectors attach to real users — but you do not write the money events themselves.purchase, dedup, UTM/source segmentation, checkout.success metadata for Stripe) — internal web developer. This includes the email→revenue UTM capture — persist email utm_* + recipient into Stripe metadata at checkout → "revenue per email" (Nic, no release · master brief Task 27). In-app purchase attribution metadata is RN‑6 (RC subscriber attributes / AF additional data), not web GTM.checkout.themovementathlete.com — Stripe/web and internal. RN‑4 concerns the in-app paywall only.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.
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 capability | Needs a release? | Owner | Notes |
|---|---|---|---|
| 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 — once | You — 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. |
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.
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 work | Build when | Examples |
|---|---|---|
| Screen-INDEPENDENT survives any redesign | Now — this brief | Canonical 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 flow | Onboarding- 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.
user_id. One id, used in every SDK. Confirm the exact field with TMA before you wire it.Purchases.logIn(userId) // RevenueCat amplitude.setUserId(userId) // Amplitude setCustomerUserId(userId) // AppsFlyer
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.)
Purchases.logOut(), reset the Amplitude user id, and clear the AppsFlyer customer id on logout.TMA-NATIVE-EVENT-CATALOG.html.push_permission_requested / push_permission_granted / push_permission_denied — the 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.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.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.push_opened tied to its campaign id. (TMA's desk confirms via the Amplitude API.)Intercom.loginUnidentifiedUser()
Intercom.sendTokenToIntercom(token)With Expo, the
@intercom/intercom-react-native config plugin wires the native side automatically — confirm it's present.
Intercom.loginUserWithUserAttributes with the canonical id) so the two don't double-count.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..p8 key; Android Firebase service-account JSON) — not your hours. Your code just needs the token registered.RevenueCatUI (Paywalls)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.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.app/_layout.tsx, services/AppsFlyerService.ts, services/FacebookService.tstimeToWaitForATTUserAuthorization.initializeAppsFlyer() on iOS and Android — remove the “ATT not granted → skip AppsFlyer” branch in _layout.tsx.timeToWaitForATTUserAuthorization (e.g. 10–60s) into initSdk so the first launch waits for the ATT answer before the AppsFlyer launch/install ping.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.AppsFlyerService + auth register/login hooks · builds on RN‑1 (does not replace it)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.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.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
{ 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.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.)utm/media_source + user_id + email + appsflyer_id) visible in AF + RC attributes + TMA backend export.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.
AppsFlyerService (OneLink listener) + existing DeepLinkService / router · iOS Associated Domains entitlement · Android App-Links intent filtersconsole.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).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.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.AppsFlyerService / AnalyticsService.logScreenViewapp_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.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).
screen_name as AF / RC additional data so purchase context shows last screen before checkout.app_open).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_session_started · auth_login_succeeded · auth_login_failed · auth_register_succeeded · auth_register_failed · auth_logout · auth_forgot_password_requested · auth_mode_switched
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_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.
subscription_checkout_screen_opened · begin_checkout · payment_initiated · subscription_checkout_confirmed
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.
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.
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".
| Task | Verified by | Pass condition |
|---|---|---|
| RN‑1 | RC + Amplitude + AppsFlyer APIs | One test purchase shows the same user id in all three systems. |
| RN‑2 | Amplitude API | Workout + 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‑3 | Intercom API | A never-signed-up install is a push-reachable contact; a test push delivers. |
| RN‑4 | RevenueCat dashboard + a real device | A 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‑5 | Meta Events Manager + AppsFlyer | ATE True Status Rate available; ATE volume Quality issue cleared/trending; Meta test install attributes in AF with ATE matching ATT. |
| RN‑6 | AF + RC attributes + TMA backend | Google-test install → register produces media_source/utm + user_id + email + appsflyer_id stitch. |
| RN‑7 | Firebase DebugView + AppsFlyer | Multi-screen session shows distinct screen events, not only app_open. |
Each sprint closes only when the items below are handed over. Hours claimed must be ≤ hard cap (or an approved written CR).
| Sprint | Calendar | Ship | Must hand over | Cap |
|---|---|---|---|---|
| A · R1 | 2 working days Day1 AM: RN‑5 · rest: RN‑4 |
RN‑4 + RN‑5 on TestFlight / internal track |
|
15h |
| B · R2 | 3–4 working days RN‑1→6→7→3→2 |
RN‑1 + RN‑2 + RN‑3 + RN‑6 + RN‑7 |
|
25h +3h add-on |
proj4a978c73), Amplitude (US region), AppsFlyer, Intercom.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.
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 assumption | Repo reality | Verdict |
|---|---|---|
| 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. |
console.logs today — unify it with DeepLinkService so email OneLinks open the right screen (direct + deferred), and register the OneLink + ESP click domains in Associated Domains / App-Links. Fits the RN‑6 test plan.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.