Sprint Spec & Code Touchpoints

Companion to the RN Team Brief. File-level instructions against MovementAthlete-RN. Hours below are code-audited locked estimates for a developer who already built this app’s RevenueCat + AppsFlyer plumbing — not greenfield.
v2.2 · 14 Jul 2026 · Locked total hour ceiling for all seven tasks: 40h · Quotes of “2–3 weeks” (~80–120h) without a written delta against the file list below are rejected.
Locked hours — how to read this

Est = honest expected hands-on coding + self-test on a signed build. Hard cap = maximum hours without a written change request that names extra files and why. Store review wait time does not count. Exploring the repo you already built is not a separate week.

Release calendar (suggested)

BlockCalendarTasksEstHard cap
Sprint A · R1 2 working days
not 2 weeks
RN‑5 morning → RN‑4 remainder + TestFlight 13h 15h
Sprint B · R2 3–4 working days
rides redesign build
RN‑1 → RN‑6 → RN‑7 → RN‑3 → RN‑2 22h 25h
All native scope 35h 40h
Decision locked for R2: Amplitude is the client event destination

TMA funnels and cross-source stitching live in Amplitude. Add @amplitude/analytics-react-native (or current RN package TMA specifies). Keep Firebase only where Google Ads / existing calls already need it — do not invent a second funnel taxonomy in Firebase. Catalog event names fire to Amplitude.

SPRINT A · RELEASE 1
Day-by-day — RN‑5 then RN‑4
Est 13h Cap 15h
Day 1 · morning (3h)RN‑5 ATE/ATT fix
  • Edit AppsFlyer always-init + ATT wait; mirror ATE on Facebook SDK if still used
  • Signed iOS build → ATT grant + ATT deny paths both initialise AF
  • Deliverable note: before/after screenshots of AF debug logs
Day 1 · afternoon + Day 2 (10h)RN‑4 RevenueCat Paywalls
  • Add react-native-purchases-ui; present remote paywall on current Offering
  • Wire existing upgrade entry points; keep checkout analytics firing
  • Dashboard change visible on device with no rebuild; two-variant Experiment smoke
SPRINT B · RELEASE 2
Day-by-day — identity → stitch → screens → Intercom → events
Est 22h Cap 25h + deep-link add-on 2.5h / 3h → 24.5h / 28h all-in
Day 1 (5h)RN‑1 (4h) + start RN‑6 (1h)
  • Amplitude SDK init + identify; AF setCustomerUserId; RC attributes; logout clear
  • Start persisting AF conversion payload to AsyncStorage
Day 2 (6h) — finish RN‑6 (3h) + RN‑7 (2h) + RN‑3 (1h kickoff)
  • Stitch on register/login; POST to TMA endpoint (or agreed stub payload)
  • ➕ Deep-link add-on: wire OneLink listener → DeepLinkService (direct + deferred) + register OneLink/ESP domains (Associated Domains / App-Links) — +~2.5h, may spill into Day 3
  • Expo Router screen listener → Amplitude + AF + existing Firebase logScreenView
  • Intercom loginUnidentifiedUser on first launch
Day 3 (8h)RN‑2 auth + workout + push/review events into Amplitude
  • Hook existing fire-points in Auth/Workout/Notification services → Amplitude catalog names
  • Do not redesign Firebase history; dual-write Amplitude is fine for transition
Day 4 morning (3h) — finish RN‑3 priming + signed TestFlight for TMA verify
  • Permission pre-prompt placement proposal; token → Intercom
  • Short “what fires where” note for §9 sign-off

Per-task locked estimates

TaskWhy this isn’t a weekEstHard cap
RN‑5One gate in _layout.tsx + init options + optional FB ATE. No new SDK.2.5h3h
RN‑4Offerings already via getRevenueCatOfferings; only Paywalls UI + present. Cap rises only if SDK bump breaks Android billing (written). 10h12h
RN‑1RC logIn already exists; add AF + Amplitude identify + RC attributes at existing auth hooks.4h5h
RN‑6Extend existing handleAttributionData; call stitch beside Intercom create.3.5h4h
RN‑6 ➕Deep-link add-on: unify OneLink listener with the existing DeepLinkService; associated-domains / App-Links config. Email → right screen, direct + deferred.2.5h3h
RN‑7One navigation listener in root layout; wrappers already exist.2h3h
RN‑3Push + Intercom identified login already present; add anonymous + token + pre-prompt.2h3h
RN‑2Fire-points largely exist in *AnalyticsService.ts; add Amplitude + catalog names + push/review gaps.8h10h

Release 1 — files & functions

RN‑5 · HARD CAP 3h
ATE / ATT / AppsFlyer always-init

