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..ea2de51a3 100644 --- a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift +++ b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift @@ -31,6 +31,31 @@ func pubkyAuthDisplayPublicKey(_ publicKey: String?) -> String { return "\(rawKey.prefix(4))...\(rawKey.suffix(4))" } +/// 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 { + 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 } + text = stripped + } +} + +/// 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"), + requesterText.isEmpty + ? t("pubky_auth__description_prefix") + "" + serviceText + "" + t("pubky_auth__description_suffix") + : pubkyAuthFilledText(t("pubky_auth__description_named"), variables: ["clientId": requesterText, "service": serviceText]), accentColor: .textPrimary, accentFont: Fonts.bold ) @@ -341,9 +373,14 @@ struct PubkyAuthApprovalSheet: View { private var successDescriptionText: some View { BodyMText( - t("pubky_auth__success_prefix") + "" + truncatedPublicKey + "" + requesterText.isEmpty + ? t("pubky_auth__success_prefix") + "" + truncatedPublicKey + "" + t("pubky_auth__success_middle") + "" + serviceText + "" - + t("pubky_auth__success_suffix"), + + t("pubky_auth__success_suffix") + : pubkyAuthFilledText( + t("pubky_auth__success_named"), + variables: ["pubky": truncatedPublicKey, "clientId": requesterText, "service": serviceText] + ), accentColor: .textPrimary, accentFont: Fonts.bold ) @@ -357,15 +394,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 +413,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) + .fill(Color.gray5) + .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/BitkitTests/PubkyAuthApprovalSheetTests.swift b/BitkitTests/PubkyAuthApprovalSheetTests.swift index 5f5027cdd..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 { diff --git a/changelog.d/next/880.changed.md b/changelog.d/next/880.changed.md new file mode 100644 index 000000000..6f31052f7 --- /dev/null +++ b/changelog.d/next/880.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.