This commit is contained in:
@@ -12,64 +12,34 @@ struct LoginRootView: View {
|
|||||||
MacPairingView(model: model)
|
MacPairingView(model: model)
|
||||||
#else
|
#else
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ZStack(alignment: .top) {
|
PairingWelcomeView(
|
||||||
LiveQRScannerView { payload in
|
model: model,
|
||||||
|
onScanRequested: {
|
||||||
|
model.isScannerPresented = true
|
||||||
|
},
|
||||||
|
onNFCRequested: {
|
||||||
|
isNFCSheetPresented = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.fullScreenCover(isPresented: $model.isScannerPresented) {
|
||||||
|
QRScannerSheet(
|
||||||
|
title: "Scan pairing QR",
|
||||||
|
description: "Open pairing in your idp.global web session, then scan the QR shown there to link this iPhone.",
|
||||||
|
navigationTitle: "Link Passport"
|
||||||
|
) { payload in
|
||||||
model.manualPairingPayload = payload
|
model.manualPairingPayload = payload
|
||||||
Task {
|
Task {
|
||||||
await model.signIn(with: payload, transport: .qr)
|
await model.signIn(with: payload, transport: .qr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
IdPGlassCapsule {
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
Text("Scan a pairing code")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Text("Turn this iPhone into your idp.global passport with QR or NFC.")
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.top, 12)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button {
|
|
||||||
isNFCSheetPresented = true
|
|
||||||
} label: {
|
|
||||||
IdPGlassCapsule {
|
|
||||||
HStack(spacing: 10) {
|
|
||||||
Image(systemName: "wave.3.right")
|
|
||||||
.foregroundStyle(IdP.tint)
|
|
||||||
Text("Hold near NFC tag")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.bottom, 24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
|
||||||
Button("Use demo payload") {
|
|
||||||
Task {
|
|
||||||
await model.signInWithSuggestedPayload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.font(.footnote)
|
|
||||||
.disabled(model.isAuthenticating || model.suggestedPairingPayload.isEmpty)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isNFCSheetPresented) {
|
.sheet(isPresented: $isNFCSheetPresented) {
|
||||||
NFCSheet(actionTitle: "Approve") { request in
|
NFCSheet(
|
||||||
|
title: "Hold near pairing tag",
|
||||||
|
message: "Use a supported idp.global NFC tag to link this iPhone and attach a signed location proof.",
|
||||||
|
actionTitle: "Link device"
|
||||||
|
) { request in
|
||||||
await model.signIn(with: request)
|
await model.signIn(with: request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +47,163 @@ struct LoginRootView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
private struct PairingWelcomeView: View {
|
||||||
|
@ObservedObject var model: AppViewModel
|
||||||
|
let onScanRequested: () -> Void
|
||||||
|
let onNFCRequested: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
AppScrollScreen(compactLayout: true, bottomPadding: 150) {
|
||||||
|
AppPanel(compactLayout: true, radius: 30) {
|
||||||
|
ShadcnBadge(
|
||||||
|
title: "Passport setup",
|
||||||
|
tone: .accent,
|
||||||
|
leading: Image(systemName: "shield.lefthalf.filled")
|
||||||
|
)
|
||||||
|
|
||||||
|
HStack(alignment: .top, spacing: 16) {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||||
|
.fill(IdP.tint.opacity(0.14))
|
||||||
|
.frame(width: 72, height: 72)
|
||||||
|
|
||||||
|
Image(systemName: "qrcode.viewfinder")
|
||||||
|
.font(.system(size: 30, weight: .semibold))
|
||||||
|
.foregroundStyle(IdP.tint)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Link this iPhone to your idp.global account")
|
||||||
|
.font(.title2.weight(.bold))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
|
Text("Start pairing in your browser, then scan the fresh QR on the next screen. This device becomes your passport for approvals, alerts, and identity proofs.")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(Color.idpMutedForeground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("No camera? The scanner screen also lets you paste a pairing link manually.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(Color.idpMutedForeground)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSectionCard(
|
||||||
|
title: "How pairing works",
|
||||||
|
subtitle: "The QR is created by the web session you want to trust.",
|
||||||
|
compactLayout: true
|
||||||
|
) {
|
||||||
|
VStack(spacing: 14) {
|
||||||
|
PairingStepRow(
|
||||||
|
number: 1,
|
||||||
|
title: "Start from the web",
|
||||||
|
message: "Open idp.global in your browser and begin a new device pairing flow."
|
||||||
|
)
|
||||||
|
|
||||||
|
PairingStepRow(
|
||||||
|
number: 2,
|
||||||
|
title: "Scan the QR",
|
||||||
|
message: "Use the camera on the next screen to scan the pairing QR shown by that browser session."
|
||||||
|
)
|
||||||
|
|
||||||
|
PairingStepRow(
|
||||||
|
number: 3,
|
||||||
|
title: "Approve future sign-ins",
|
||||||
|
message: "Once linked, this iPhone can receive approval requests and security alerts."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSectionCard(
|
||||||
|
title: "Before you scan",
|
||||||
|
subtitle: "A few quick checks help the link complete cleanly.",
|
||||||
|
compactLayout: true
|
||||||
|
) {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
PairingNoteRow(icon: "safari", text: "Keep the pairing page open in your browser until this phone confirms the link.")
|
||||||
|
PairingNoteRow(icon: "camera.fill", text: "Grant camera access when prompted so the QR can be read directly.")
|
||||||
|
PairingNoteRow(icon: "wave.3.right", text: "If your organization uses NFC tags, you can link with NFC instead of QR.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Welcome")
|
||||||
|
.idpInlineNavigationTitle()
|
||||||
|
.safeAreaInset(edge: .bottom) {
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Button(action: onScanRequested) {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
if model.isAuthenticating {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
.tint(Color.idpPrimaryForeground)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "camera.viewfinder")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(model.isAuthenticating ? "Linking device..." : "Scan pairing QR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(PrimaryActionStyle())
|
||||||
|
.disabled(model.isAuthenticating)
|
||||||
|
|
||||||
|
Button(action: onNFCRequested) {
|
||||||
|
Label("Use NFC tag instead", systemImage: "wave.3.right")
|
||||||
|
}
|
||||||
|
.buttonStyle(SecondaryActionStyle())
|
||||||
|
.disabled(model.isAuthenticating)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.top, 10)
|
||||||
|
.padding(.bottom, 14)
|
||||||
|
.background(.regularMaterial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PairingStepRow: View {
|
||||||
|
let number: Int
|
||||||
|
let title: String
|
||||||
|
let message: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 12) {
|
||||||
|
Text("\(number)")
|
||||||
|
.font(.subheadline.weight(.bold))
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
.foregroundStyle(IdP.tint)
|
||||||
|
.background(IdP.tint.opacity(0.12), in: Circle())
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
Text(message)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(Color.idpMutedForeground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PairingNoteRow: View {
|
||||||
|
let icon: String
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 10) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.foregroundStyle(IdP.tint)
|
||||||
|
.frame(width: 18, height: 18)
|
||||||
|
|
||||||
|
Text(text)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(Color.idpMutedForeground)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
private struct MacPairingView: View {
|
private struct MacPairingView: View {
|
||||||
@ObservedObject var model: AppViewModel
|
@ObservedObject var model: AppViewModel
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import AppKit
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct QRScannerSheet: View {
|
struct QRScannerSheet: View {
|
||||||
let seededPayload: String
|
|
||||||
let title: String
|
let title: String
|
||||||
let description: String
|
let description: String
|
||||||
let navigationTitleText: String
|
let navigationTitleText: String
|
||||||
@@ -19,13 +18,11 @@ struct QRScannerSheet: View {
|
|||||||
@State private var manualFallback = ""
|
@State private var manualFallback = ""
|
||||||
|
|
||||||
init(
|
init(
|
||||||
seededPayload: String,
|
|
||||||
title: String = "Scan QR",
|
title: String = "Scan QR",
|
||||||
description: String = "Use the camera to scan an idp.global QR challenge.",
|
description: String = "Use the camera to scan an idp.global QR challenge.",
|
||||||
navigationTitle: String = "Scan QR",
|
navigationTitle: String = "Scan QR",
|
||||||
onCodeScanned: @escaping (String) -> Void
|
onCodeScanned: @escaping (String) -> Void
|
||||||
) {
|
) {
|
||||||
self.seededPayload = seededPayload
|
|
||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
self.navigationTitleText = navigationTitle
|
self.navigationTitleText = navigationTitle
|
||||||
@@ -55,33 +52,18 @@ struct QRScannerSheet: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
ManualFallbackCard(
|
||||||
Text("Manual fallback")
|
manualFallback: $manualFallback,
|
||||||
.font(.headline)
|
onUsePayload: {
|
||||||
|
let chosen = manualFallback.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
TextEditor(text: $manualFallback)
|
guard !chosen.isEmpty else { return }
|
||||||
.font(.footnote.monospaced())
|
onCodeScanned(chosen)
|
||||||
.scrollContentBackground(.hidden)
|
dismiss()
|
||||||
.frame(minHeight: 110)
|
},
|
||||||
.padding(10)
|
onPasteFromClipboard: {
|
||||||
.background(Color.idpTertiaryFill, in: RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous))
|
manualFallback = ClipboardPayloadReader.readString()
|
||||||
|
|
||||||
VStack(spacing: 10) {
|
|
||||||
Button("Use payload") {
|
|
||||||
let chosen = manualFallback.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
onCodeScanned(chosen.isEmpty ? seededPayload : chosen)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.buttonStyle(PrimaryActionStyle())
|
|
||||||
|
|
||||||
Button("Use demo payload") {
|
|
||||||
manualFallback = seededPayload
|
|
||||||
}
|
|
||||||
.buttonStyle(SecondaryActionStyle())
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
.padding(18)
|
|
||||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous))
|
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
}
|
}
|
||||||
@@ -94,9 +76,6 @@ struct QRScannerSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
|
||||||
manualFallback = seededPayload
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +98,7 @@ struct LiveQRScannerView: View {
|
|||||||
@State private var didDetectCode = false
|
@State private var didDetectCode = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottomLeading) {
|
ZStack {
|
||||||
Group {
|
Group {
|
||||||
if scanner.isPreviewAvailable {
|
if scanner.isPreviewAvailable {
|
||||||
ScannerPreview(session: scanner.captureSession)
|
ScannerPreview(session: scanner.captureSession)
|
||||||
@@ -141,21 +120,46 @@ struct LiveQRScannerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Color.black.opacity(0.18)
|
LinearGradient(
|
||||||
|
colors: [Color.black.opacity(0.64), Color.clear, Color.black.opacity(0.78)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
ScanFrameOverlay(detected: didDetectCode)
|
ScanFrameOverlay(detected: didDetectCode)
|
||||||
.padding(40)
|
.padding(40)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack {
|
||||||
Text("Point the camera at the pairing QR")
|
HStack {
|
||||||
.font(.headline.weight(.semibold))
|
ScannerStatusBadge(title: scanner.isPreviewAvailable ? "Camera ready" : "Manual fallback")
|
||||||
.foregroundStyle(.white)
|
Spacer()
|
||||||
Text(scanner.statusMessage)
|
}
|
||||||
.font(.subheadline)
|
.padding(.horizontal, 20)
|
||||||
.foregroundStyle(.white.opacity(0.84))
|
.padding(.top, 18)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Align the pairing QR inside the frame")
|
||||||
|
.font(.headline.weight(.semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
|
Text(scanner.statusMessage)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.white.opacity(0.84))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(18)
|
||||||
|
.background(Color.black.opacity(0.34), in: RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 24, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.12), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
.padding(.bottom, 18)
|
||||||
}
|
}
|
||||||
.padding(22)
|
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
scanner.onCodeScanned = { payload in
|
scanner.onCodeScanned = { payload in
|
||||||
@@ -185,6 +189,17 @@ private struct ScanFrameOverlay: View {
|
|||||||
let inset = detected ? 18.0 : 0
|
let inset = detected ? 18.0 : 0
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 30, style: .continuous)
|
||||||
|
.fill(Color.black.opacity(0.18))
|
||||||
|
.frame(width: size - inset, height: size - inset)
|
||||||
|
|
||||||
|
RoundedRectangle(cornerRadius: 30, style: .continuous)
|
||||||
|
.stroke(
|
||||||
|
Color.white.opacity(detected ? 0.95 : 0.75),
|
||||||
|
style: StrokeStyle(lineWidth: detected ? 3 : 2, lineCap: .round, lineJoin: .round)
|
||||||
|
)
|
||||||
|
.frame(width: size - inset, height: size - inset)
|
||||||
|
|
||||||
CornerTick(rotation: .degrees(0))
|
CornerTick(rotation: .degrees(0))
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
CornerTick(rotation: .degrees(90))
|
CornerTick(rotation: .degrees(90))
|
||||||
@@ -202,6 +217,110 @@ private struct ScanFrameOverlay: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ScannerStatusBadge: View {
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Circle()
|
||||||
|
.fill(IdP.tint)
|
||||||
|
.frame(width: 8, height: 8)
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color.black.opacity(0.26), in: Capsule(style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
Capsule(style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.10), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ManualFallbackCard: View {
|
||||||
|
@Binding var manualFallback: String
|
||||||
|
let onUsePayload: () -> Void
|
||||||
|
let onPasteFromClipboard: () -> Void
|
||||||
|
|
||||||
|
private var hasManualPayload: Bool {
|
||||||
|
!manualFallback.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
HStack(alignment: .top, spacing: 12) {
|
||||||
|
Image(systemName: "doc.text.viewfinder")
|
||||||
|
.font(.system(size: 18, weight: .semibold))
|
||||||
|
.foregroundStyle(IdP.tint)
|
||||||
|
.frame(width: 34, height: 34)
|
||||||
|
.background(IdP.tint.opacity(0.12), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Paste pairing link")
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
Text("If scanning is unavailable, paste the idp.global pairing link from your browser or clipboard.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(Color.idpMutedForeground)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
TextEditor(text: $manualFallback)
|
||||||
|
.font(.footnote.monospaced())
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.frame(minHeight: 110)
|
||||||
|
.padding(10)
|
||||||
|
.background(Color.idpTertiaryFill, in: RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous))
|
||||||
|
|
||||||
|
if manualFallback.isEmpty {
|
||||||
|
Text("idp.global://pair?token=...")
|
||||||
|
.font(.footnote.monospaced())
|
||||||
|
.foregroundStyle(Color.idpMutedForeground)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 18)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Button("Use pasted payload", action: onUsePayload)
|
||||||
|
.buttonStyle(PrimaryActionStyle())
|
||||||
|
.disabled(!hasManualPayload)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
onPasteFromClipboard()
|
||||||
|
} label: {
|
||||||
|
Label("Paste from clipboard", systemImage: "doc.on.clipboard")
|
||||||
|
}
|
||||||
|
.buttonStyle(SecondaryActionStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(18)
|
||||||
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: IdP.cardRadius, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.10), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ClipboardPayloadReader {
|
||||||
|
static func readString() -> String {
|
||||||
|
#if os(iOS)
|
||||||
|
UIPasteboard.general.string?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
#elseif os(macOS)
|
||||||
|
NSPasteboard.general.string(forType: .string)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
#else
|
||||||
|
""
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private struct CornerTick: View {
|
private struct CornerTick: View {
|
||||||
let rotation: Angle
|
let rotation: Angle
|
||||||
|
|
||||||
@@ -233,7 +352,7 @@ private final class QRScannerViewModel: NSObject, ObservableObject, AVCaptureMet
|
|||||||
#if os(iOS) && targetEnvironment(simulator)
|
#if os(iOS) && targetEnvironment(simulator)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isPreviewAvailable = false
|
isPreviewAvailable = false
|
||||||
statusMessage = "The iOS simulator has no live camera feed. Use the demo payload below."
|
statusMessage = "The iOS simulator has no live camera feed. Paste a pairing link below instead."
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user