Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
b4625c4
test(ios): cover pending push opt-out
azooz2003-bit Aug 13, 2026
e7c9a6a
fix(ios): update push toggle optimistically
azooz2003-bit Aug 13, 2026
d2d527a
test(ios): make push toggle timing assertion deterministic
azooz2003-bit Aug 13, 2026
952586e
fix(ios): restore toast settings binding
azooz2003-bit Aug 13, 2026
207491f
fix(ios): remove retired toast settings toggle
azooz2003-bit Aug 13, 2026
63ed21a
fix(ios): initialize task composer setting
azooz2003-bit Aug 13, 2026
12cf0c5
fix(ios): tie push toggle mutation to view lifecycle
azooz2003-bit Aug 13, 2026
653a43d
fix(ios): bound push toggle mutations
azooz2003-bit Aug 13, 2026
a162e51
refactor(ios): isolate push preview mutation gate
azooz2003-bit Aug 13, 2026
6b69895
fix(ios): surface uncertain push toggle outcomes
azooz2003-bit Aug 13, 2026
7f4f7d3
Merge remote-tracking branch 'origin/main' into fix-ios-push-toggle-off
azooz2003-bit Aug 13, 2026
5f4c064
fix(ios): bound push readiness reconciliation
azooz2003-bit Aug 13, 2026
4492cf1
fix(ios): serialize timed out push mutations
azooz2003-bit Aug 13, 2026
57bef7d
fix(ios): serialize push retries after reconciliation
azooz2003-bit Aug 13, 2026
e679e16
fix(ios): own push preference mutation in coordinator
azooz2003-bit Aug 13, 2026
5bc3c10
fix(ios): preempt stale push registration work
azooz2003-bit Aug 13, 2026
09868c8
test(push): require serialized opt-out mutation
azooz2003-bit Aug 13, 2026
4a5301d
test(push): cover ordered opt-out intents
azooz2003-bit Aug 14, 2026
90a97c1
fix(push): serialize notification intent mutations
azooz2003-bit Aug 14, 2026
8bde795
test(push): cover latest intent ordering
azooz2003-bit Aug 14, 2026
6300751
fix(push): make latest notification intent authoritative
azooz2003-bit Aug 14, 2026
9f86b86
test(push): deduplicate same-generation activation
azooz2003-bit Aug 14, 2026
4379d55
fix(push): coalesce repeated registration intents
azooz2003-bit Aug 14, 2026
d86f774
fix(push): coalesce pending notification intents
azooz2003-bit Aug 14, 2026
88b75b0
test(push): recover disabled startup cleanup
azooz2003-bit Aug 14, 2026
ebcdf1c
fix(push): recover interrupted opt-out cleanup
azooz2003-bit Aug 14, 2026
0b2802b
test(push): persist opt-out before cleanup
azooz2003-bit Aug 14, 2026
9177de8
fix(push): atomically commit coordinator intent
azooz2003-bit Aug 14, 2026
87da95b
refactor(push): align service package conventions
azooz2003-bit Aug 14, 2026
7c7f86f
test(push): cover startup ownership and stale snapshots
azooz2003-bit Aug 14, 2026
0e9a3fa
fix(push): fence in-flight cleanup and stale snapshots
azooz2003-bit Aug 14, 2026
d5818e9
test(push): cover reenable generation during cleanup
azooz2003-bit Aug 14, 2026
cdffcba
fix(push): submit every current enable generation
azooz2003-bit Aug 14, 2026
15270dc
test(push): preserve enable after caller cancellation
azooz2003-bit Aug 14, 2026
5f2c42b
fix(push): let intent drains outlive caller cancellation
azooz2003-bit Aug 14, 2026
66b9763
test(push): preserve absent preference on startup
azooz2003-bit Aug 14, 2026
9c94426
fix(push): require explicit opt-out for startup cleanup
azooz2003-bit Aug 14, 2026
f5c9664
test(push): preserve denied reenable generation
azooz2003-bit Aug 14, 2026
6bdd586
fix(push): propagate denied enable generation
azooz2003-bit Aug 14, 2026
b93b7e0
fix(push): unify preference mutation ordering
azooz2003-bit Aug 14, 2026
e8a3df1
chore(push): isolate intent kind type
azooz2003-bit Aug 14, 2026
845883c
test(push): cancel queued mutation waiter
azooz2003-bit Aug 14, 2026
886a28c
fix(push): drop cancelled mutation waiters
azooz2003-bit Aug 14, 2026
18c5584
test(push): accept initial coordinator generation
azooz2003-bit Aug 14, 2026
abe6ac9
fix(push): accept generation zero startup intent
azooz2003-bit Aug 14, 2026
be49424
fix(push): recheck cancellation at gate handoff
azooz2003-bit Aug 14, 2026
bf861ab
test(push): recover cancelled queued registration
azooz2003-bit Aug 14, 2026
a9d3eec
fix(push): recover cancelled registration attempts
azooz2003-bit Aug 14, 2026
c2c06d7
test(push): fix cancellation regression setup
azooz2003-bit Aug 14, 2026
59ded95
test(push): bound stalled settings mutation
azooz2003-bit Aug 14, 2026
cb19142
fix(push): bound settings mutation lifetime
azooz2003-bit Aug 14, 2026
8631125
chore(push): isolate timeout outcome type
azooz2003-bit Aug 14, 2026
fa2d641
test(push): retry timed out enable intent
azooz2003-bit Aug 14, 2026
15f23dd
fix(push): permit retry after settings timeout
azooz2003-bit Aug 14, 2026
81f03ee
test(push): cover late prompt and worker cancellation
azooz2003-bit Aug 14, 2026
72d7ee6
fix(push): reconcile late authorization and cancel workers
azooz2003-bit Aug 14, 2026
7c6ef3c
test(push): isolate mutation timeout generations
azooz2003-bit Aug 14, 2026
f1582d5
test(push): preserve queued sign-out cleanup on cancellation
azooz2003-bit Aug 14, 2026
84bd0cc
fix(push): persist sign-out cleanup before mutation gate
azooz2003-bit Aug 14, 2026
62a72a7
test(push): cover stale registration and public enable timeout
azooz2003-bit Aug 14, 2026
197fb04
fix(push): fence direct registration commits and bound enables
azooz2003-bit Aug 14, 2026
015f353
test(push): fence opt-out cleanup to persisted owner
azooz2003-bit Aug 14, 2026
a441e85
fix(push): require persisted owner for opt-out cleanup
azooz2003-bit Aug 14, 2026
eb29f93
test(push): seed cleanup owner in queued sign-out
azooz2003-bit Aug 14, 2026
8546b3d
fix(push): reject unproven legacy cleanup credentials
azooz2003-bit Aug 14, 2026
75937e4
fix(push): store boolean settings mutation task
azooz2003-bit Aug 14, 2026
87c0033
test(push): cover stalled intent and timeout worker ownership
azooz2003-bit Aug 14, 2026
25ba6fd
fix(push): retain bounded mutation lanes until completion
azooz2003-bit Aug 14, 2026
1130e64
refactor(push): keep intent lane state flat
azooz2003-bit Aug 14, 2026
f3b831f
test(push): require timed-out intent to reach service
azooz2003-bit Aug 14, 2026
faa340e
fix(push): commit intents before bounded reconciliation
azooz2003-bit Aug 14, 2026
1f9c96d
test(push): require bounded timeout recovery
azooz2003-bit Aug 14, 2026
6e98cee
fix(push): bound timeout recovery lanes
azooz2003-bit Aug 14, 2026
0cfa6da
test(push): cover late intent and timeout winner races
azooz2003-bit Aug 14, 2026
6832304
fix(push): fence late intents and timeout races
azooz2003-bit Aug 14, 2026
5528198
test(push): cover startup drain and same-lane recovery
azooz2003-bit Aug 14, 2026
01fdd8e
fix(push): bound same-lane recovery and startup cleanup
azooz2003-bit Aug 14, 2026
6019041
test(push): wait for same-lane recovery state
azooz2003-bit Aug 14, 2026
61b1afc
test(push): require fresh bounded recovery after timeout
azooz2003-bit Aug 14, 2026
9b0f659
fix(push): bound cached registration recovery
azooz2003-bit Aug 14, 2026
890b6b9
refactor(push): isolate intent worker type
azooz2003-bit Aug 14, 2026
86c89f5
test(push): protect newer registration from stale sign-out
azooz2003-bit Aug 14, 2026
e599af9
fix(push): order sign-out with newer registration
azooz2003-bit Aug 14, 2026
c33317a
refactor(push): use one reconciliation worker
azooz2003-bit Aug 14, 2026
d7ebaae
test(ios): recover timed-out push opt-out
azooz2003-bit Aug 14, 2026
8a8fb0b
fix(ios): recover timed-out push cleanup
azooz2003-bit Aug 14, 2026
8a982c1
Merge remote-tracking branch 'origin/main' into fix-ios-push-toggle-off
azooz2003-bit Aug 14, 2026
c9cc7c1
refactor(ios): reduce push toggle fix scope
azooz2003-bit Aug 14, 2026
4666aa7
test(ios): cover pending push opt-out
azooz2003-bit Aug 14, 2026
ed6e5e0
fix(ios): update push toggle optimistically
azooz2003-bit Aug 14, 2026
dc37932
test(ios): signal pending push mutation
azooz2003-bit Aug 14, 2026
253e670
fix(ios): keep push toggle interactive
azooz2003-bit Aug 14, 2026
2f0e084
test(ios): bound pending push fixture
azooz2003-bit Aug 14, 2026
1834813
test(ios): isolate push fixture state
azooz2003-bit Aug 14, 2026
71818d7
fix(ios): serialize push toggle mutations
azooz2003-bit Aug 14, 2026
e6d722d
fix(ios): preserve queued push intent
azooz2003-bit Aug 14, 2026
38ffb32
fix(ios): honor resolved push state
azooz2003-bit Aug 14, 2026
9aae5b3
test(ios): cover superseded push intent
azooz2003-bit Aug 14, 2026
c9be524
fix(ios): supersede stale push intents
azooz2003-bit Aug 14, 2026
3607cc8
fix(ios): reconcile started push mutations
azooz2003-bit Aug 14, 2026
1fa4230
refactor(push): own intent reconciliation
azooz2003-bit Aug 14, 2026
9800bdd
test(push): require immediate opt-out cleanup
azooz2003-bit Aug 14, 2026
d34c247
fix(push): let opt-outs bypass stale work
azooz2003-bit Aug 14, 2026
d9a4ea5
fix(push): authorize before backend reconciliation
azooz2003-bit Aug 14, 2026
c32c631
test(push): bound pending cleanup storage
azooz2003-bit Aug 14, 2026
4364c2d
fix(push): bound pending cleanup state
azooz2003-bit Aug 14, 2026
708bc6b
fix(push): preserve overflow cleanup obligations
azooz2003-bit Aug 15, 2026
48730ca
Merge origin/main into fix-ios-push-toggle-off
azooz2003-bit Aug 15, 2026
3f6a681
fix(push): page durable cleanup overflow
azooz2003-bit Aug 15, 2026
0c443b5
fix(push): discard stale cleanup index entries
azooz2003-bit Aug 15, 2026
3a25ecf
fix(push): continue paged cleanup drains
azooz2003-bit Aug 15, 2026
d3841ca
fix(push): supersede direct cleanup retries
azooz2003-bit Aug 15, 2026
8dc9926
fix(push): preserve cleanup intent across retries
azooz2003-bit Aug 15, 2026
5146847
fix(push): preserve generation through cleanup recovery
azooz2003-bit Aug 15, 2026
d212b57
fix(push): index durable cleanup obligations
azooz2003-bit Aug 15, 2026
d1ef41b
fix(push): make cleanup migration transactional
azooz2003-bit Aug 15, 2026
b4ff551
fix(push): close notification intent races
azooz2003-bit Aug 15, 2026
8fa5021
fix(push): bound recovery workers
azooz2003-bit Aug 15, 2026
869b992
fix(push): preserve ambiguous cleanup state
azooz2003-bit Aug 15, 2026
8677b43
fix(push): await bounded opt-out cleanup
azooz2003-bit Aug 15, 2026
52e1b9c
Merge remote-tracking branch 'origin/main' into fix-ios-push-toggle-off
azooz2003-bit Aug 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SwiftUI
struct MobilePushReadinessPreviewView: View {
private let fixture: Fixture
private let rejectsMacMutations: Bool
private let delaysPhoneMutation: Bool

@State private var phoneEnabled: Bool
@State private var authorization: MobilePushAuthorization
Expand All @@ -22,6 +23,7 @@ struct MobilePushReadinessPreviewView: View {
let fixture = Fixture(rawValue: state) ?? .healthy
self.fixture = fixture
self.rejectsMacMutations = environment["CMUX_UITEST_PUSH_MUTATION_FAILURE"] == "1"
self.delaysPhoneMutation = environment["CMUX_UITEST_PUSH_PHONE_MUTATION_DELAY"] == "1"
self._phoneEnabled = State(initialValue: fixture.registration.isEnabled)
self._authorization = State(initialValue: fixture.authorization)
self._registration = State(initialValue: fixture.registration)
Expand Down Expand Up @@ -69,6 +71,9 @@ struct MobilePushReadinessPreviewView: View {

@MainActor
private func setPhoneEnabled(_ enabled: Bool) async -> Bool {
if delaysPhoneMutation {
try? await Task.sleep(for: .milliseconds(500))
}
phoneEnabled = enabled
registration = enabled
? Self.registered
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,11 @@ struct MobilePushSettingsContent: View {
Group {
statusRow

Toggle(
L10n.string(
"mobile.notifications.phoneEnabled",
defaultValue: "Allow Push Alerts on This iPhone"
),
isOn: phoneEnabledBinding
MobilePushToggle(
isEnabled: $phoneEnabled,
isUpdating: $isMutatingPhone,
onChange: onPhoneEnabledChange
)
.accessibilityIdentifier("MobileSettingsNotifications")
.disabled(isMutatingPhone)

if let repair = readiness.repair,
Self.shouldPresentRepair(repair, canConnectMac: canConnectMac),
Expand Down Expand Up @@ -248,25 +244,6 @@ struct MobilePushSettingsContent: View {
.accessibilityIdentifier("MobileSettingsPushReadinessStatus")
}

private var phoneEnabledBinding: Binding<Bool> {
Binding(
get: { phoneEnabled },
set: { requested in
guard !isMutatingPhone else { return }
let confirmed = phoneEnabled
phoneEnabled = requested
isMutatingPhone = true
Task {
let succeeded = await onPhoneEnabledChange(requested)
if !succeeded {
phoneEnabled = confirmed
}
isMutatingPhone = false
}
}
)
}

private var macForwardingBinding: Binding<Bool> {
Binding(
get: { macForwardingEnabled },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#if os(iOS)
import CmuxMobileSupport
import SwiftUI

/// The phone push preference is persisted asynchronously, so update the
/// control optimistically and roll it back only when the mutation fails.
/// Keeping this binding shared prevents release and diagnostic settings from
/// drifting into different interaction behavior.
struct MobilePushToggle: View {
@Binding var isEnabled: Bool
@Binding var isUpdating: Bool
let onChange: @MainActor (Bool) async -> Bool

var body: some View {
Toggle(
L10n.string(
"mobile.notifications.phoneEnabled",
defaultValue: "Allow Push Alerts on This iPhone"
),
isOn: binding
)
.accessibilityIdentifier("MobileSettingsNotifications")
.disabled(isUpdating)
}

private var binding: Binding<Bool> {
Binding(
get: { isEnabled },
set: { requested in
guard !isUpdating else { return }
let previous = isEnabled
isEnabled = requested
isUpdating = true
Task { @MainActor in
defer { isUpdating = false }
if !(await onChange(requested)) {
isEnabled = previous
}
}
}
)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ struct MobileSettingsView: View {
/// `isEnabled` as a non-observable `UserDefaults` read, so reading it
/// directly in `body` would not re-render when it flips.
@State private var notificationsEnabled = false
@State private var notificationsToggleUpdating = false
#if DEBUG
@State private var debugReplyScheduled: Bool?
#endif
Expand Down Expand Up @@ -428,21 +429,11 @@ struct MobileSettingsView: View {
.foregroundStyle(.secondary)
}
#else
Toggle(
L10n.string(
"mobile.notifications.phoneEnabled",
defaultValue: "Allow Push Alerts on This iPhone"
),
isOn: Binding(
get: { notificationsEnabled },
set: { enabled in
Task { @MainActor in
notificationsEnabled = await updatePhonePushEnabled(enabled)
}
}
)
MobilePushToggle(
isEnabled: $notificationsEnabled,
isUpdating: $notificationsToggleUpdating,
onChange: updatePhonePushEnabled
)
.accessibilityIdentifier("MobileSettingsNotifications")
#endif
}

Expand Down
24 changes: 24 additions & 0 deletions ios/cmuxUITests/PushReadinessUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,30 @@ final class PushReadinessUITests: XCTestCase {
waitForValue(forwarding, "0")
}

@MainActor
func testPhonePushToggleTurnsOffWhileMutationIsPending() {
let app = launchPreview(
"healthy",
extraEnvironment: ["CMUX_UITEST_PUSH_PHONE_MUTATION_DELAY": "1"]
)
defer { app.terminate() }

let phone = app.switches["MobileSettingsNotifications"]
XCTAssertTrue(phone.waitForExistence(timeout: 8))
XCTAssertEqual(phone.value as? String, "1")

tapSwitch(phone)

XCTAssertEqual(
phone.value as? String,
"0",
"The toggle must reflect the requested opt-out before the async cleanup finishes"
)
XCTAssertFalse(phone.isEnabled)
waitForEnabled(phone)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
XCTAssertEqual(phone.value as? String, "0")
}

@MainActor
func testFailedMacMutationRollsBackAndStaysVisible() {
let app = launchPreview(
Expand Down