MovementAthlete-RN. Hours below are code-audited locked estimates for a developer who already built this app’s RevenueCat + AppsFlyer plumbing — not greenfield.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.
| Block | Calendar | Tasks | Est | Hard 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 | ||
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.
react-native-purchases-ui; present remote paywall on current OfferingsetCustomerUserId; RC attributes; logout clearDeepLinkService (direct + deferred) + register OneLink/ESP domains (Associated Domains / App-Links) — +~2.5h, may spill into Day 3loginUnidentifiedUser on first launch| Task | Why this isn’t a week | Est | Hard cap |
|---|---|---|---|
| RN‑5 | One gate in _layout.tsx + init options + optional FB ATE. No new SDK. | 2.5h | 3h |
| RN‑4 | Offerings already via getRevenueCatOfferings; only Paywalls UI + present. Cap rises only if SDK bump breaks Android billing (written). | 10h | 12h |
| RN‑1 | RC logIn already exists; add AF + Amplitude identify + RC attributes at existing auth hooks. | 4h | 5h |
| RN‑6 | Extend existing handleAttributionData; call stitch beside Intercom create. | 3.5h | 4h |
| RN‑6 ➕ | Deep-link add-on: unify OneLink listener with the existing DeepLinkService; associated-domains / App-Links config. Email → right screen, direct + deferred. | 2.5h | 3h |
| RN‑7 | One navigation listener in root layout; wrappers already exist. | 2h | 3h |
| RN‑3 | Push + Intercom identified login already present; add anonymous + token + pre-prompt. | 2h | 3h |
| RN‑2 | Fire-points largely exist in *AnalyticsService.ts; add Amplitude + catalog names + push/review gaps. | 8h | 10h |
| File | Function / region | Change |
|---|---|---|
app/_layout.tsx |
RootLayout → useEffect “Initialize AppsFlyer only after ATT…” (~L137–165) |
Always call initializeAppsFlyer(). Still request ATT. Remove “Skipping AppsFlyer initialization” on deny. |
services/AppsFlyerService.ts |
initializeAppsFlyer → AppsFlyerLib.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). |
false forever// _layout.tsx — target shape (conceptual)
initializeAppsFlyer(); // always
const { status } = await requestTrackingPermissionsAsync();
setFacebookATE(status === 'granted');
services/RevenueCatService.ts — configureRevenueCat, logInRevenueCatservices/RevenueCatOffersService.ts — getRevenueCatOfferings, offering helpershooks/useRevenueCatOffers.ts, purchase paths in PaymentService.ts| File | Function / region | Change |
|---|---|---|
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.tsxcomponents/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. |
| File | Function | Change |
|---|---|---|
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.tsxapp/(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. |
| File | Function | Change |
|---|---|---|
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.ts → DeepLinkService |
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. |
| File | Function | Change |
|---|---|---|
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.
| File | Function | Change |
|---|---|---|
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. |
Existing Firebase fire-points — extend, don’t rewrite the workout player:
| Existing file | Existing 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 |
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”.
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.
| Rule | Detail |
|---|---|
| Hour caps | R1 ≤ 15h · R2 ≤ 25h core (≤ 28h incl. deep-link add-on) · All ≤ 40h core / ≈43h all-in. |
| Counts toward hours | Coding, device self-test, writing the short “what fires where” note, shipping TestFlight / internal track. |
| Doesn’t count | App 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. |
| Overage | Only with written change request referencing a new file list and acceptance from TMA before the hours are worked. |
| Sign-off | TMA analytics desk vs live APIs (§9 of brief). “Looks fine locally” ≠ done. |
| Confirm-pass | Still 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. |
Companion to TMA RN Brief v2.2 · index.html · Code-audited against MovementAthlete-RN · 14 Jul 2026