15 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 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 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; 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 -
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 (tryVisitASiteShownalready persisted; dialog won't reappear;isStillOnboarding() = false,isShowingContextualOnboardingDialog = falseon fresh 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; 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) -
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 #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
-
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) -
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 #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)
-
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; no promo 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. Mitigated by the
isEligibleToPresentgate checkingisStillOnboarding() || isShowingContextualOnboardingDialog; covered by testing flows #5, #9, #12. - Promo shown twice — duplicate impression if
subscriptionPromotionDialogSeenis not persisted correctly. Mitigated by setting the flag inmarkLaunchPromptPresented()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
isEligibleToPresentreturnsisOnboardingComplete, 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
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 neitherisStillOnboarding()norisShowingContextualOnboardingDialogis true. 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.