Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
131 changes: 131 additions & 0 deletions .agent-artifacts/ui-evidence/sca-2171-cancel-flow-parity/evidence.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
{
"version": 1,
"images": [
{
"file": "001-web-plans-confirm-before-desktop.png",
"platform": "web",
"description": "Before: the web PlansSheet cancel path is a plain confirm dialog with no reason and no offer (desktop width).",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "002-web-plans-confirm-before-mobile.png",
"platform": "web",
"description": "Before: the same plain confirm dialog at 390px width.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "003-web-plans-entry-before-desktop.png",
"platform": "web",
"description": "Before: Cancel Subscription in the PlansSheet opens the plain confirm; entry point label and position unchanged.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "004-web-plans-entry-after-desktop.png",
"platform": "web",
"description": "After: the same Cancel Subscription button opens the reason-first flow (flag on).",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "005-web-cancel-reason-desktop.png",
"platform": "web",
"description": "After: web reason step, seven mobile reasons, Continue disabled until one is chosen.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "006-web-cancel-feedback-desktop.png",
"platform": "web",
"description": "After: web feedback step for too_expensive with the mobile title, subtitle and 300-character field.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "007-web-cancel-offer-desktop.png",
"platform": "web",
"description": "After: web consequences step with the existing 50%-off-3-months stay offer and the destructive confirm.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "008-web-cancel-done-desktop.png",
"platform": "web",
"description": "After: web success state after the reason-carrying DELETE succeeds.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "009-web-cancel-no-offer-desktop.png",
"platform": "web",
"description": "After: web offer-ineligible path, Keep Subscription primary and Cancel Subscription still reachable.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "010-web-cancel-reason-mobile.png",
"platform": "web",
"description": "After: web reason step at 390px width.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "011-web-cancel-offer-mobile.png",
"platform": "web",
"description": "After: web offer step at 390px width.",
"captured_by": "agent",
"source": "playwright-e2e"
},
{
"file": "012-desktop-macos-manage-only-before.png",
"platform": "desktop-macos",
"description": "Before: the macOS current-plan card exposes Manage only; cancellation is the Stripe portal. Composed from the shipped card controls.",
"captured_by": "agent",
"source": "visual-audit"
},
{
"file": "013-desktop-macos-cancel-entry-after.png",
"platform": "desktop-macos",
"description": "After: the new Cancel Subscription row under the plan card, one click from the plan. Composed from the shipped card controls.",
"captured_by": "agent",
"source": "visual-audit"
},
{
"file": "014-desktop-macos-cancel-reason.png",
"platform": "desktop-macos",
"description": "After: macOS reason step rendered from the shipped SwiftUI sheet.",
"captured_by": "agent",
"source": "visual-audit"
},
{
"file": "015-desktop-macos-cancel-feedback.png",
"platform": "desktop-macos",
"description": "After: macOS feedback step for too_expensive.",
"captured_by": "agent",
"source": "visual-audit"
},
{
"file": "016-desktop-macos-cancel-offer.png",
"platform": "desktop-macos",
"description": "After: macOS consequences step with the existing stay offer and the destructive confirm.",
"captured_by": "agent",
"source": "visual-audit"
},
{
"file": "017-desktop-macos-cancel-no-offer.png",
"platform": "desktop-macos",
"description": "After: macOS offer-ineligible path, Keep Subscription primary and Cancel Subscription still reachable.",
"captured_by": "agent",
"source": "visual-audit"
},
{
"file": "018-desktop-macos-cancel-done.png",
"platform": "desktop-macos",
"description": "After: macOS success state after the reason-carrying DELETE succeeds.",
"captured_by": "agent",
"source": "visual-audit"
}
]
}
1 change: 1 addition & 0 deletions backend/docs/feature-flag-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ and an explicit empty literal renders as `''`.
| `VITE_ENABLE_GMAIL_SESSION` | Keep shipped Gmail session integration enabled | windows | build_define | open | — | — | — | — | keep | keep | — | unowned |
| `X-Omi-Memory-Default-Delete-Supported` | Indicate default-memory deletion support | backend, macos | server_capability | closed | — | — | — | — | keep | keep | — | unowned |
| `X-Omi-Memory-Device-Scope-Supported` | Indicate device-scoped memory query support | backend, macos, windows | server_capability | closed | — | — | — | — | keep | keep | — | unowned |
| `cancel_flow_parity` | Default off; web and macOS cancellations ask why and show the existing retention offer before confirming | web-app, macos | posthog | closed | — | — | — | absent (enable) | keep | keep | — | Goal: Churn / SCA-2171 |
| `mobile-chat-apps-all-channels` | Omi store only: private chat channel endpoints for David; prevents the public Telegram rule from exposing iMessage/WhatsApp. Default off, explicit allowlist only; backend channel admission remains authoritative | mobile | posthog | closed | — | — | — | absent (enable) | keep | keep | — | auditfix |
| `watch_copy_dedupe_off` | True keeps and uploads Watch recovery copies; false deduplicates copies covered at least 95% by server-reachable live custody | mobile | posthog | closed | — | — | — | expected (kill) | keep | keep | — | uicleanup |

