diff --git a/work/projects/ios-subscription-day7-promo-pr.md b/work/projects/ios-subscription-day7-promo-pr.md index 9bebd2a9..94755765 100644 --- a/work/projects/ios-subscription-day7-promo-pr.md +++ b/work/projects/ios-subscription-day7-promo-pr.md @@ -8,7 +8,7 @@ CC: N/A - 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. +- `SubscriptionPromoExistingUserCoordinator` implements a softer gate: eligible when onboarding is complete **or** no contextual onboarding dialog is either pending (`isStillOnboarding`) or currently visible on screen (`isShowingContextualOnboardingDialog`) — allowing single-tab users who never reached the fire/EOJ flow to see the launch promo, while preventing the promo from appearing on top of active onboarding dialogs on cold launch. - 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). @@ -31,45 +31,33 @@ To reach this state: complete linear onboarding → on the *"Want easy access to Contextual onboarding then starts on the browser. The *"Ready to get started? Try a search!"* dialog appears with search suggestion chips. -1. **"Try a Search" dialog visible; stop app; wait ≥1 min; cold launch** - On the *"Ready to get started? Try a search!"* dialog, do not tap any chip → force-quit → wait ≥1 min → cold launch +1. **"Try a Search" dialog visible; hide app, restore; stop app, cold launch — no promo both times** + On the *"Ready to get started? Try a search!"* dialog, do not tap any chip → press home button → switch back to DuckDuckGo → **No promo sheet** (foregrounding) → force-quit → wait ≥1 min → cold launch → **No promo sheet** -2. **"Try a Search" dialog visible; hide app; reopen (foreground)** - Same as above → press home button instead of quitting → switch back to DuckDuckGo - → **No promo sheet** (foregrounding, not a cold launch) - -3. **Select a search chip; "That's DuckDuckGo Search!" appears; tap "Got it!"; "Try Visiting a Site" appears; stop app; cold launch** - On *"Ready to get started? Try a search!"* select a search suggestion chip → SERP loads → *"That's DuckDuckGo Search!"* dialog → tap **"Got it!"** → *"Next, try visiting a site!"* dialog appears → force-quit → wait ≥1 min → cold launch +2. **Select a search chip; "Try Visiting a Site" appears; hide app, restore; stop app, cold launch — no promo both times** + On *"Ready to get started? Try a search!"* select a search suggestion chip → SERP loads → *"That's DuckDuckGo Search!"* dialog → tap **"Got it!"** → *"Next, try visiting a site!"* appears → press home button → switch back to DuckDuckGo → **No promo sheet** (foregrounding) → force-quit → wait ≥1 min → cold launch → **No promo sheet** -4. **"Try Visiting a Site" dialog visible; hide app; reopen (foreground)** - Same as above (up to *"Next, try visiting a site!"* dialog visible) → press home button → switch back to DuckDuckGo - → **No promo sheet** (foregrounding, not a cold launch) - -5. **"Try Visiting a Site" dialog visible; tap X; stop app; cold launch** +3. **"Try Visiting a Site" dialog visible; tap X; stop app; cold launch** On *"Next, try visiting a site!"* tap the **X** (skip/close) → confirm skip if prompted → force-quit → wait ≥1 min → cold launch → **Day-7 promo sheet appears** (fire tutorial not started; `subscriptionPromotionPending = false`) -6. **Complete "Try Visiting a Site"; fire button tutorial shows; stop app; cold launch** +4. **Complete "Try Visiting a Site"; fire button tutorial shows; stop app; cold launch** Visit a site from the *"Try Visiting a Site"* step → tracker dialog → **"Got it!"** → fire button is highlighted by tutorial → force-quit without tapping fire → wait ≥1 min → cold launch → **No promo sheet** -7. **Press 🔥 fire button; contextual subscription promo "Oh, before I forget..." appears; stop app; cold launch** - Follow fire tutorial → tap the 🔥 **Fire button** → fire confirmation → contextual subscription promo *"Oh, before I forget..."* appears → force-quit without interacting → wait ≥1 min → cold launch +5. **Press 🔥 fire button; contextual subscription promo "Oh, before I forget..." appears; hide app, restore; stop app, cold launch — no promo both times** + Follow fire tutorial → tap the 🔥 **Fire button** → fire confirmation → contextual subscription promo *"Oh, before I forget..."* appears → press home button → switch back to DuckDuckGo → **No promo sheet** (foregrounding; `subscriptionPromotionPending = true`) → force-quit → wait ≥1 min → cold launch → **No promo sheet** (`subscriptionPromotionPending = true`; launch promo blocked to avoid clash with contextual flow) -8. **"Oh, before I forget..." contextual promo visible; hide app; reopen (foreground)** - Same as above (contextual subscription promo visible on screen) → press home button → switch back to DuckDuckGo - → **No promo sheet** (foregrounding; also `subscriptionPromotionPending = true`) - -9. **Tap "Try it free!" / "Learn More" on "Oh, before I forget..."; stop app; cold launch** +6. **Tap "Try it free!" / "Learn More" on "Oh, before I forget..."; stop app; cold launch** On the *"Oh, before I forget..."* contextual promo, tap **"Try it free!"** (or **"Learn More"**) → force-quit → wait ≥1 min → cold launch → **Day-7 promo sheet appears** (`hasSeenOnboarding = true`; launch promo not yet shown) -10. **Tap "No thanks" on "Oh, before I forget..."; stop app; cold launch** - On the *"Oh, before I forget..."* contextual promo, tap **"No thanks"** → force-quit → wait ≥1 min → cold launch - → **Day-7 promo sheet appears** (`hasSeenOnboarding = true`) +7. **Tap "No thanks" on "Oh, before I forget..."; stop app; cold launch** + On the *"Oh, before I forget..."* contextual promo, tap **"No thanks"** → force-quit → wait ≥1 min → cold launch + → **Day-7 promo sheet appears** (`hasSeenOnboarding = true`) --- @@ -79,55 +67,51 @@ To reach this state: complete linear onboarding → on the *"Want easy access to Contextual onboarding starts with *"Ready to get started? Try a search or AI chat!"* dialog showing both search and Duck.ai suggestion chips. -11. **Select a search chip; "Try Visiting a Site" appears; stop app; cold launch** - On *"Ready to get started? Try a search or AI chat!"* select a **search suggestion chip** → SERP loads → *"That's DuckDuckGo Search!"* → *"Next, try visiting a site!"* dialog → force-quit → wait ≥1 min → cold launch - → **No promo sheet** +8. **Select a search chip; "Try Visiting a Site" appears; stop app; cold launch** + On *"Ready to get started? Try a search or AI chat!"* select a **search suggestion chip** → SERP loads → *"That's DuckDuckGo Search!"* → *"Next, try visiting a site!"* dialog → force-quit → wait ≥1 min → cold launch + → **No promo sheet** -12. **Select a search chip; complete contextual onboarding to fire step; contextual promo pending; stop app; cold launch** - Select a search chip → complete *"Try Visiting a Site"* → fire tutorial → tap 🔥 Fire button → *"Oh, before I forget..."* appears → force-quit → cold launch - → **No promo sheet** (`subscriptionPromotionPending = true`) +9. **Select a search chip; complete contextual onboarding to fire step; contextual promo pending; stop app; cold launch** + Select a search chip → complete *"Try Visiting a Site"* → fire tutorial → tap 🔥 Fire button → *"Oh, before I forget..."* appears → force-quit → cold launch + → **No promo sheet** (`subscriptionPromotionPending = true`) -13. **Select a search chip; complete full contextual onboarding including EOJ promo; stop app; cold launch** - Same as #12 but fully complete the contextual subscription promo (tap **"Try it free!"**, **"Learn More"**, or **"No thanks"**) → force-quit → cold launch +10. **Select a search chip; complete full contextual onboarding including EOJ promo; stop app; cold launch** + Same as #9 but fully complete the contextual subscription promo (tap **"Try it free!"**, **"Learn More"**, or **"No thanks"**) → force-quit → cold launch → **Day-7 promo sheet appears** (`hasSeenOnboarding = true`) --- #### C — Linear onboarding "Search & Duck.ai" path — AI suggestion variant -14. **Select a Duck.ai chip; duck.ai dialog appears; tap X; stop app; cold launch** - On *"Ready to get started? Try a search or AI chat!"* select a **Duck.ai suggestion chip** → Duck.ai opens → contextual duck.ai dialog appears → tap **X** → force-quit → wait ≥1 min → cold launch +11. **Select a Duck.ai chip; dialog visible; hide app, restore; tap X; stop app, cold launch** + On *"Ready to get started? Try a search or AI chat!"* select a **Duck.ai suggestion chip** → Duck.ai opens → contextual duck.ai dialog appears → press home button → switch back to DuckDuckGo → **No promo sheet** (foregrounding) → tap **X** → force-quit → wait ≥1 min → cold launch → **Day-7 promo sheet appears** (before fire step; `subscriptionPromotionPending = false`) -15. **Duck.ai contextual dialog visible; hide app; reopen (foreground)** - Same as above (duck.ai contextual dialog visible) → press home button → switch back to DuckDuckGo - → **No promo sheet** (foregrounding, not a cold launch) - -16. **Select a Duck.ai chip; complete duck.ai dialogs; fire pending; stop app; cold launch** +12. **Select a Duck.ai chip; complete duck.ai dialogs; fire pending; stop app; cold launch** Select Duck.ai chip → complete duck.ai contextual dialogs ("Got it!") → fire tutorial → tap 🔥 Fire button → *"Oh, before I forget..."* appears → force-quit → cold launch → **No promo sheet** (`subscriptionPromotionPending = true`) -17. **Select a Duck.ai chip; complete full contextual onboarding; stop app; cold launch** - Same as #16 but fully complete the contextual subscription promo → force-quit → cold launch +13. **Select a Duck.ai chip; complete full contextual onboarding; stop app; cold launch** + Same as #12 but fully complete the contextual subscription promo → force-quit → cold launch → **Day-7 promo sheet appears** (`hasSeenOnboarding = true`) --- #### D — Day-7 promo interaction flows (once promo appears) -18. **Tap "Try it free!" / "Learn More" CTA; verify destination; relaunch** +14. **Tap "Try it free!" / "Learn More" CTA; verify destination; relaunch** Trigger the day-7 promo (any flow above) → on the *"Did you know?"* sheet tap **"Try it free!"** (or **"Learn More"**) → Privacy Pro subscription page opens → force-quit → cold launch → Subscription page opened with `funnel_modal_ios__subscriptionnudge` origin; **no promo sheet on next launch** -19. **Tap "No thanks" (dismiss button); relaunch** +15. **Tap "No thanks" (dismiss button); relaunch** Trigger promo → tap **"No thanks"** → force-quit → cold launch → **No promo sheet** (already shown) -20. **Tap ✕ (close icon); relaunch** +16. **Tap ✕ (close icon); relaunch** Trigger promo → tap the **✕ icon** (top-right) → force-quit → cold launch → **No promo sheet** (already shown) -21. **Hide app while promo is visible; reopen; then cold launch** +17. **Hide app while promo is visible; reopen; then cold launch** Trigger promo → press home button without dismissing → switch back to DuckDuckGo → force-quit → cold launch → Promo was visible on reopen; **no promo sheet on next cold launch** (already shown) @@ -135,23 +119,23 @@ Contextual onboarding starts with *"Ready to get started? Try a search or AI cha #### E — Edge cases / negative flows -22. **Cooldown not passed: launch immediately after install** +18. **Cooldown not passed: launch immediately after install** Fresh install → enable flags → complete linear + contextual onboarding → cold launch immediately (< 1 min since install) → **No promo sheet** (cooldown not met) -23. **`subscriptionPromoForExistingUsers` flag disabled** +19. **`subscriptionPromoForExistingUsers` flag disabled** Disable `subscriptionPromoForExistingUsers` flag → complete onboarding → wait ≥1 min → cold launch → **No promo sheet** -24. **`privacyProOnboardingPromotion` flag disabled** +20. **`privacyProOnboardingPromotion` flag disabled** Disable `privacyProOnboardingPromotion` → same setup → **No promo sheet** -25. **WinBack-eligible user: WinBack promo takes priority** +21. **WinBack-eligible user: WinBack promo takes priority** Set up a user qualifying for both WinBack offer and day-7 promo → cold launch → **WinBack offer sheet appears** (higher priority); day-7 promo stays pending for the next launch after WinBack is dismissed -26. **Reinstaller-eligible user: reinstaller promo takes priority** +22. **Reinstaller-eligible user: reinstaller promo takes priority** User who skipped linear onboarding (tap X → "Start Browsing") + `subscriptionPromoForReinstallers` enabled → cold launch → **Reinstaller promo sheet appears** (higher priority) @@ -159,7 +143,7 @@ Contextual onboarding starts with *"Ready to get started? Try a search or AI cha #### F — Other modal prompts not regressed by per-provider gate refactor -27. **Default Browser prompt does not appear before onboarding is complete** +23. **Default Browser prompt does not appear before onboarding is complete** User who has NOT completed contextual onboarding (`hasSeenOnboarding = false`, `subscriptionPromotionPending = false`, `subscriptionPromoForExistingUsers` disabled to prevent that promo) → cold launch → **No Default Browser prompt** (default `isEligibleToPresent` still requires onboarding complete) *(Validates the per-provider gate refactor hasn't changed behaviour for other providers)* @@ -168,11 +152,11 @@ Contextual onboarding starts with *"Ready to get started? Try a search or AI cha #### G — Upgrade scenario -28. **Upgrade from pre-feature build; onboarding complete; promo shown on first launch after update** +24. **Upgrade from pre-feature build; onboarding complete; promo shown on first launch after update** Install build at commit `0f064b1e66` → complete linear + contextual onboarding all the way through the *"Oh, before I forget..."* contextual promo → update app to `alex/subscription-day7-promo` with flags enabled → cold launch (install date is ≥1 min ago) → **Day-7 promo sheet appears** (`hasSeenOnboarding = true` carried over; `subscriptionPromotionDialogSeen` = false since it's a new UserDefaults key; cooldown already passed) -29. **Upgrade from pre-feature build; contextual onboarding incomplete; promo shown on first launch after update** +25. **Upgrade from pre-feature build; contextual onboarding incomplete; no promo on first launch after update** Install build at commit `0f064b1e66` → complete linear onboarding only (stop before completing contextual dialogs) → update to `alex/subscription-day7-promo` → cold launch (install date ≥1 min ago) → **No promo sheet** @@ -184,9 +168,9 @@ Contextual onboarding starts with *"Ready to get started? Try a search or AI cha #### 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 #7, #12, #16. -- **Promo shown twice** — duplicate impression if `subscriptionPromotionDialogSeen` is not persisted correctly. Mitigated by setting the flag in `markLaunchPromptPresented()` before presenting; covered by flows #18–20. -- **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 #27. +- **Promo appears during active contextual onboarding** — would create two overlapping subscription-related sheets. Mitigated by the `isEligibleToPresent` gate checking `isStillOnboarding() || isShowingContextualOnboardingDialog`; 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–16. +- **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 #23. - **Cooldown bypassed by date manipulation** — low risk for production; the debug override (1-min) must be reverted before shipping. ### Quality Considerations @@ -200,6 +184,6 @@ Contextual onboarding starts with *"Ready to get started? Try a search or AI cha ### 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. +- 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 neither `isStillOnboarding()` nor `isShowingContextualOnboardingDialog` is true. - `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.