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

15 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 no contextual onboarding dialog is 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).
  • Adds SubscriptionPromoUITests covering promo appearance after onboarding and suppression while contextual onboarding dialogs are active.

Testing Steps

Prerequisites

  • Build from alex/subscription-day7-promo.
  • Be an Internal User (add || DEBUG to the #if check in InternalUserStore.swift).
  • 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; 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. Select a search chip; "Try Visiting a Site" appears; hide app, restore; 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!" appears → press home button → switch back to DuckDuckGo → No promo sheet (foregrounding; isShowingContextualOnboardingDialog = true) → force-quit → wait ≥1 min → cold launch
    Day-7 promo sheet appears (tryVisitASiteShown already persisted; dialog won't reappear; isShowingContextualOnboardingDialog = false on fresh 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)

  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

  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)

  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)

  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)


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

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

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

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

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


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 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 #1416.
  • 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

  • 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 isShowingContextualOnboardingDialog is false.
  • 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.