Expand Down
1 change: 1 addition & 0 deletions config/feature-flags.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
flags:
- {"key":"cancel_flow_parity","aliases":[],"kind":"posthog","lifecycle":"config_switch","surfaces":["web-app","macos"],"summary":"Default off; web and macOS cancellations ask why and show the existing retention offer before confirming","fail":"closed","owner":"Goal: Churn / SCA-2171","created":"2026-11-14","terminal":"keep","decision":"keep","posthog":{"row":"absent","role":"enable"},"notes":"Off until David records approval of the renders and copy; absent, unlisted or failed reads keep today\u2019s plain web confirm and macOS Manage-only paths. The flag never gates the cancel entry point. On adds the mobile reason/feedback/offer steps and emits the five existing cancel events with platform=web|macos. No price, plan, ceiling, mobile-flow or backend change."}
- {"key":"BATCH_PIPELINE_FRESH_GUARD_ENABLED","aliases":[],"kind":"env","lifecycle":"ops_kill","surfaces":["backend"],"summary":"Default on; off/false/0/no stops fresh-delivery count queries and hides all fresh guard gauges. No scheduling or result-stamp effect.","fail":"open","owner":"Goal: Pipeline","created":"2026-11-13","terminal":"keep","decision":"keep","notes":"SCA-2012 fresh-first step 1. Measurement only: fresh commits in the 10\u201320 minute window and their pending subset on the maintenance leader once per minute. Missing/failed/empty samples never produce a healthy ratio. The merge stamp is unconditional. backend/docs/listen_pusher_pipeline/changes/2026-11-13-fresh-delivery-guard.md"}
- {"key":"STT_FREE_ALLOWANCE_WATCH_ENABLED","aliases":[],"kind":"env","lifecycle":"ops_kill","surfaces":["backend"],"summary":"Default on; shared free transcription shadow reservations and settlement observations, with no serving admission effect","fail":"open","owner":"margin/SCA-1750","created":"2026-11-12","terminal":"keep","decision":"keep","notes":"false/0/off/no stops new observations and reservations; confirmed billing operations can still settle. The core ledger has no cohort/launch-date gate. Existing serving admission remains authoritative. Request/renewal durations must not be summed as settled-overage proof. Full-day aggregate reconciliation is required before enforcement. See backend/docs/listen_pusher_pipeline/changes/2026-11-12-free-allowance-watch.md."}
- {"key":"BATCH_PIPELINE_UPLOAD_COALESCING_ENABLED","aliases":[],"kind":"env","lifecycle":"config_switch","surfaces":["backend"],"summary":"Default OFF; only true admits durable pre-unit upload holds. OFF drains accepted holds individually and retains compatible status/erasure readers.","fail":"closed","owner":"margin / SCA-1800","created":"2026-11-12","terminal":"keep","decision":"keep","notes":"No activation in this PR. One target/session/device/policy/privacy context; <=60s fixed release deadline. Runtime values and staging/quality gates remain unqualified. See backend/docs/runbooks/batch-upload-coalescing.md."}
Expand Down
5 changes: 5 additions & 0 deletions desktop/macos/Desktop/Sources/APIClient.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Foundation

