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.