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.