Check warning on line 1 in desktop/macos/Desktop/Sources/APIClient.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

desktop/macos/Desktop/Sources/APIClient.swift is 1479 lines; consider splitting files over 800 lines.
import OmiWAL

private struct DesktopPublicWebSearchRequest: Encodable {
Expand Down Expand Up @@ -502,6 +502,7 @@

func delete(
_ endpoint: String,
body: Data? = nil,
requireAuth: Bool = true,
customBaseURL: String? = nil,
includeBYOK: Bool = true,
Expand All @@ -524,6 +525,10 @@
includeBYOK: includeBYOK,
expectedAuthOwnerId: authOwnerId
)
if let body {
request.httpBody = body
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
try validateExpectedOwner(effectiveAuthPolicy)

try await performVoidRequest(request, authPolicy: effectiveAuthPolicy)
Expand Down
54 changes: 54 additions & 0 deletions desktop/macos/Desktop/Sources/AnalyticsManager.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import AppKit

Check warning on line 1 in desktop/macos/Desktop/Sources/AnalyticsManager.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

desktop/macos/Desktop/Sources/AnalyticsManager.swift is 1758 lines; consider splitting files over 800 lines.
import Foundation

/// Closed reason a presented notification left the screen. Auto-hide and explicit
Expand Down Expand Up @@ -1641,6 +1641,45 @@
PostHogManager.shared.track("Upgrade Cancelled")
}

// MARK: - Cancel flow parity (SCA-2171)
//
// Same five wire names as mobile; the only addition is `platform`, which the
// macOS super properties already register as "macos" and is emitted here
// explicitly so the web/macOS/mobile cancel-reason rate is segmentable.

private var cancelFlowProperties: [String: Any] { ["platform": "macos"] }

func subscriptionCancelFlowStarted() {
PostHogManager.shared.track("Subscription Cancel Flow Started", properties: cancelFlowProperties)
}

func subscriptionCancelReasonSelected(reason: String?) {
var properties = cancelFlowProperties
properties["reason"] = SubscriptionFunnelWire.cancelReason(reason)
PostHogManager.shared.track("Subscription Cancel Reason Selected", properties: properties)
}

func subscriptionCancelConfirmed(reason: String?, hasDetails: Bool) {
var properties = cancelFlowProperties
properties["reason"] = SubscriptionFunnelWire.cancelReason(reason)
properties["has_details"] = hasDetails
PostHogManager.shared.track("Subscription Cancel Confirmed", properties: properties)
}

func subscriptionCancelKeptPlan(step: Int, reason: String?) {
var properties = cancelFlowProperties
properties["step"] = step
properties["reason"] = SubscriptionFunnelWire.cancelReason(reason)
PostHogManager.shared.track("Subscription Cancel Kept Plan", properties: properties)
}

func subscriptionCancelAbandoned(step: Int, reason: String?) {
var properties = cancelFlowProperties
properties["step"] = step
properties["reason"] = SubscriptionFunnelWire.cancelReason(reason)
PostHogManager.shared.track("Subscription Cancel Abandoned", properties: properties)
}

}

enum SubscriptionFunnelWire {
Expand Down Expand Up @@ -1701,4 +1740,19 @@
"billing_interval": interval(billingInterval),
]
}

/// Closed cancel-reason vocabulary shared with mobile; absent/unknown reasons
/// keep the contract's `none`/`unknown` buckets instead of a free string.
static func cancelReason(_ raw: String?) -> String {
let value = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
switch value {
case "too_expensive", "not_using_enough", "missing_features", "audio_quality", "battery_drain",
"found_alternative", "other":
return value
case "none":
return "none"
default:
return "unknown"
}
}
}
Loading
Loading