Files to touch

FileFunction / regionChange
app/_layout.tsx RootLayoutuseEffect “Initialize AppsFlyer only after ATT…” (~L137–165) Always call initializeAppsFlyer(). Still request ATT. Remove “Skipping AppsFlyer initialization” on deny.
services/AppsFlyerService.ts initializeAppsFlyerAppsFlyerLib.initSdk({…}) Add timeToWaitForATTUserAuthorization: 10 (or 60). Keep listeners.
services/FacebookService.ts initialize / new helper after ATT resolve iOS: Settings.setAdvertiserTrackingEnabled(attGranted) via react-native-fbsdk-next when ATT resolves (for iOS < 17 / Events Manager).

Do NOT

  • Hardcode ATE=false forever
  • Rewrite AppsFlyer from scratch
  • Bill research time as “week of ATT investigation”
// _layout.tsx — target shape (conceptual)
initializeAppsFlyer(); // always
const { status } = await requestTrackingPermissionsAsync();
setFacebookATE(status === 'granted');
RN‑4 · HARD CAP 12h
RevenueCat Paywalls SDK

Already done (do not rebuild)

  • services/RevenueCatService.tsconfigureRevenueCat, logInRevenueCat
  • services/RevenueCatOffersService.tsgetRevenueCatOfferings, offering helpers
  • hooks/useRevenueCatOffers.ts, purchase paths in PaymentService.ts

Files to touch

FileFunction / regionChange
package.json deps Add react-native-purchases-ui (version compatible with purchases ^9.9). Bump purchases only if required for Paywalls.
app/(app)/pages/upgradePremium.tsx purchase / plan render path Present RevenueCat remote Paywall for current Offering (or Offering id from API). Keep existing checkout event hooks.
components/MultipleOffersModal.tsx
components/LastChanceOfferModal.tsx
purchase handlers Same remote paywall / offering present pattern — or route into shared present helper.
New (optional): services/RevenueCatPaywallService.ts presentPaywall() / presentPaywallIfNeeded() Thin wrapper around RevenueCatUI so screens stay thin.
services/PromoAnalyticsService.ts + purchase analytics trackLandedOnPricingPage, trackCheckout, etc. Ensure still fire with catalog/RN‑4 names from new present path.

Daily progress checkpoints (count only if these ship)

  • Hour 0–2: package install + compile both platforms
  • Hour 2–7: present remote paywall on Upgrade Premium
  • Hour 7–10: modals + analytics continuity
  • Hour 10–12: dashboard change without rebuild + Experiment smoke

Release 2 — files & functions

RN‑1 · HARD CAP 5h
Canonical user identity (Amplitude + AF + RC)
FileFunctionChange
New: services/AmplitudeService.ts initAmplitude, identifyUser, resetAmplitudeUser, track Wrap @amplitude/analytics-react-native. API key from TMA.
app/_layout.tsx startup effects beside Intercom / RC initAmplitude() once. When user.id known: identify all SDKs (RC already via logInRevenueCat).
services/AppsFlyerService.ts New exports: setAppsFlyerCustomerUserId, getAppsFlyerUID Call AppsFlyerLib.setCustomerUserId / getAppsFlyerUID.
services/RevenueCatService.ts extend logInRevenueCat or add setRevenueCatAttributionAttributes Purchases.setAttributes({ $amplitudeUserId, $appsflyerId, … })
app/(auth)/login.tsx
app/(auth)/register.tsx
success paths that already call createOrUpdateIntercomUser Same moment: identifyUser(userId), AF customer id, RC attributes. Also AnalyticsService.setUserId (Firebase) if still used.
Logout path (auth slice / logout handler) wherever clearUser / logout runs Purchases.logOut(), Amplitude reset, clear AF customer id.
RN‑6 · HARD CAP 4h  +  deep-link add-on cap 3h
UTM / media_source persist + Google stitch  ·  ➕ email deep-link (OneLink → right screen)
FileFunctionChange
services/AppsFlyerService.ts handleAttributionData Persist full GCD for all sources (not Facebook-only). Keys: media_source, campaign, adset, ad, af_status, utm_*.
same file New: getStoredAttribution, applyAttributionToUser Read storage; setAdditionalData; return payload for backend/RC.
app/(auth)/register.tsx / login.tsx post-success Intercom block Call applyAttributionToUser({ userId, email }) after RN‑1 identify.
API hook (TMA provides URL) POST once { user_id, email, appsflyer_id, media_source, campaign, utm_* }. If endpoint not ready, queue locally + log — don’t invent hours for waiting.
➕ Deep-link add-on (email → app · new 14 Jul · +2.5h / cap 3h) — same OneLink infra + same release, so it lives here
services/AppsFlyerService.tsDeepLinkService onDeepLink / onAppOpenAttribution + onInstallConversionData Replace the console.log with a route call: map deep_link_value → screen via DeepLinkService. Handle deferred (first launch, is_first_launch payload) as well as direct.
ios/**/*.entitlements + app.json (associatedDomains) Associated Domains Add applinks: for the OneLink subdomain AND the ESP click domain (Customer.io / ActiveCampaign). TMA provides both hosts.
android/app/src/main/AndroidManifest.xml App-Links intent filters Add <intent-filter android:autoVerify="true"> for the same two hosts.
route map (TMA provides the table) deep_link_value → screen: workout · resume_assessment · upgrade · update_payment · today. Unknown / empty → app home; never crash.
RN‑7 · HARD CAP 3h
Screen-level tracking
FileFunctionChange
New or services/AppsFlyerService.ts logAppsFlyerScreen(route) appsFlyer.logEvent('af_content_view', { af_content_id, af_content_type:'screen', screen_name })
services/AmplitudeService.ts track('screen_viewed', { screen_name }) or Amplitude screen API Catalog-compatible screen event.
app/_layout.tsx (or small components/ScreenTracker.tsx) Expo Router usePathname / useSegments effect On change → Amplitude + AF + existing logScreenView.

