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). - Adds
SubscriptionPromoUITestscovering 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
|| DEBUGto the#ifcheck inInternalUserStore.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.
-
"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;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 checkingisShowingContextualOnboardingDialog; 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 asisShowingContextualOnboardingDialogis false. 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.