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

13 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 (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

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

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

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

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

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

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

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

  2. Dismiss day-7 promo via swipe-down / X; relaunch
    Trigger promo → swipe down or tap X → force-quit → cold launch
    No promo (already shown)

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

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

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

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

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

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

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

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

  2. 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 #1415.
  • 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.