diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings
index 03e5c931e..70f81e5c8 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..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
}
@@ -143,7 +153,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 +213,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)
}
@@ -213,18 +223,28 @@ struct PubkyAuthApprovalSheet: View {
private var successContent: some View {
VStack(alignment: .leading, spacing: 0) {
- successDescriptionText
- .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)
- .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()
@@ -246,21 +266,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 +280,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 +309,7 @@ struct PubkyAuthApprovalSheet: View {
.accessibilityIdentifier("PubkySignupHomeserver")
} else {
profileCard
- .padding(.bottom, 16)
+ .padding(.bottom, 24)
}
}
.frame(minHeight: geometry.size.height, alignment: .top)
@@ -308,12 +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(
- t("pubky_auth__description_prefix") + "" + serviceText + "" + t("pubky_auth__description_suffix"),
+ requesterText.isEmpty
+ ? t("pubky_auth__description_prefix") + "" + serviceText + "" + t("pubky_auth__description_suffix")
+ : t("pubky_auth__description_named", variables: ["clientId": requesterText, "service": serviceText]),
accentColor: .textPrimary,
accentFont: Fonts.bold
)
@@ -341,9 +358,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")
+ : t(
+ "pubky_auth__success_named",
+ variables: ["pubky": truncatedPublicKey, "clientId": requesterText, "service": serviceText]
+ ),
accentColor: .textPrimary,
accentFont: Fonts.bold
)
@@ -357,15 +379,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 +398,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..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")
+ }
}
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.