From b176557fe21d114d98e2f30ededb0b0635ab1351 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 6 Oct 2026 10:23:03 -0300 Subject: [PATCH 1/5] fix: align paykit auth sheet with figma Co-Authored-By: Claude Opus 5.5 --- .../Localization/en.lproj/Localizable.strings | 10 ++- .../PubkyAuthApprovalSheet.swift | 83 ++++++++++--------- .../next/paykit-auth-figma-parity.changed.md | 1 + 3 files changed, 53 insertions(+), 41 deletions(-) create mode 100644 changelog.d/next/paykit-auth-figma-parity.changed.md diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 507a5ae6c..c273e1dc6 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -718,26 +718,28 @@ "pubky_auth__title" = "Authorize"; "pubky_auth__description_prefix" = "A service is requesting permission to access and edit your "; "pubky_auth__description_suffix" = " data."; -"pubky_auth__requester" = "Requester ID: {clientId}"; +"pubky_auth__description_named" = "{clientId} is requesting permission to access and edit your {service} data."; +"pubky_auth__details" = "DETAILS"; +"pubky_auth__before_you_continue" = "BEFORE YOU CONTINUE"; "pubky_auth__requested_permissions" = "REQUESTED PERMISSIONS"; "pubky_auth__watch_only_account_default_name" = "{service} account"; "pubky_auth__watch_only_account_fallback_name" = "Paykit server account"; "pubky_auth__watch_only_account_name_error" = "Enter an account name between 1 and 64 characters."; "pubky_auth__watch_only_intro_approve" = "Approve"; "pubky_auth__watch_only_intro_description" = "To earn, you need to share a watch-only Bitcoin account with Paykit. It can view sales activity, but cannot spend funds."; -"pubky_auth__paykit_access_title" = "PRIVATE PAYKIT ACCESS"; -"pubky_auth__paykit_access_description" = "Read and manage your private Paykit data, and send Paykit messages on your behalf. Your Pubky identity secret and wallet spending keys are not shared."; +"pubky_auth__paykit_access_description" = "This service will manage your private Paykit data and send Paykit messages for you. Your pubky secret and wallet keys stay private."; "pubky_auth__watch_only_intro_relay" = "Your authorization will be delivered to {relay}."; "pubky_auth__watch_only_intro_nav_title" = "Earn"; "pubky_auth__watch_only_intro_title" = "EARN BITCOIN\nFROM YOUR\nCONTENT"; "pubky_auth__watch_only_account_xpub_error" = "Bitkit could not create a valid account xpub."; -"pubky_auth__trust_warning" = "Make sure you trust the service, browser, or device before authorizing with your pubky."; +"pubky_auth__trust_warning" = "Make sure you trust the service, browser, and device before authorizing with your pubky."; "pubky_auth__authorization_relay" = "AUTHORIZATION RELAY"; "pubky_auth__authorizing" = "Authorizing..."; "pubky_auth__success_title" = "Authorization Successful"; "pubky_auth__success_prefix" = "You authorized with pubky "; "pubky_auth__success_middle" = " and gave the service permission to access and edit your "; "pubky_auth__success_suffix" = " data."; +"pubky_auth__success_named" = "You authorized with pubky {pubky} and gave {clientId} permission to access and edit your {service} data."; "pubky_auth__biometric_failed" = "Authentication Failed"; "pubky_auth__already_signed_in" = "Already signed in"; "pubky_auth__no_identity" = "Pubky Identity Required"; diff --git a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift index b790c1069..665d4416b 100644 --- a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift +++ b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift @@ -143,7 +143,7 @@ struct PubkyAuthApprovalSheet: View { navTitle: t("pubky_auth__watch_only_intro_nav_title"), title: t("pubky_auth__watch_only_intro_title"), description: watchOnlyConsentDescription, - image: "coin-stack", + image: "coin-stack-4", continueText: t("pubky_auth__watch_only_intro_approve"), cancelText: t("common__cancel"), accentColor: .blueAccent, @@ -203,7 +203,7 @@ struct PubkyAuthApprovalSheet: View { VStack(alignment: .leading, spacing: 0) { approvalDetails - BodyMSBText(t("pubky_auth__authorizing"), textColor: .white32) + BodySSBText(t("pubky_auth__authorizing"), textColor: .white32) .frame(maxWidth: .infinity) .frame(height: 56) } @@ -214,6 +214,7 @@ struct PubkyAuthApprovalSheet: View { private var successContent: some View { VStack(alignment: .leading, spacing: 0) { successDescriptionText + .padding(.horizontal, 16) .padding(.bottom, 16) Spacer() @@ -222,6 +223,7 @@ struct PubkyAuthApprovalSheet: View { .resizable() .scaledToFit() .frame(width: 256, height: 256) + .scaleEffect(Self.checkIllustrationScale) .frame(maxWidth: .infinity) Spacer() @@ -246,21 +248,12 @@ struct PubkyAuthApprovalSheet: View { if !config.request.permissions.isEmpty { descriptionText - .padding(.bottom, 8) - } - - if !config.request.clientID.isEmpty { - BodySText(t("pubky_auth__requester", variables: ["clientId": config.request.clientID])) - .lineLimit(1) - .truncationMode(.tail) .padding(.bottom, 32) - } else { - Spacer().frame(height: 24) } if let relayOrigin = config.request.relayOrigin { relayOriginSection(relayOrigin) - .padding(.bottom, 24) + .padding(.bottom, 32) } if !config.request.permissions.isEmpty { @@ -269,11 +262,11 @@ struct PubkyAuthApprovalSheet: View { if config.request.bitkitClaim?.includesPaykitAccess == true { VStack(alignment: .leading, spacing: 8) { - CaptionMText(t("pubky_auth__paykit_access_title"), textColor: .white64) - BodySText(t("pubky_auth__paykit_access_description")) + CaptionMText(t("pubky_auth__details"), textColor: .white64) + BodySText(t("pubky_auth__paykit_access_description"), textColor: .textPrimary) .fixedSize(horizontal: false, vertical: true) } - .padding(.top, 24) + .padding(.top, 32) .accessibilityElement(children: .contain) .accessibilityIdentifier("PubkyAuthPaykitAccess") } @@ -298,7 +291,7 @@ struct PubkyAuthApprovalSheet: View { .accessibilityIdentifier("PubkySignupHomeserver") } else { profileCard - .padding(.bottom, 16) + .padding(.bottom, 24) } } .frame(minHeight: geometry.size.height, alignment: .top) @@ -313,7 +306,9 @@ struct PubkyAuthApprovalSheet: View { private var descriptionText: some View { BodyMText( - t("pubky_auth__description_prefix") + "" + serviceText + "" + t("pubky_auth__description_suffix"), + config.request.clientID.isEmpty + ? t("pubky_auth__description_prefix") + "" + serviceText + "" + t("pubky_auth__description_suffix") + : t("pubky_auth__description_named", variables: ["clientId": config.request.clientID, "service": serviceText]), accentColor: .textPrimary, accentFont: Fonts.bold ) @@ -341,9 +336,14 @@ struct PubkyAuthApprovalSheet: View { private var successDescriptionText: some View { BodyMText( - t("pubky_auth__success_prefix") + "" + truncatedPublicKey + "" + config.request.clientID.isEmpty + ? t("pubky_auth__success_prefix") + "" + truncatedPublicKey + "" + t("pubky_auth__success_middle") + "" + serviceText + "" - + t("pubky_auth__success_suffix"), + + t("pubky_auth__success_suffix") + : t( + "pubky_auth__success_named", + variables: ["pubky": truncatedPublicKey, "clientId": config.request.clientID, "service": serviceText] + ), accentColor: .textPrimary, accentFont: Fonts.bold ) @@ -357,15 +357,13 @@ struct PubkyAuthApprovalSheet: View { ForEach(Array(config.request.permissions.enumerated()), id: \.offset) { _, permission in permissionRow(permission) } - - CustomDivider(color: .white10) } } private func permissionRow(_ permission: PubkyAuthPermission) -> some View { HStack(spacing: 4) { Image(systemName: "folder") - .font(.system(size: 14)) + .font(.system(size: 16)) .foregroundColor(.white) BodySSBText(permission.displayPath) @@ -378,42 +376,53 @@ struct PubkyAuthApprovalSheet: View { } private var trustWarning: some View { - BodySText(t("pubky_auth__trust_warning")) - .lineSpacing(4) + VStack(alignment: .leading, spacing: 8) { + CaptionMText(t("pubky_auth__before_you_continue"), textColor: .white64) + BodySText(t("pubky_auth__trust_warning")) + .fixedSize(horizontal: false, vertical: true) + } } private var profileCard: some View { - VStack(spacing: 16) { - CaptionMText( - truncatedPublicKey.localizedUppercase, - textColor: .white64 - ) - + HStack(spacing: 16) { if let imageUri = pubkyProfile.displayImageUri { - PubkyImage(uri: imageUri, size: 96) + PubkyImage(uri: imageUri, size: 48) } else { Circle() .fill(Color.pubkyGreen) - .frame(width: 96, height: 96) + .frame(width: 48, height: 48) .overlay { Image("user-square") .resizable() .scaledToFit() .foregroundColor(.white32) - .frame(width: 48, height: 48) + .frame(width: 24, height: 24) } } - HeadlineText(pubkyProfile.displayName ?? "") - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) + VStack(alignment: .leading, spacing: 0) { + CaptionMText( + truncatedPublicKey.localizedUppercase, + textColor: .white64 + ) + .lineLimit(1) + + BodyMSBText(pubkyProfile.displayName ?? "") + .lineLimit(1) + .truncationMode(.tail) + } + + Spacer(minLength: 0) } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, alignment: .leading) .padding(24) .background(Color.gray6) .cornerRadius(16) } + /// Figma draws the check illustration at 274pt inside its 256pt slot. + private static let checkIllustrationScale: CGFloat = 274.0 / 256.0 + // MARK: - Actions @MainActor diff --git a/changelog.d/next/paykit-auth-figma-parity.changed.md b/changelog.d/next/paykit-auth-figma-parity.changed.md new file mode 100644 index 000000000..6f31052f7 --- /dev/null +++ b/changelog.d/next/paykit-auth-figma-parity.changed.md @@ -0,0 +1 @@ +Pubky authorization dialogs now name the requesting service, group permissions and details under clear headings, and show a compact profile card. From f72b2607ad055517e180a6a94593e7cc7c842155 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 8 Oct 2026 08:40:19 -0300 Subject: [PATCH 2/5] fix: use grey placeholder avatar on auth profile card Co-Authored-By: Claude Opus 5.5 --- .../Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift index 665d4416b..2516df6fa 100644 --- a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift +++ b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift @@ -389,7 +389,7 @@ struct PubkyAuthApprovalSheet: View { PubkyImage(uri: imageUri, size: 48) } else { Circle() - .fill(Color.pubkyGreen) + .fill(Color.gray5) .frame(width: 48, height: 48) .overlay { Image("user-square") From 4b469299824df45648cfd46b85d36d985b0b034a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 8 Oct 2026 13:06:40 -0300 Subject: [PATCH 3/5] fix: handle long or marked-up requester ids on auth sheet Co-Authored-By: Claude Opus 5.5 --- .../PubkyAuthApprovalSheet.swift | 54 +++++++++++++------ BitkitTests/PubkyAuthApprovalSheetTests.swift | 6 +++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift index 2516df6fa..0efcc25bc 100644 --- a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift +++ b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift @@ -31,6 +31,16 @@ func pubkyAuthDisplayPublicKey(_ publicKey: String?) -> String { return "\(rawKey.prefix(4))...\(rawKey.suffix(4))" } +/// Request data is shown literally: accent tags inside it must not be read as markup by the text components. +func pubkyAuthLiteralText(_ value: String) -> String { + var text = value + while true { + let stripped = text.replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "") + if stripped == text { return text } + text = stripped + } +} + struct PubkyAuthApprovalConfig { let request: PubkyAuthRequest } @@ -213,20 +223,28 @@ struct PubkyAuthApprovalSheet: View { private var successContent: some View { VStack(alignment: .leading, spacing: 0) { - successDescriptionText - .padding(.horizontal, 16) - .padding(.bottom, 16) + GeometryReader { geometry in + ScrollView { + VStack(alignment: .leading, spacing: 0) { + successDescriptionText + .padding(.horizontal, 16) + .padding(.bottom, 16) - Spacer() + Spacer(minLength: 0) - Image("check") - .resizable() - .scaledToFit() - .frame(width: 256, height: 256) - .scaleEffect(Self.checkIllustrationScale) - .frame(maxWidth: .infinity) + Image("check") + .resizable() + .scaledToFit() + .frame(width: 256, height: 256) + .scaleEffect(Self.checkIllustrationScale) + .frame(maxWidth: .infinity) - Spacer() + Spacer(minLength: 16) + } + .frame(minHeight: geometry.size.height, alignment: .top) + } + .scrollIndicators(.hidden) + } CustomButton(title: t("common__ok")) { sheets.hideSheet() @@ -301,14 +319,18 @@ struct PubkyAuthApprovalSheet: View { } private var serviceText: String { - config.request.serviceNames.joined(separator: " and ") + pubkyAuthLiteralText(config.request.serviceNames.joined(separator: " and ")) + } + + private var requesterText: String { + pubkyAuthLiteralText(config.request.clientID) } private var descriptionText: some View { BodyMText( - config.request.clientID.isEmpty + requesterText.isEmpty ? t("pubky_auth__description_prefix") + "" + serviceText + "" + t("pubky_auth__description_suffix") - : t("pubky_auth__description_named", variables: ["clientId": config.request.clientID, "service": serviceText]), + : t("pubky_auth__description_named", variables: ["clientId": requesterText, "service": serviceText]), accentColor: .textPrimary, accentFont: Fonts.bold ) @@ -336,13 +358,13 @@ struct PubkyAuthApprovalSheet: View { private var successDescriptionText: some View { BodyMText( - config.request.clientID.isEmpty + requesterText.isEmpty ? t("pubky_auth__success_prefix") + "" + truncatedPublicKey + "" + t("pubky_auth__success_middle") + "" + serviceText + "" + t("pubky_auth__success_suffix") : t( "pubky_auth__success_named", - variables: ["pubky": truncatedPublicKey, "clientId": config.request.clientID, "service": serviceText] + variables: ["pubky": truncatedPublicKey, "clientId": requesterText, "service": serviceText] ), accentColor: .textPrimary, accentFont: Fonts.bold diff --git a/BitkitTests/PubkyAuthApprovalSheetTests.swift b/BitkitTests/PubkyAuthApprovalSheetTests.swift index 5f5027cdd..878327217 100644 --- a/BitkitTests/PubkyAuthApprovalSheetTests.swift +++ b/BitkitTests/PubkyAuthApprovalSheetTests.swift @@ -621,4 +621,10 @@ private func XCTAssertThrowsErrorAsync( _ = try await expression() XCTFail("Expected expression to throw", file: file, line: line) } catch {} + + func testRequestTextLosesAccentMarkupEvenWhenTagsAreNested() { + XCTAssertEqual(pubkyAuthLiteralText("evil.app"), "evil.app") + XCTAssertEqual(pubkyAuthLiteralText("ent>evilent>.app"), "evil.app") + XCTAssertEqual(pubkyAuthLiteralText("app.paykit.server"), "app.paykit.server") + } } From 0142ac8ea5f0d44559795ee52eb0fbaf4800ac3f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 8 Oct 2026 13:06:40 -0300 Subject: [PATCH 4/5] chore: name auth sheet changelog fragment after pr Co-Authored-By: Claude Opus 5.5 --- .../next/{paykit-auth-figma-parity.changed.md => 880.changed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{paykit-auth-figma-parity.changed.md => 880.changed.md} (100%) diff --git a/changelog.d/next/paykit-auth-figma-parity.changed.md b/changelog.d/next/880.changed.md similarity index 100% rename from changelog.d/next/paykit-auth-figma-parity.changed.md rename to changelog.d/next/880.changed.md From 508c58f00c560eb6c6157d764bebf2e751baf772 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Oct 2026 08:20:15 -0300 Subject: [PATCH 5/5] fix: show requester ids literally in auth sentences Co-Authored-By: Claude Opus 5.5 --- .../PubkyAuthApprovalSheet.swift | 25 ++++++++++++--- BitkitTests/PubkyAuthApprovalSheetTests.swift | 32 +++++++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift index 0efcc25bc..ea2de51a3 100644 --- a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift +++ b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift @@ -31,9 +31,11 @@ func pubkyAuthDisplayPublicKey(_ publicKey: String?) -> String { return "\(rawKey.prefix(4))...\(rawKey.suffix(4))" } -/// Request data is shown literally: accent tags inside it must not be read as markup by the text components. +/// Request data is shown literally: control and bidi characters are dropped so it cannot add lines or reorder the +/// sentence, and accent tags inside it must not be read as markup by the text components. func pubkyAuthLiteralText(_ value: String) -> String { - var text = value + let hidden: Set = [.control, .format, .lineSeparator, .paragraphSeparator] + var text = String(String.UnicodeScalarView(value.unicodeScalars.filter { !hidden.contains($0.properties.generalCategory) })) while true { let stripped = text.replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "") if stripped == text { return text } @@ -41,6 +43,19 @@ func pubkyAuthLiteralText(_ value: String) -> String { } } +/// Fills each `{placeholder}` from the template only, so request data that itself contains a placeholder stays as it is. +func pubkyAuthFilledText(_ template: String, variables: [String: String]) -> String { + var result = "" + var rest = Substring(template) + while let open = rest.firstIndex(of: "{"), let close = rest[open...].firstIndex(of: "}") { + let key = String(rest[rest.index(after: open) ..< close]) + result += rest[.." + serviceText + "" + t("pubky_auth__description_suffix") - : t("pubky_auth__description_named", variables: ["clientId": requesterText, "service": serviceText]), + : pubkyAuthFilledText(t("pubky_auth__description_named"), variables: ["clientId": requesterText, "service": serviceText]), accentColor: .textPrimary, accentFont: Fonts.bold ) @@ -362,8 +377,8 @@ struct PubkyAuthApprovalSheet: View { ? t("pubky_auth__success_prefix") + "" + truncatedPublicKey + "" + t("pubky_auth__success_middle") + "" + serviceText + "" + t("pubky_auth__success_suffix") - : t( - "pubky_auth__success_named", + : pubkyAuthFilledText( + t("pubky_auth__success_named"), variables: ["pubky": truncatedPublicKey, "clientId": requesterText, "service": serviceText] ), accentColor: .textPrimary, diff --git a/BitkitTests/PubkyAuthApprovalSheetTests.swift b/BitkitTests/PubkyAuthApprovalSheetTests.swift index 878327217..60da9a049 100644 --- a/BitkitTests/PubkyAuthApprovalSheetTests.swift +++ b/BitkitTests/PubkyAuthApprovalSheetTests.swift @@ -537,6 +537,32 @@ final class PubkyAuthApprovalSheetTests: XCTestCase { XCTAssertEqual(manager.accounts.first?.isTrackingEnabled, false) XCTAssertEqual(node.trackingChanges, [true, false]) } + + func testRequestTextLosesAccentMarkupEvenWhenTagsAreNested() { + XCTAssertEqual(pubkyAuthLiteralText("evil.app"), "evil.app") + XCTAssertEqual(pubkyAuthLiteralText("ent>evilent>.app"), "evil.app") + XCTAssertEqual(pubkyAuthLiteralText("app.paykit.server"), "app.paykit.server") + } + + func testRequestTextLosesLineBreaksAndBidiControls() { + XCTAssertEqual(pubkyAuthLiteralText("bank.com\nBitkit is safe."), "bank.comBitkit is safe.") + XCTAssertEqual(pubkyAuthLiteralText("\u{202E}moc.knab\u{202C}"), "moc.knab") + XCTAssertEqual(pubkyAuthLiteralText("a\u{2028}b\u{2066}c\u{200B}d"), "abcd") + XCTAssertEqual(pubkyAuthLiteralText("evil"), "evil") + } + + func testFilledTextKeepsPlaceholdersInsideRequestData() { + let template = "{clientId} wants {service} for {pubky}" + XCTAssertEqual( + pubkyAuthFilledText(template, variables: ["clientId": "{service}", "service": "{clientId}", "pubky": "{pubky}"]), + "{service} wants {clientId} for {pubky}" + ) + XCTAssertEqual( + pubkyAuthFilledText(template, variables: ["clientId": "bank.com", "service": "paykit"]), + "bank.com wants paykit for {pubky}" + ) + XCTAssertEqual(pubkyAuthFilledText("no placeholders { here", variables: ["here": "x"]), "no placeholders { here") + } } private enum ApprovalFakeError: Error { @@ -621,10 +647,4 @@ private func XCTAssertThrowsErrorAsync( _ = try await expression() XCTFail("Expected expression to throw", file: file, line: line) } catch {} - - func testRequestTextLosesAccentMarkupEvenWhenTagsAreNested() { - XCTAssertEqual(pubkyAuthLiteralText("evil.app"), "evil.app") - XCTAssertEqual(pubkyAuthLiteralText("ent>evilent>.app"), "evil.app") - XCTAssertEqual(pubkyAuthLiteralText("app.paykit.server"), "app.paykit.server") - } }