chore: snapshot uncommitted local work before rebasing onto cloud-agent
Safety commit so the 94 uncommitted files in the working tree (in-flight Account/Mac/Tip work plus the feature-preview recording tooling) survive the move onto cursor/cloud-agent-1787824905651-vxuwc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// MacAccountAppleButton.swift
|
||||
// OSGKeyboard · Mac
|
||||
//
|
||||
// Sign in with Apple for the menu-bar app. Sign-in and delete-account
|
||||
// reauthentication share one button, exactly like the iOS account center.
|
||||
// Nonce generation and credential validation come from the cross-platform
|
||||
// `AccountAppleNonce` / `AccountAppleAuthorizationPayload` factories, so the
|
||||
// security-relevant path is identical on both platforms — only the labels,
|
||||
// which resolve from Shared.strings here instead of the iOS app bundle, differ.
|
||||
//
|
||||
// This works in a non-sandboxed Developer ID build: Sign in with Apple needs
|
||||
// only the `com.apple.developer.applesignin` entitlement, never the Mac App
|
||||
// Store. The Mac App ID must be grouped under the iOS primary App ID or the
|
||||
// same person authenticates as a different Apple `sub` here than on iPhone.
|
||||
|
||||
import AuthenticationServices
|
||||
import SwiftUI
|
||||
|
||||
struct MacAccountAppleButton: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@EnvironmentObject private var coordinator: AccountSessionCoordinator
|
||||
|
||||
let purpose: AccountAppleAuthorizationPurpose
|
||||
let language: AppUILanguage
|
||||
var onSignedIn: () -> Void = {}
|
||||
var onDeleted: () -> Void = {}
|
||||
|
||||
@State private var rawNonce: String?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if purpose == .signIn, coordinator.operation == .signingIn {
|
||||
loadingButton
|
||||
} else {
|
||||
SignInWithAppleButton(
|
||||
purpose == .signIn ? .signIn : .continue,
|
||||
onRequest: prepareRequest,
|
||||
onCompletion: completeAuthorization
|
||||
)
|
||||
.signInWithAppleButtonStyle(colorScheme == .dark ? .white : .black)
|
||||
// 系统按钮的文字/图标随按钮高度缩放,40pt 时文字明显偏小。
|
||||
.frame(height: MacMetrics.appleButtonHeight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
|
||||
.accessibilityIdentifier(
|
||||
purpose == .signIn
|
||||
? "mac.account.signIn.apple"
|
||||
: "mac.account.delete.reauthenticate"
|
||||
)
|
||||
}
|
||||
}
|
||||
// Fixed, not `maxWidth`: inside a settings row the label column is also
|
||||
// flexible, and two flexible siblings split the row evenly — which
|
||||
// shrank the button and squeezed the copy next to it.
|
||||
.frame(width: 260)
|
||||
}
|
||||
|
||||
private var loadingButton: some View {
|
||||
HStack(spacing: Spacing.sm) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.tint(foreground)
|
||||
Text(MacL10n.string("mac.account.signIn.loading", language: language))
|
||||
.font(TypeStyle.bodyEmph)
|
||||
}
|
||||
.foregroundStyle(foreground)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: MacMetrics.appleButtonHeight)
|
||||
.background(background)
|
||||
.clipShape(RoundedRectangle(cornerRadius: Radius.medium, style: .continuous))
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityIdentifier("mac.account.signIn.loading")
|
||||
}
|
||||
|
||||
private var background: Color { colorScheme == .dark ? .white : .black }
|
||||
private var foreground: Color { colorScheme == .dark ? .black : .white }
|
||||
|
||||
private func prepareRequest(_ request: ASAuthorizationAppleIDRequest) {
|
||||
guard let nonce = AccountAppleNonce.make() else {
|
||||
rawNonce = nil
|
||||
request.nonce = nil
|
||||
return
|
||||
}
|
||||
rawNonce = nonce.rawValue
|
||||
request.requestedScopes = purpose == .signIn ? [.fullName] : []
|
||||
request.nonce = nonce.sha256Hex
|
||||
}
|
||||
|
||||
private func completeAuthorization(_ result: Result<ASAuthorization, Error>) {
|
||||
guard let rawNonce else {
|
||||
OSGDiag.log(
|
||||
"appleAuthorization failed stage=nonce-unavailable",
|
||||
category: "account"
|
||||
)
|
||||
coordinator.recordAppleAuthorizationFailure(detail: "nonce-unavailable")
|
||||
return
|
||||
}
|
||||
self.rawNonce = nil
|
||||
|
||||
switch result {
|
||||
case let .success(authorization):
|
||||
guard let payload = AccountAppleAuthorizationPayload.make(
|
||||
from: authorization,
|
||||
rawNonce: rawNonce
|
||||
) else {
|
||||
OSGDiag.log(
|
||||
"appleAuthorization failed stage=credential-payload",
|
||||
category: "account"
|
||||
)
|
||||
coordinator.recordAppleAuthorizationFailure(detail: "credential-payload")
|
||||
return
|
||||
}
|
||||
Task {
|
||||
switch purpose {
|
||||
case .signIn:
|
||||
await coordinator.signIn(with: payload)
|
||||
if coordinator.isSignedIn {
|
||||
ProviderConfig.shared.reloadFromPersistedStorage()
|
||||
onSignedIn()
|
||||
}
|
||||
case .deleteAccount:
|
||||
await coordinator.deleteAccount(with: payload)
|
||||
if !coordinator.isSignedIn {
|
||||
onDeleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .failure(error):
|
||||
let diagnostic = error as NSError
|
||||
OSGDiag.log(
|
||||
"appleAuthorization failed stage=system "
|
||||
+ "domain=\(diagnostic.domain) code=\(diagnostic.code)",
|
||||
category: "account"
|
||||
)
|
||||
if (error as? ASAuthorizationError)?.code != .canceled {
|
||||
coordinator.recordAppleAuthorizationFailure(
|
||||
detail: "as-\(diagnostic.domain)-\(diagnostic.code)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user