Do not hand-instrument every screen file. One listener covers the app.

RN‑3 · HARD CAP 3h
Intercom anonymous + push token
FileFunctionChange
services/IntercomService.ts New: loginAnonymousIntercom Intercom.loginUnidentifiedUser()
app/_layout.tsx beside initializeIntercom If no authenticated user → anonymous login once.
services/PushNotificationService.ts token registration path (~L62 area) Ensure Intercom.sendTokenToIntercom(token) for anonymous + identified.
Product-approved screen (e.g. after first workout) pre-prompt UI Soft ask before OS permission; fire push_permission_* via RN‑2.
RN‑2 · HARD CAP 10h
Amplitude catalog events (auth + workout + push/review)

Existing Firebase fire-points — extend, don’t rewrite the workout player:

Existing fileExisting function (call site already exists)Add
services/AuthAnalyticsService.ts trackSignedInApp, trackSignedUpMobile, failures/abandon Also amplitude.track('auth_login_succeeded'|…) per catalog
services/WorkoutAnalyticsService.ts trackWorkoutStarted, section finished, abandoned, resumed, … Map to catalog workout_* names in Amplitude
services/NotificationAnalyticsService.ts trackNotificationPermission*, trackNotificationOpened Catalog push_* names + campaign id property
services/AppLifecycleAnalyticsService.ts trackSessionStarted / deep link app_opened with open_source
services/AppRatingService.ts requestReview Fire in_app_review_prompted when prompt shown
Auth / workout screens Only if a catalog event has no existing helper Add one helper + one call — flag in confirm-pass if scatter > expected
10h includes dual-write, not a rewrite

If workout lifecycle is more scattered than these services, flag in the 15‑min confirm-pass and re-quote only the delta files — not “add two weeks”.

Quote & scope rules (TMA ↔ RN team)

These hours replace vague calendar quotes

The repo was audited on 14 Jul 2026. The file lists above are the scope. A quote of “2–3 weeks” for this body of work is rejected unless you produce a written delta: extra files, why they’re needed, and hours per file. Exploring familiar plumbing is included in the estimates — it is not a discovery engagement.

RuleDetail
Hour capsR1 ≤ 15h · R2 ≤ 25h core (≤ 28h incl. deep-link add-on) · All ≤ 40h core / ≈43h all-in.
Counts toward hoursCoding, device self-test, writing the short “what fires where” note, shipping TestFlight / internal track.
Doesn’t countApp Store / Play review wait, Meta/AppsFlyer dashboard clicks owned by TMA, waiting on API keys TMA already committed to provide within 1 business day, re-learning this codebase.
OverageOnly with written change request referencing a new file list and acceptance from TMA before the hours are worked.
Sign-offTMA analytics desk vs live APIs (§9 of brief). “Looks fine locally” ≠ done.
Confirm-passStill the 15‑min questions in the brief. Wrong answers can move RN‑4 toward the high end of the 12h cap — not into multiple weeks.
Deliverable format TMA expects per sprint
  1. PR / commit list mapped to RN‑# task ids
  2. Hours claimed ≤ hard cap (or approved CR)
  3. Signed build link
  4. One-page note: files touched, functions changed, any catalog deviation

Companion to TMA RN Brief v2.2 · index.html · Code-audited against MovementAthlete-RN · 14 Jul 2026