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

176 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
8. **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`)
9. **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`)
10. **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
11. **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)
12. **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`)
13. **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)
14. **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
15. **Dismiss day-7 promo via swipe-down / X; relaunch**
Trigger promo → swipe down or tap X → force-quit → cold launch
**No promo** (already shown)
16. **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
17. **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)
18. **`subscriptionPromoForExistingUsers` flag disabled**
Disable `subscriptionPromoForExistingUsers` in debug menu → complete onboarding → wait ≥1 min → cold launch
**No promo**
19. **`privacyProOnboardingPromotion` flag disabled**
Disable `privacyProOnboardingPromotion` → same setup
**No promo**
20. **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
21. **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
22. **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
23. **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)
24. **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.