16 KiB
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 thefunnel_modal_ios__subscriptionnudgefunnel origin. - Adds
subscriptionPromoForExistingUsersremote feature flag (backed byPrivacyProSubfeature.subscriptionPromoForExistingUsers), guarded additionally by the existingprivacyProOnboardingPromotiongate. - 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
hasSeenOnboardingguard fromModalPromptCoordinationServiceand replaces it with a per-providerisEligibleToPresent(isOnboardingComplete: Bool) -> Boolmethod onModalPromptProvider. The manager resolves the onboarding state once and passes it to each provider, keeping a single source of truth. SubscriptionPromoExistingUserCoordinatorimplements 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 passedisOnboardingCompletevalue — preserving their previous behaviour without each owning anonboardingStatusProviderdependency. - Inserts
subscriptionPromoExistingUserat 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
|| DEBUGto the#ifcheck inInternalUserStore.swift). - Enable both flags in the Internal Settings debug menu:
subscriptionPromoForExistingUsersandprivacyProOnboardingPromotion. - 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.
-
"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 -
"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) -
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 -
"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) -
"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) -
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 -
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) -
"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; alsosubscriptionPromotionPending = true) -
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) -
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.
-
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 -
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) -
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
-
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) -
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) -
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) -
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)
-
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 withfunnel_modal_ios__subscriptionnudgeorigin; no promo sheet on next launch -
Tap "No thanks" (dismiss button); relaunch
Trigger promo → tap "No thanks" → force-quit → cold launch
→ No promo sheet (already shown) -
Tap ✕ (close icon); relaunch
Trigger promo → tap the ✕ icon (top-right) → force-quit → cold launch
→ No promo sheet (already shown) -
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
-
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) -
subscriptionPromoForExistingUsersflag disabled
DisablesubscriptionPromoForExistingUsersflag → complete onboarding → wait ≥1 min → cold launch
→ No promo sheet -
privacyProOnboardingPromotionflag disabled
DisableprivacyProOnboardingPromotion→ same setup
→ No promo sheet -
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 -
Reinstaller-eligible user: reinstaller promo takes priority
User who skipped linear onboarding (tap X → "Start Browsing") +subscriptionPromoForReinstallersenabled → cold launch
→ Reinstaller promo sheet appears (higher priority)
F — Other modal prompts not regressed by per-provider gate refactor
- Default Browser prompt does not appear before onboarding is complete
User who has NOT completed contextual onboarding (hasSeenOnboarding = false,subscriptionPromotionPending = false,subscriptionPromoForExistingUsersdisabled to prevent that promo) → cold launch
→ No Default Browser prompt (defaultisEligibleToPresentstill requires onboarding complete)
(Validates the per-provider gate refactor hasn't changed behaviour for other providers)
G — Upgrade scenario
-
Upgrade from pre-feature build; onboarding complete; promo shown on first launch after update
Install build at commit0f064b1e66→ complete linear + contextual onboarding all the way through the "Oh, before I forget..." contextual promo → update app toalex/subscription-day7-promowith flags enabled → cold launch (install date is ≥1 min ago)
→ Day-7 promo sheet appears (hasSeenOnboarding = truecarried over;subscriptionPromotionDialogSeen= false since it's a new UserDefaults key; cooldown already passed) -
Upgrade from pre-feature build; contextual onboarding incomplete; promo shown on first launch after update
Install build at commit0f064b1e66→ complete linear onboarding only (stop before completing contextual dialogs) → update toalex/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
isEligibleToPresentgate checkingsubscriptionPromotionPending(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
subscriptionPromotionDialogSeenis not persisted correctly. Mitigated by setting the flag inmarkLaunchPromptPresented()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
isEligibleToPresentreturnsisOnboardingComplete, 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
returningUserandfreeTrialboolean 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:
SubscriptionPromoExistingUserCoordinatorTestscovers eligibility logic, pixel firing, CTA navigation, and dismiss handling.
Notes to Reviewer
- The
isEligibleToPresent(isOnboardingComplete:)protocol method replaces the old global service-levelhasSeenOnboardingguard. 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 thatSubscriptionPromoExistingUserCoordinatorcan present to users who haven't fully completed contextual onboarding, as long as the contextual subscription promo is not pending. hasAppStoreProductsAvailablecheck is commented out inshouldPresentLaunchPrompt()— intentional for the current testing phase; evaluate whether to re-enable before rollout.- The reinstaller promo coordinator (
SubscriptionPromoCoordinator) and the existing-user coordinator shareSubscriptionPromoCoordinatingandSubscriptionPromoModalPromptProvider, keeping the sheet UI consistent.