Files
obsidian-vault/work/projects/ios-subscription-day7-promo-pr.md
T

16 KiB
Raw Blame History

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 (add || DEBUG to the #if check in InternalUserStore.swift).
  • Enable both flags in the Internal Settings debug menu: subscriptionPromoForExistingUsers and privacyProOnboardingPromotion.
  • 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 Xcode Stop button or app switcher swipe-up.
  • "Hide app" = press home button / swipe up to background without quitting.
  • Day-7 promo sheet = the "Did you know?" half-sheet (title: "Did you know?", CTA: "Try it free!" or "Learn More", dismiss: "No thanks", close: ✕ icon top-right).

A — Linear onboarding "Search only" path

To reach this state: complete linear onboarding → on the "Want easy access to private AI chat in the address bar?" screen select "Search only" → tap "Next" → complete remaining steps.

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
    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
    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
    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
    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
    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
    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)


B — Linear onboarding "Search & Duck.ai" path — search suggestion variant

To reach this state: complete linear onboarding → on the "Want easy access to private AI chat in the address bar?" screen select "Toggle between Search and Duck.ai" → tap "Next" → complete remaining steps.

Contextual onboarding starts with "Ready to get started? Try a search or AI chat!" dialog showing both search and Duck.ai suggestion chips.

  1. 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

  2. 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)

  3. 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
    Day-7 promo sheet appears (hasSeenOnboarding = true)


C — Linear onboarding "Search & Duck.ai" path — AI suggestion variant

  1. 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
    Day-7 promo sheet appears (before fire step; subscriptionPromotionPending = false)

  2. 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)

  3. 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)

  4. 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
    Day-7 promo sheet appears (hasSeenOnboarding = true)


D — Day-7 promo interaction flows (once promo appears)

  1. 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

  2. Tap "No thanks" (dismiss button); relaunch
    Trigger promo → tap "No thanks" → force-quit → cold launch
    No promo sheet (already shown)

  3. Tap ✕ (close icon); relaunch
    Trigger promo → tap the ✕ icon (top-right) → force-quit → cold launch
    No promo sheet (already shown)

  4. 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)


E — Edge cases / negative flows

  1. 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)

  2. subscriptionPromoForExistingUsers flag disabled
    Disable subscriptionPromoForExistingUsers flag → complete onboarding → wait ≥1 min → cold launch
    No promo sheet

  3. privacyProOnboardingPromotion flag disabled
    Disable privacyProOnboardingPromotion → same setup
    No promo sheet

  4. 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

  5. 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)


F — Other modal prompts not regressed by per-provider gate refactor

  1. 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)

G — Upgrade scenario

  1. 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)

  2. Upgrade from pre-feature build; contextual onboarding incomplete; promo shown 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


Impact and Risks

Impact Level: Medium

What could go wrong?

  • Promo appears during active contextual onboarding — would create two overlapping subscription-related sheets. Partially mitigated by the isEligibleToPresent gate checking subscriptionPromotionPending (covers the EOJ/fire step; flows #7, #12, #16). Known gap: earlier contextual dialogs ("Try a Search", "Try Visiting a Site", fire tutorial) are not yet guarded — the promo currently appears on top of them on cold relaunch. This needs to be fixed before shipping (flows #1, #3, #6, #11, #29 are marked ⚠️).
  • Promo shown twice — duplicate impression if subscriptionPromotionDialogSeen is not persisted correctly. Mitigated by setting the flag in markLaunchPromptPresented() before presenting; covered by flows #1820.
  • 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.
  • 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.