diff --git a/work/projects/ios-subscription-day7-promo-pr.md b/work/projects/ios-subscription-day7-promo-pr.md new file mode 100644 index 00000000..63589bcf --- /dev/null +++ b/work/projects/ios-subscription-day7-promo-pr.md @@ -0,0 +1,175 @@ +Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1214798984829406 +Tech Design URL: N/A +CC: N/A + +### Description + +- Adds `SubscriptionPromoExistingUserCoordinator` — a new launch-prompt coordinator targeting existing users (7+ days since install) who completed linear onboarding normally and have never seen a subscription offer. Shows a half-sheet with a CTA to the purchase flow using the `funnel_modal_ios__subscriptionnudge` funnel origin. +- Adds `subscriptionPromoForExistingUsers` remote feature flag (backed by `PrivacyProSubfeature.subscriptionPromoForExistingUsers`), guarded additionally by the existing `privacyProOnboardingPromotion` gate. +- Adds `SubscriptionFunnelOrigin.existingUserPromo` (`funnel_modal_ios__subscriptionnudge`) and three pixel events: `m_privacy-pro_existing_user_promotion_impression`, `…_tap`, `…_dismiss`. +- Refactors the per-provider onboarding gate: removes the global `hasSeenOnboarding` guard from `ModalPromptCoordinationService` and replaces it with a per-provider `isEligibleToPresent(isOnboardingComplete: Bool) -> Bool` method on `ModalPromptProvider`. The manager resolves the onboarding state once and passes it to each provider, keeping a single source of truth. +- `SubscriptionPromoExistingUserCoordinator` implements a softer gate: eligible when onboarding is complete **or** the contextual subscription promo is no longer pending — allowing single-tab users who never reached the fire/EOJ flow to see the launch promo. +- All other providers (`WinBackOffer`, `DefaultBrowser`, `NewAddressBarPicker`, `WhatsNew`) use the default implementation, which returns the passed `isOnboardingComplete` value — preserving their previous behaviour without each owning an `onboardingStatusProvider` dependency. +- Inserts `subscriptionPromoExistingUser` at priority 3 in the modal prompt queue (after reinstaller promo, before address bar picker). + +### Testing Steps + +**Prerequisites** +- Build from `alex/subscription-day7-promo`. +- Be an Internal User (Settings → DuckDuckGo Settings → long-press version). +- Enable both flags via the Internal Settings debug menu: `privacyProOnboardingPromotion` and `subscriptionPromoForExistingUsers`. +- The cooldown is currently overridden to **1 minute** from install date for easier manual testing (production will require 7 days). +- "Stop app" = force-quit via app switcher. "Hide app" = home button / swipe up to background without quitting. + +--- + +#### A — Linear onboarding "Search only" path + +1. **Stop at SERP Dax dialog; wait ≥1 min; cold launch** + Complete linear onboarding ("Search only" → "Continue" through all steps) → first search triggers SERP Dax dialog → force-quit app → wait ≥1 min → cold launch + → **Promo sheet should appear** (single-tab state: `subscriptionPromotionPending = false`, `hasSeenOnboarding = false`, cooldown passed) + +2. **Stop at SERP Dax dialog; hide app; reopen (foreground)** + Same setup → background app instead of quitting → switch back to DuckDuckGo + → **No promo** (not a cold launch) + +3. **Tap "Got It" on SERP Dax dialog; stop app; wait ≥1 min; cold launch** + Complete linear onboarding "Search only" → first search → SERP Dax dialog → tap "Got It" → force-quit → wait ≥1 min → cold launch + → **Promo sheet should appear** (fire tutorial not yet done; `subscriptionPromotionPending = false`) + +4. **Tap "Got It" on SERP dialog; fire icon tutorial shows; stop app; wait ≥1 min; cold launch** + Same as above → do NOT press the fire button → force-quit → cold launch + → **Promo sheet should appear** (not yet in EOJ flow) + +5. **Complete fire button step; contextual subscription promo is pending; stop app; cold launch** + Complete linear "Search only" → SERP dialog → "Got It" → fire tutorial → press fire button → contextual EOJ subscription promo dialog appears → force-quit without interacting → wait ≥1 min → cold launch + → **No promo** (`subscriptionPromotionPending = true`; launch promo blocked to avoid clash with contextual flow) + +6. **Complete contextual subscription promo via CTA; stop app; cold launch** + Same as #5 but fully complete the contextual subscription promo (tap "Try it Free" / "Learn More") → force-quit → cold launch + → **Promo sheet should appear** (`hasSeenOnboarding = true`; launch promo has not been shown yet) + +7. **Dismiss contextual subscription promo via X; stop app; cold launch** + Same as #5 but dismiss via X button on the contextual subscription promo → force-quit → cold launch + → **Promo sheet should appear** (`hasSeenOnboarding = true`) + +--- + +#### B — Linear onboarding "Search & Duck.ai" path — search suggestion variant + +8. **Select search suggestion on SERP; tap X on SERP Dax dialog; stop app; cold launch** + Complete linear onboarding "Search & Duck.ai" → first search → select a search suggestion → SERP Dax dialog appears → tap X → force-quit → wait ≥1 min → cold launch + → **Promo sheet should appear** (before fire step; `subscriptionPromotionPending = false`) + +9. **Select search suggestion; complete SERP dialog + duck.ai dialog; fire pending; stop app; cold launch** + Complete linear "Search & Duck.ai" → SERP suggestion → "Got It" on SERP dialog → duck.ai suggestion dialog → continue → fire tutorial → press fire → contextual EOJ subscription promo appears → force-quit → cold launch + → **No promo** (`subscriptionPromotionPending = true`) + +10. **Select search suggestion; complete full contextual onboarding including EOJ promo; stop app; cold launch** + Same as #9 but fully complete EOJ subscription promo (CTA or X) → force-quit → cold launch + → **Promo sheet should appear** (`hasSeenOnboarding = true`) + +--- + +#### C — Linear onboarding "Search & Duck.ai" path — AI suggestion variant + +11. **Select AI suggestion on SERP; tap X on duck.ai dialog; stop app; cold launch** + Complete linear "Search & Duck.ai" → first search → select AI/duck.ai suggestion → duck.ai dialog → tap X → force-quit → wait ≥1 min → cold launch + → **Promo sheet should appear** (before fire step) + +12. **Select AI suggestion; complete duck.ai dialog; fire pending; stop app; cold launch** + Complete linear "Search & Duck.ai" → AI suggestion → duck.ai contextual dialog → "Got It" → fire tutorial → press fire → EOJ subscription promo appears → force-quit → cold launch + → **No promo** (`subscriptionPromotionPending = true`) + +13. **Select AI suggestion; complete full contextual onboarding; stop app; cold launch** + Same as #12 but fully complete EOJ subscription promo → force-quit → cold launch + → **Promo sheet should appear** + +--- + +#### D — Promo interaction flows (once promo appears) + +14. **Tap CTA on day-7 promo; verify destination; relaunch** + Trigger promo (any flow above) → tap "Try it Free" (or "Learn More" if no free trial) → subscription purchase/info page opens → force-quit → cold launch + → Subscription page opens with `funnel_modal_ios__subscriptionnudge` origin; **promo does NOT appear again** on next launch + +15. **Dismiss day-7 promo via swipe-down / X; relaunch** + Trigger promo → swipe down or tap X → force-quit → cold launch + → **No promo** (already shown) + +16. **Background app while promo is showing; reopen** + Trigger promo → background app (don't dismiss) → reopen + → Promo should still be visible (or gracefully dismissed); **no second promo on next cold launch** + +--- + +#### E — Edge cases / negative flows + +17. **Cooldown not passed: immediate cold launch after install** + Fresh install → enable flags → complete linear + contextual onboarding immediately → cold launch (< 1 min) + → **No promo** (cooldown not met) + +18. **`subscriptionPromoForExistingUsers` flag disabled** + Disable `subscriptionPromoForExistingUsers` in debug menu → complete onboarding → wait ≥1 min → cold launch + → **No promo** + +19. **`privacyProOnboardingPromotion` flag disabled** + Disable `privacyProOnboardingPromotion` → same setup + → **No promo** + +20. **WinBack-eligible user does not see day-7 promo** + Set up a user who qualifies for the WinBack offer AND the day-7 promo → cold launch + → **WinBack offer appears** (higher priority); day-7 promo stays pending for future launch + +21. **Reinstaller promo eligible user does not see day-7 promo** + User who skipped linear onboarding (`isDismissed = true`) + reinstaller flag enabled → cold launch + → **Reinstaller promo appears** (higher priority) + +--- + +#### F — Other modal prompts not affected by per-provider gate refactor + +22. **Default Browser prompt still respects onboarding gate** + User who has NOT completed contextual onboarding (`hasSeenOnboarding = false`); no subscription promo pending → cold launch + → **No Default Browser prompt** (default `isEligibleToPresent` requires onboarding complete) + *(Validates per-provider gate applies to other providers correctly)* + +--- + +#### G — Upgrade scenario + +23. **Upgrade from pre-feature build; onboarding complete; promo shown on first launch** + Install build at commit `0f064b1e66` → complete linear + contextual onboarding (including EOJ fire + subscription promo dialog) → update app to `alex/subscription-day7-promo` with flags enabled → cold launch (≥1 min since original install date) + → **Day-7 promo sheet appears** (`hasSeenOnboarding = true` carried over; `subscriptionPromotionDialogSeen` false on new property; cooldown already passed) + +24. **Upgrade from pre-feature build; onboarding incomplete; promo shown on first launch** + Install build at commit `0f064b1e66` → complete linear onboarding only (do not complete contextual dialogs) → update to `alex/subscription-day7-promo` → cold launch (≥1 min since install) + → **Day-7 promo sheet appears** (`subscriptionPromotionPending = false` since user never reached fire step; cooldown passed) + +--- + +### Impact and Risks + +**Impact Level: Medium** + +#### What could go wrong? + +- **Promo appears during active contextual onboarding** — would create two overlapping subscription-related sheets. Mitigated by the `isEligibleToPresent` gate checking `subscriptionPromotionPending`; covered by testing flows #5, #9, #12. +- **Promo shown twice** — duplicate impression if `subscriptionPromotionDialogSeen` is not persisted correctly. Mitigated by setting the flag in `markLaunchPromptPresented()` before presenting; covered by flows #14–15. +- **Other modal prompts regressed by gate refactor** — existing providers could break if the per-provider default gate behaves differently from the removed global gate. The default `isEligibleToPresent` returns `isOnboardingComplete`, which is functionally equivalent; covered by flow #22. +- **Cooldown bypassed by date manipulation** — low risk for production; the debug override (1-min) must be reverted before shipping. + +### Quality Considerations + +- **Privacy**: No new user data collected. Pixels carry only `returningUser` and `freeTrial` boolean parameters, consistent with existing subscription promo pixels. +- **Performance**: Eligibility check is synchronous and lightweight; no network calls added to the launch critical path. +- **Feature flag gating**: Double-gated (`subscriptionPromoForExistingUsers` + `privacyProOnboardingPromotion`), enabling gradual rollout and instant kill-switch. +- **Cooldown override**: The 1-minute override in `hasCooldownPassed()` must be reverted to the 7-day logic (`cooldownDays = 7`) before the flag is enabled in production. The TODO comment marks it clearly. +- **Funnel origin**: Uses a new distinct origin value (`funnel_modal_ios__subscriptionnudge`) so this promo's conversion can be tracked independently from the reinstaller and skipped-onboarding promos. +- **Test coverage**: `SubscriptionPromoExistingUserCoordinatorTests` covers eligibility logic, pixel firing, CTA navigation, and dismiss handling. + +### Notes to Reviewer + +- The `isEligibleToPresent(isOnboardingComplete:)` protocol method replaces the old global service-level `hasSeenOnboarding` guard. The net behaviour for existing providers is identical — they now check onboarding state per-provider via the default implementation rather than a single service gate. The only change in observable behaviour is that `SubscriptionPromoExistingUserCoordinator` can present to users who haven't fully completed contextual onboarding, as long as the contextual subscription promo is not pending. +- `hasAppStoreProductsAvailable` check is commented out in `shouldPresentLaunchPrompt()` — intentional for the current testing phase; evaluate whether to re-enable before rollout. +- The reinstaller promo coordinator (`SubscriptionPromoCoordinator`) and the existing-user coordinator share `SubscriptionPromoCoordinating` and `SubscriptionPromoModalPromptProvider`, keeping the sheet UI consistent.