[2026-07-10] eagle: work/projects/ios-subscription-day7-promo-pr.md
This commit is contained in:
@@ -17,132 +17,163 @@ CC: N/A
|
||||
**Prerequisites**
|
||||
- Build from `alex/subscription-day7-promo`.
|
||||
- Be an Internal User (add `|| DEBUG` to the `#if` check in `InternalUserStore.swift`).
|
||||
- Enable both flags in the Internal Settings debug menu: `subscriptionPromoForExistingUsers` and `privacyProOnboardingPromotion`.
|
||||
- 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. "Hide app" = home button / swipe up to background without quitting.
|
||||
- **"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
|
||||
|
||||
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)
|
||||
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.
|
||||
|
||||
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)
|
||||
Contextual onboarding then starts on the browser. The *"Ready to get started? Try a search!"* dialog appears with search suggestion chips.
|
||||
|
||||
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`)
|
||||
1. **"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
|
||||
→ **Day-7 promo sheet appears** (`subscriptionPromotionPending = false`, `hasSeenOnboarding = false`, cooldown passed)
|
||||
|
||||
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)
|
||||
2. **"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)
|
||||
|
||||
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)
|
||||
3. **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
|
||||
→ **Day-7 promo sheet appears** (fire tutorial not yet done; `subscriptionPromotionPending = false`)
|
||||
|
||||
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)
|
||||
4. **"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)
|
||||
|
||||
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`)
|
||||
5. **"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`)
|
||||
|
||||
6. **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
|
||||
→ **Day-7 promo sheet appears** (not yet in EOJ flow; `subscriptionPromotionPending = false`)
|
||||
|
||||
7. **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)
|
||||
|
||||
8. **"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; also `subscriptionPromotionPending = true`)
|
||||
|
||||
9. **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)
|
||||
|
||||
10. **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
|
||||
|
||||
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`)
|
||||
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.
|
||||
|
||||
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`)
|
||||
Contextual onboarding starts with *"Ready to get started? Try a search or AI chat!"* dialog showing both search and Duck.ai suggestion chips.
|
||||
|
||||
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`)
|
||||
11. **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
|
||||
→ **Day-7 promo sheet appears** (before fire step; `subscriptionPromotionPending = false`)
|
||||
|
||||
12. **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`)
|
||||
|
||||
13. **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
|
||||
|
||||
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)
|
||||
14. **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`)
|
||||
|
||||
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`)
|
||||
15. **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)
|
||||
|
||||
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**
|
||||
16. **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`)
|
||||
|
||||
17. **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 — Promo interaction flows (once promo appears)
|
||||
#### D — Day-7 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
|
||||
18. **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 with `funnel_modal_ios__subscriptionnudge` origin; **no promo sheet 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)
|
||||
19. **Tap "No thanks" (dismiss button); relaunch**
|
||||
Trigger promo → tap **"No thanks"** → force-quit → cold launch
|
||||
→ **No promo sheet** (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**
|
||||
20. **Tap ✕ (close icon); relaunch**
|
||||
Trigger promo → tap the **✕ icon** (top-right) → force-quit → cold launch
|
||||
→ **No promo sheet** (already shown)
|
||||
|
||||
21. **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
|
||||
|
||||
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)
|
||||
22. **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)
|
||||
|
||||
18. **`subscriptionPromoForExistingUsers` flag disabled**
|
||||
Disable `subscriptionPromoForExistingUsers` in debug menu → complete onboarding → wait ≥1 min → cold launch
|
||||
→ **No promo**
|
||||
23. **`subscriptionPromoForExistingUsers` flag disabled**
|
||||
Disable `subscriptionPromoForExistingUsers` flag → complete onboarding → wait ≥1 min → cold launch
|
||||
→ **No promo sheet**
|
||||
|
||||
19. **`privacyProOnboardingPromotion` flag disabled**
|
||||
24. **`privacyProOnboardingPromotion` flag disabled**
|
||||
Disable `privacyProOnboardingPromotion` → same setup
|
||||
→ **No promo**
|
||||
→ **No promo sheet**
|
||||
|
||||
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
|
||||
25. **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
|
||||
|
||||
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)
|
||||
26. **Reinstaller-eligible user: reinstaller promo takes priority**
|
||||
User who skipped linear onboarding (tap X → "Start Browsing") + `subscriptionPromoForReinstallers` enabled → cold launch
|
||||
→ **Reinstaller promo sheet appears** (higher priority)
|
||||
|
||||
---
|
||||
|
||||
#### F — Other modal prompts not affected by per-provider gate refactor
|
||||
#### F — Other modal prompts not regressed 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)*
|
||||
27. **Default Browser prompt does not appear before onboarding is complete**
|
||||
User who has NOT completed contextual onboarding (`hasSeenOnboarding = false`, `subscriptionPromotionPending = false`, `subscriptionPromoForExistingUsers` disabled to prevent that promo) → cold launch
|
||||
→ **No Default Browser prompt** (default `isEligibleToPresent` still requires onboarding complete)
|
||||
*(Validates the per-provider gate refactor hasn't changed behaviour for other providers)*
|
||||
|
||||
---
|
||||
|
||||
#### 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)
|
||||
28. **Upgrade from pre-feature build; onboarding complete; promo shown on first launch after update**
|
||||
Install build at commit `0f064b1e66` → complete linear + contextual onboarding all the way through the *"Oh, before I forget..."* contextual promo → update app to `alex/subscription-day7-promo` with flags enabled → cold launch (install date is ≥1 min ago)
|
||||
→ **Day-7 promo sheet appears** (`hasSeenOnboarding = true` carried over; `subscriptionPromotionDialogSeen` = false since it's a new UserDefaults key; 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)
|
||||
29. **Upgrade from pre-feature build; contextual onboarding incomplete; promo shown on first launch after update**
|
||||
Install build at commit `0f064b1e66` → complete linear onboarding only (stop before completing contextual dialogs) → update to `alex/subscription-day7-promo` → cold launch (install date ≥1 min ago)
|
||||
→ **Day-7 promo sheet appears** (`subscriptionPromotionPending = false` since user never reached fire step; cooldown passed)
|
||||
|
||||
---
|
||||
@@ -153,9 +184,9 @@ CC: N/A
|
||||
|
||||
#### 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 #14–15.
|
||||
- **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.
|
||||
- **Promo appears during active contextual onboarding** — would create two overlapping subscription-related sheets. Mitigated by the `isEligibleToPresent` gate checking `subscriptionPromotionPending`; covered by testing flows #7, #12, #16.
|
||||
- **Promo shown twice** — duplicate impression if `subscriptionPromotionDialogSeen` is not persisted correctly. Mitigated by setting the flag in `markLaunchPromptPresented()` 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 `isEligibleToPresent` returns `isOnboardingComplete`, 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
|
||||
|
||||
Reference in New Issue
Block a user