From 5b5c27c5c969b05125d154df8055ffb8198464c2 Mon Sep 17 00:00:00 2001 From: Stephen Heaps Date: Wed, 25 Oct 2023 14:36:41 -0400 Subject: [PATCH 1/6] Add view for displaying Sign In With Ethereum / Brave Wallet requests. Add container view for `SignMessageRequest`s. --- .../Crypto/Accounts/AccountView.swift | 4 +- Sources/BraveWallet/OriginInfoFavicon.swift | 52 ++ .../Panels/RequestContainerView.swift | 2 +- .../SignInWithEthereumView.swift | 237 +++++++++ .../SignMessageErrorView.swift | 2 +- .../SignMessageRequestContainerView.swift | 95 ++++ .../SignMessageRequestView.swift | 400 ++++++++++++++++ .../SignatureRequestView.swift | 451 ------------------ Sources/BraveWallet/WalletStrings.swift | 105 ++++ 9 files changed, 894 insertions(+), 454 deletions(-) create mode 100644 Sources/BraveWallet/OriginInfoFavicon.swift create mode 100644 Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift create mode 100644 Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift create mode 100644 Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift delete mode 100644 Sources/BraveWallet/Panels/Signature Request/SignatureRequestView.swift diff --git a/Sources/BraveWallet/Crypto/Accounts/AccountView.swift b/Sources/BraveWallet/Crypto/Accounts/AccountView.swift index 58c9be65cdd..3954eef002f 100644 --- a/Sources/BraveWallet/Crypto/Accounts/AccountView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/AccountView.swift @@ -11,13 +11,15 @@ struct AccountView: View { var address: String /// The account name describing what the account is for var name: String + /// The shape of the blockie used + var blockieShape: Blockie.Shape = .circle @ScaledMetric private var avatarSize = 40.0 private let maxAvatarSize: CGFloat = 80.0 var body: some View { HStack { - Blockie(address: address) + Blockie(address: address, shape: blockieShape) .frame(width: min(avatarSize, maxAvatarSize), height: min(avatarSize, maxAvatarSize)) VStack(alignment: .leading, spacing: 2) { Text(name) diff --git a/Sources/BraveWallet/OriginInfoFavicon.swift b/Sources/BraveWallet/OriginInfoFavicon.swift new file mode 100644 index 00000000000..ae88e7bd03b --- /dev/null +++ b/Sources/BraveWallet/OriginInfoFavicon.swift @@ -0,0 +1,52 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import SwiftUI +import BraveCore + +/// Displays the favicon for the OriginInfo, or the Brave Wallet logo for BraveWallet origin. +struct OriginInfoFavicon: View { + + let originInfo: BraveWallet.OriginInfo + + @ScaledMetric var faviconSize: CGFloat = 48 + let maxFaviconSize: CGFloat = 96 + + var body: some View { + Group { + if originInfo.isBraveWalletOrigin { + Image("wallet-brave-icon", bundle: .module) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(4) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(.braveDisabled)) + } else { + if let url = URL(string: originInfo.originSpec) { + FaviconReader(url: url) { image in + if let image = image { + Image(uiImage: image) + .resizable() + } else { + globeFavicon + } + } + } else { + globeFavicon + } + } + } + .frame(width: min(faviconSize, maxFaviconSize), height: min(faviconSize, maxFaviconSize)) + .clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous)) + } + + private var globeFavicon: some View { + Image(systemName: "globe") + .resizable() + .aspectRatio(contentMode: .fit) + .padding(8) + .background(Color(.braveDisabled)) + } +} diff --git a/Sources/BraveWallet/Panels/RequestContainerView.swift b/Sources/BraveWallet/Panels/RequestContainerView.swift index b1edec8a041..435109a4a0b 100644 --- a/Sources/BraveWallet/Panels/RequestContainerView.swift +++ b/Sources/BraveWallet/Panels/RequestContainerView.swift @@ -55,7 +55,7 @@ struct RequestContainerView: View { onDismiss: onDismiss ) case let .signMessage(requests): - SignatureRequestView( + SignMessageRequestContainerView( requests: requests, keyringStore: keyringStore, cryptoStore: cryptoStore, diff --git a/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift b/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift new file mode 100644 index 00000000000..05877cfd534 --- /dev/null +++ b/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift @@ -0,0 +1,237 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import SwiftUI +import BraveStrings +import BraveCore +import DesignSystem + +/// View for showing `SignMessageRequest` for ethSiweData +struct SignInWithEthereumView: View { + + let account: BraveWallet.AccountInfo + let originInfo: BraveWallet.OriginInfo + let message: BraveWallet.SIWEMessage + var action: (_ approved: Bool) -> Void + + @State private var isShowingDetails: Bool = false + @Environment(\.sizeCategory) private var sizeCategory + + var body: some View { + ScrollView { + VStack(spacing: 10) { + faviconAndOrigin + + messageContainer + + buttonsContainer + .padding(.top) + .opacity(sizeCategory.isAccessibilityCategory ? 0 : 1) + .accessibility(hidden: sizeCategory.isAccessibilityCategory) + } + .padding() + } + .overlay( + Group { + if sizeCategory.isAccessibilityCategory { + buttonsContainer + .frame(maxWidth: .infinity) + .padding(.top) + .background( + LinearGradient( + stops: [ + .init(color: Color(.braveGroupedBackground).opacity(0), location: 0), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 0.05), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() + .allowsHitTesting(false) + ) + } + }, + alignment: .bottom + ) + .background(Color(braveSystemName: .containerHighlight)) + .navigationTitle(Strings.Wallet.signInWithBraveWallet) + } + + private var faviconAndOrigin: some View { + VStack(spacing: 8) { + OriginInfoFavicon(originInfo: originInfo) + Text(verbatim: originInfo.eTldPlusOne) + Text(originInfo: originInfo) + .font(.caption) + .foregroundColor(Color(.braveLabel)) + .multilineTextAlignment(.center) + } + } + + private var messageContainer: some View { + VStack(alignment: .leading, spacing: 10) { + AddressView(address: account.address) { + AccountView( + address: account.address, + name: account.name, + blockieShape: .rectangle + ) + } + + // 'You are signing into xyz. Brave Wallet will share your wallet address with xyz.' + Text(String.localizedStringWithFormat( + Strings.Wallet.signInWithBraveWalletMessage, + originInfo.eTldPlusOne, originInfo.eTldPlusOne + )) + + NavigationLink(destination: SignInWithEthereumDetailsView(message: message)) { + Text(Strings.Wallet.seeDetailsButtonTitle) + .fontWeight(.semibold) + .foregroundColor(Color(braveSystemName: .textInteractive)) + .contentShape(Rectangle()) + } + + if let statement = message.statement, let resources = message.resources { + Divider() + + VStack(alignment: .leading, spacing: 6) { + Text(Strings.Wallet.siweMessageLabel) + .fontWeight(.semibold) + Text(verbatim: statement) + .textSelection(.enabled) + } + + VStack(alignment: .leading, spacing: 6) { + Text(Strings.Wallet.siweResourcesLabel) + .fontWeight(.semibold) + ForEach(resources.indices, id: \.self) { index in + if let resource = resources[safe: index] { + Text(verbatim: resource.absoluteString) + .textSelection(.enabled) + } + } + } + } + } + .padding() + .foregroundColor(Color(braveSystemName: .textPrimary)) + .multilineTextAlignment(.leading) + .background( + Color(braveSystemName: .containerBackground) + .cornerRadius(12) + ) + } + + @ViewBuilder private var buttonsContainer: some View { + if sizeCategory.isAccessibilityCategory { + VStack { + buttons + } + } else { + HStack { + buttons + } + } + } + + @ViewBuilder private var buttons: some View { + Button(action: { // cancel + action(false) + }) { + Text(Strings.cancelButtonTitle) + } + .buttonStyle(BraveOutlineButtonStyle(size: .large)) + Button(action: { // approve + action(true) + }) { + Text(Strings.Wallet.siweSignInButtonTitle) + } + .buttonStyle(BraveFilledButtonStyle(size: .large)) + } +} + +/// The view pushed when user taps to view request details. +private struct SignInWithEthereumDetailsView: View { + + let message: BraveWallet.SIWEMessage + + var body: some View { + ScrollView { + LazyVStack { + LazyVStack { + LazyVStack { // Max view count on `LazyVStack` + detailRow(title: Strings.Wallet.siweOriginLabel, value: Text(urlOrigin: message.origin)) + Divider() + detailRow(title: Strings.Wallet.siweAddressLabel, value: Text(verbatim: message.address)) + if let statement = message.statement { + Divider() + detailRow(title: Strings.Wallet.siweStatementLabel, value: Text(verbatim: statement)) + } + Divider() + detailRow(title: Strings.Wallet.siweURILabel, value: Text(verbatim: message.uri.absoluteString)) + } + LazyVStack { // Max view count on `LazyVStack` + Divider() + detailRow(title: Strings.Wallet.siweVersionLabel, value: Text(verbatim: "\(message.version)")) + Divider() + detailRow(title: Strings.Wallet.siweChainIDLabel, value: Text(verbatim: "\(message.chainId)")) + Divider() + detailRow(title: Strings.Wallet.siweIssuedAtLabel, value: Text(verbatim: message.issuedAt)) + if let expirationTime = message.expirationTime { + Divider() + detailRow(title: Strings.Wallet.siweExpirationTimeLabel, value: Text(verbatim: expirationTime)) + } + Divider() + detailRow(title: Strings.Wallet.siweNonceLabel, value: Text(verbatim: message.nonce)) + if let resources = message.resources { + Divider() + detailRow( + title: Strings.Wallet.siweResourcesLabel, + value: Text(verbatim: resources.map(\.absoluteString).joined(separator: "\n")) + ) + } + } + } + .frame(maxWidth: .infinity) + } + .padding(16) + .multilineTextAlignment(.leading) + } + .navigationTitle(Strings.Wallet.siweDetailsTitle) + .navigationBarTitleDisplayMode(.inline) + .background(Color(braveSystemName: .containerHighlight)) + } + + private func detailRow(title: String, value: String) -> some View { + HStack(spacing: 12) { + Text(title) + .fontWeight(.semibold) + .foregroundColor(Color(braveSystemName: .textSecondary)) + .frame(width: 100, alignment: .leading) + Text(verbatim: value) + .foregroundColor(Color(braveSystemName: .textPrimary)) + .textSelection(.enabled) + Spacer() + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + + private func detailRow(title: String, value: Text) -> some View { + HStack(spacing: 12) { + Text(title) + .fontWeight(.semibold) + .foregroundColor(Color(braveSystemName: .textSecondary)) + .frame(width: 100, alignment: .leading) + value + .foregroundColor(Color(braveSystemName: .textPrimary)) + .textSelection(.enabled) + Spacer() + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } +} diff --git a/Sources/BraveWallet/Panels/Signature Request/SignMessageErrorView.swift b/Sources/BraveWallet/Panels/Signature Request/SignMessageErrorView.swift index f8dfe9d1d8b..4f20cceae3f 100644 --- a/Sources/BraveWallet/Panels/Signature Request/SignMessageErrorView.swift +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageErrorView.swift @@ -41,7 +41,7 @@ struct SignMessageErrorView: View { .padding(.top, 16) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(braveSystemName: .containerBackground).ignoresSafeArea()) + .background(Color(braveSystemName: .containerHighlight).ignoresSafeArea()) .navigationTitle(Strings.Wallet.securityRiskDetectedTitle) .navigationBarTitleDisplayMode(.inline) } diff --git a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift new file mode 100644 index 00000000000..8f69b858209 --- /dev/null +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift @@ -0,0 +1,95 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import SwiftUI +import BraveStrings +import BraveCore +import DesignSystem + +/// View for displaying an array of `SignMessageRequest`s` +struct SignMessageRequestContainerView: View { + + var requests: [BraveWallet.SignMessageRequest] + @ObservedObject var keyringStore: KeyringStore + var cryptoStore: CryptoStore + @ObservedObject var networkStore: NetworkStore + var onDismiss: () -> Void + + @State private var requestIndex: Int = 0 + + /// A map between request index and a boolean value indicates this request message needs pilcrow formating + @State private var needPilcrowFormatted: [Int32: Bool] = [0: false] + /// A map between request index and a boolean value indicates this request message is displayed as + /// its original content + @State private var showOrignalMessage: [Int32: Bool] = [0: true] + + /// The current request + private var currentRequest: BraveWallet.SignMessageRequest { + requests[requestIndex] + } + + /// The account for the current request + private var currentRequestAccount: BraveWallet.AccountInfo { + keyringStore.allAccounts.first(where: { $0.address == currentRequest.accountId.address }) ?? keyringStore.selectedAccount + } + + /// The network for the current request + private var currentRequestNetwork: BraveWallet.NetworkInfo? { + networkStore.allChains.first(where: { $0.chainId == currentRequest.chainId }) + } + + var body: some View { + Group { + if let ethSiweData = currentRequest.signData.ethSiweData { + SignInWithEthereumView( + account: currentRequestAccount, + originInfo: currentRequest.originInfo, + message: ethSiweData, + action: { approved in + cryptoStore.handleWebpageRequestResponse(.signMessage(approved: approved, id: currentRequest.id)) + if requests.count <= 1 { + onDismiss() + } + } + ) + } else { // ethSignTypedData, ethStandardSignData, solanaSignData + SignMessageRequestView( + account: currentRequestAccount, + request: currentRequest, + network: currentRequestNetwork, + requestIndex: requestIndex, + requestCount: requests.count, + needPilcrowFormatted: $needPilcrowFormatted, + showOrignalMessage: $showOrignalMessage, + nextTapped: next, + action: { approved in + cryptoStore.handleWebpageRequestResponse(.signMessage(approved: approved, id: currentRequest.id)) + if requests.count <= 1 { + onDismiss() + } + } + ) + } + } + .navigationBarTitleDisplayMode(.inline) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(braveSystemName: .containerHighlight)) + } + + /// Advance to the next (or first if displaying the last) sign message request. + func next() { + if requestIndex + 1 < requests.count { + if let nextRequestId = requests[safe: requestIndex + 1]?.id, + showOrignalMessage[nextRequestId] == nil { + // if we have not previously assigned a `showOriginalMessage` + // value for the next request, assign it the default value now. + showOrignalMessage[nextRequestId] = true + } + requestIndex = requestIndex + 1 + } else { + requestIndex = 0 + } + } +} diff --git a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift new file mode 100644 index 00000000000..f104b217ceb --- /dev/null +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift @@ -0,0 +1,400 @@ +// Copyright 2022 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import SwiftUI +import BraveStrings +import BraveCore +import DesignSystem + +/// View for showing `SignMessageRequest` for +/// ethSignTypedData, ethStandardSignData, & solanaSignData +struct SignMessageRequestView: View { + + let account: BraveWallet.AccountInfo + let request: BraveWallet.SignMessageRequest + let network: BraveWallet.NetworkInfo? + let requestIndex: Int + let requestCount: Int + /// A map between request id and a boolean value indicates this request message needs pilcrow formating. + @Binding var needPilcrowFormatted: [Int32: Bool] + /// A map between request id and a boolean value indicates this request message is displayed as + /// its original content. + @Binding var showOrignalMessage: [Int32: Bool] + var nextTapped: () -> Void + var action: (_ approved: Bool) -> Void + + @Environment(\.sizeCategory) private var sizeCategory + @ScaledMetric private var blockieSize = 54 + private let maxBlockieSize: CGFloat = 108 + private let staticTextViewHeight: CGFloat = 200 + + /// Request display text, used as fallback. + private var requestDisplayText: String { + if requestDomain.isEmpty { + return requestMessage + } + return """ + \(Strings.Wallet.signatureRequestDomainTitle): + \(requestDomain) + + \(Strings.Wallet.signatureRequestMessageTitle): + \(requestMessage) + """ + } + + /// Formatted request display text. Will display with bold `Domain` / `Message` headers if domain is non-empty. + private var requestDisplayAttributedText: NSAttributedString? { + let metrics = UIFontMetrics(forTextStyle: .body) + let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body) + let regularFont = metrics.scaledFont(for: UIFont.systemFont(ofSize: desc.pointSize, weight: .regular)) + let regularAttributes: [NSAttributedString.Key: Any] = [ + .font: regularFont, .foregroundColor: UIColor.braveLabel] + if requestDomain.isEmpty { + // if we don't show domain, we don't need the titles so we + // can fallback to `requestDisplayText` string for perf reasons + return nil + } + let boldFont = metrics.scaledFont(for: UIFont.systemFont(ofSize: desc.pointSize, weight: .bold)) + let boldAttributes: [NSAttributedString.Key: Any] = [ + .font: boldFont, .foregroundColor: UIColor.braveLabel] + + let domainTitle = NSAttributedString(string: "\(Strings.Wallet.signatureRequestDomainTitle):\n", attributes: boldAttributes) + let domain = NSAttributedString(string: requestDomain, attributes: regularAttributes) + let messageTitle = NSAttributedString(string: "\n\(Strings.Wallet.signatureRequestMessageTitle):\n", attributes: boldAttributes) + let message = NSAttributedString(string: requestMessage, attributes: regularAttributes) + + let attrString = NSMutableAttributedString(attributedString: domainTitle) + attrString.append(domain) + attrString.append(messageTitle) + attrString.append(message) + return attrString + } + + private var currentRequestDomain: String? { + request.signData.ethSignTypedData?.domain + } + + private var requestDomain: String { + guard let domain = currentRequestDomain else { return "" } + if showOrignalMessage[request.id] == true { + return domain + } else { + let uuid = UUID() + var result = domain + if needPilcrowFormatted[request.id] == true { + var copy = domain + while copy.range(of: "\\n{2,}", options: .regularExpression) != nil { + if let range = copy.range(of: "\\n{2,}", options: .regularExpression) { + let newlines = String(copy[range]) + result.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") + copy.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") + } + } + } + if domain.hasUnknownUnicode { + result = result.printableWithUnknownUnicode + } + + return result.replacingOccurrences(of: uuid.uuidString, with: "\u{00B6}") + } + } + + private var currentRequestMessage: String? { + if let ethSignTypedData = request.signData.ethSignTypedData { + return ethSignTypedData.message + } else if let ethStandardSignData = request.signData.ethStandardSignData { + return ethStandardSignData.message + } else if let solanaSignData = request.signData.solanaSignData { + return solanaSignData.message + } else { // ethSiweData displayed via `SignInWithEthereumView` + return nil + } + } + + private var requestMessage: String { + guard let message = currentRequestMessage else { + return "" + } + if showOrignalMessage[request.id] == true { + return message + } else { + let uuid = UUID() + var result = message + if needPilcrowFormatted[request.id] == true { + var copy = message + while copy.range(of: "\\n{3,}", options: .regularExpression) != nil { + if let range = copy.range(of: "\\n{3,}", options: .regularExpression) { + let newlines = String(copy[range]) + result.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") + copy.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") + } + } + } + if message.hasUnknownUnicode { + result = result.printableWithUnknownUnicode + } + + return result.replacingOccurrences(of: uuid.uuidString, with: "\u{00B6}") + } + } + + /// Header containing the current requests network chain name, and a `1 of N` & `Next` button when there are multiple requests. + private var requestsHeader: some View { + HStack { + if let network { + Text(network.chainName) + .font(.callout) + .foregroundColor(Color(.braveLabel)) + } + Spacer() + if requestCount > 1 { + NextIndexButton( + currentIndex: requestIndex, + count: requestCount, + nextTapped: nextTapped + ) + } + } + } + + private var accountInfoAndOrigin: some View { + VStack(spacing: 8) { + Blockie(address: account.address) + .frame(width: min(blockieSize, maxBlockieSize), height: min(blockieSize, maxBlockieSize)) + AddressView(address: account.address) { + VStack(spacing: 4) { + Text(account.name) + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(.braveLabel)) + Text(account.address.truncatedAddress) + .font(.subheadline.weight(.semibold)) + .foregroundColor(Color(.secondaryBraveLabel)) + } + } + Text(originInfo: request.originInfo) + .font(.caption) + .foregroundColor(Color(.braveLabel)) + .multilineTextAlignment(.center) + } + .accessibilityElement(children: .combine) + } + + var body: some View { + ScrollView { + VStack { + requestsHeader + + VStack(spacing: 12) { + accountInfoAndOrigin + + Text(Strings.Wallet.signatureRequestSubtitle) + .font(.headline) + .foregroundColor(Color(.bravePrimary)) + + if needPilcrowFormatted[request.id] == true || currentRequestMessage?.hasUnknownUnicode == true { + MessageWarningView( + needsPilcrowFormatted: needPilcrowFormatted[request.id] == true, + hasUnknownUnicode: currentRequestMessage?.hasUnknownUnicode == true, + isShowingOriginalMessage: showOrignalMessage[request.id] == true, + action: { + let value = showOrignalMessage[request.id] ?? false + showOrignalMessage[request.id] = !value + } + ) + } + } + .padding(.vertical, 32) + StaticTextView(text: requestDisplayText, attributedText: requestDisplayAttributedText, isMonospaced: false) + .frame(maxWidth: .infinity) + .frame(height: staticTextViewHeight) + .background(Color(.tertiaryBraveGroupedBackground)) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + .padding() + .background( + Color(.secondaryBraveGroupedBackground) + ) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .introspectTextView { textView in + // A flash to show users message is overflowing the text view (related to issue https://github.com/brave/brave-ios/issues/6277) + if showOrignalMessage[request.id] == true { + let currentRequestHasConsecutiveNewLines = currentRequestDomain?.hasConsecutiveNewLines == true || currentRequestMessage?.hasConsecutiveNewLines == true + if textView.contentSize.height > staticTextViewHeight && currentRequestHasConsecutiveNewLines { + needPilcrowFormatted[request.id] = true + textView.flashScrollIndicators() + } else { + needPilcrowFormatted[request.id] = false + } + } + } + + buttonsContainer + .padding(.top) + .opacity(sizeCategory.isAccessibilityCategory ? 0 : 1) + .accessibility(hidden: sizeCategory.isAccessibilityCategory) + } + .padding() + } + .foregroundColor(Color(.braveLabel)) + .overlay( + Group { + if sizeCategory.isAccessibilityCategory { + buttonsContainer + .frame(maxWidth: .infinity) + .padding(.top) + .background( + LinearGradient( + stops: [ + .init(color: Color(.braveGroupedBackground).opacity(0), location: 0), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 0.05), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + .ignoresSafeArea() + .allowsHitTesting(false) + ) + } + }, + alignment: .bottom + ) + .navigationTitle(Strings.Wallet.signatureRequestTitle) + } + + /// Cancel & Sign button container + @ViewBuilder private var buttonsContainer: some View { + if sizeCategory.isAccessibilityCategory { + VStack { + buttons + } + } else { + HStack { + buttons + } + } + } + + /// Cancel and Sign buttons + @ViewBuilder private var buttons: some View { + Button(action: { // cancel + action(false) + }) { + Label(Strings.cancelButtonTitle, systemImage: "xmark") + .imageScale(.large) + } + .buttonStyle(BraveOutlineButtonStyle(size: .large)) + .disabled(requestIndex != 0) + Button(action: { // approve + action(true) + }) { + Label(Strings.Wallet.sign, braveSystemImage: "leo.key") + .imageScale(.large) + } + .buttonStyle(BraveFilledButtonStyle(size: .large)) + .disabled(requestIndex != 0) + } +} + +/// Yellow background warning view with a button to toggle between showing original message and encoded message. +private struct MessageWarningView: View { + + let needsPilcrowFormatted: Bool + let hasUnknownUnicode: Bool + let isShowingOriginalMessage: Bool + let action: () -> Void + + @Environment(\.pixelLength) private var pixelLength + + var body: some View { + VStack(spacing: 8) { + if needsPilcrowFormatted { + Text("\(Image(systemName: "exclamationmark.triangle.fill")) \(Strings.Wallet.signMessageConsecutiveNewlineWarning)") + .font(.subheadline.weight(.medium)) + .foregroundColor(Color(.braveLabel)) + .multilineTextAlignment(.center) + } + if hasUnknownUnicode { + Text("\(Image(systemName: "exclamationmark.triangle.fill")) \(Strings.Wallet.signMessageRequestUnknownUnicodeWarning)") + .font(.subheadline.weight(.medium)) + .foregroundColor(Color(.braveLabel)) + .multilineTextAlignment(.center) + } + Button { + action() + } label: { + Text(isShowingOriginalMessage ? Strings.Wallet.signMessageShowUnknownUnicode : Strings.Wallet.signMessageShowOriginalMessage) + .font(.subheadline) + .foregroundColor(Color(.braveBlurpleTint)) + } + } + .padding(12) + .frame(maxWidth: .infinity) + .background( + Color(.braveWarningBackground) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .strokeBorder(Color(.braveWarningBorder), style: StrokeStyle(lineWidth: pixelLength)) + ) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + ) + } +} + +/// View that displays the current index, total number of items and a `Next` button to move to next index. +private struct NextIndexButton: View { + + let currentIndex: Int + let count: Int + let nextTapped: () -> Void + + var body: some View { + HStack { + Text(String.localizedStringWithFormat(Strings.Wallet.transactionCount, currentIndex + 1, count)) + .fontWeight(.semibold) + Button(action: { + nextTapped() + }) { + Text(Strings.Wallet.next) + .fontWeight(.semibold) + .foregroundColor(Color(.braveBlurpleTint)) + } + } + } +} + +extension String { + var hasUnknownUnicode: Bool { + // same requirement as desktop. Valid: [0, 127] + for c in unicodeScalars { + let ci = Int(c.value) + if ci > 127 { + return true + } + } + return false + } + + var hasConsecutiveNewLines: Bool { + // return true if string has two or more consecutive newline chars + return range(of: "\\n{3,}", options: .regularExpression) != nil + } + + var printableWithUnknownUnicode: String { + var result = "" + for c in unicodeScalars { + let ci = Int(c.value) + if let unicodeScalar = Unicode.Scalar(ci) { + if ci == 10 { // will keep newline char as it is + result += "\n" + } else { + // ascii char will be displayed as it is + // unknown (> 127) will be displayed as hex-encoded + result += unicodeScalar.escaped(asASCII: true) + } + } + } + return result + } +} diff --git a/Sources/BraveWallet/Panels/Signature Request/SignatureRequestView.swift b/Sources/BraveWallet/Panels/Signature Request/SignatureRequestView.swift deleted file mode 100644 index b067af14a56..00000000000 --- a/Sources/BraveWallet/Panels/Signature Request/SignatureRequestView.swift +++ /dev/null @@ -1,451 +0,0 @@ -// Copyright 2022 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import SwiftUI -import BraveStrings -import BraveCore -import DesignSystem - -struct SignatureRequestView: View { - var requests: [BraveWallet.SignMessageRequest] - @ObservedObject var keyringStore: KeyringStore - var cryptoStore: CryptoStore - @ObservedObject var networkStore: NetworkStore - - var onDismiss: () -> Void - - @State private var requestIndex: Int = 0 - /// A map between request index and a boolean value indicates this request message needs pilcrow formating - @State private var needPilcrowFormatted: [Int: Bool] = [0: false] - /// A map between request index and a boolean value indicates this request message is displayed as - /// its original content - @State private var showOrignalMessage: [Int: Bool] = [0: true] - @Environment(\.sizeCategory) private var sizeCategory - @Environment(\.presentationMode) @Binding private var presentationMode - @Environment(\.pixelLength) private var pixelLength - @ScaledMetric private var blockieSize = 54 - private let maxBlockieSize: CGFloat = 108 - private let staticTextViewHeight: CGFloat = 200 - - private var currentRequest: BraveWallet.SignMessageRequest { - requests[requestIndex] - } - - private var account: BraveWallet.AccountInfo { - keyringStore.allAccounts.first(where: { $0.address == currentRequest.accountId.address }) ?? keyringStore.selectedAccount - } - - private var network: BraveWallet.NetworkInfo? { - networkStore.allChains.first(where: { $0.chainId == currentRequest.chainId }) - } - - /// Request display text, used as fallback. - private var requestDisplayText: String { - if requestDomain.isEmpty { - return requestMessage - } - return """ - \(Strings.Wallet.signatureRequestDomainTitle): - \(requestDomain) - - \(Strings.Wallet.signatureRequestMessageTitle): - \(requestMessage) - """ - } - - /// Formatted request display text. Will display with bold `Domain` / `Message` headers if domain is non-empty. - private var requestDisplayAttributedText: NSAttributedString? { - let metrics = UIFontMetrics(forTextStyle: .body) - let desc = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body) - let regularFont = metrics.scaledFont(for: UIFont.systemFont(ofSize: desc.pointSize, weight: .regular)) - let regularAttributes: [NSAttributedString.Key: Any] = [ - .font: regularFont, .foregroundColor: UIColor.braveLabel] - if requestDomain.isEmpty { - // if we don't show domain, we don't need the titles so we - // can fallback to `requestDisplayText` string for perf reasons - return nil - } - let boldFont = metrics.scaledFont(for: UIFont.systemFont(ofSize: desc.pointSize, weight: .bold)) - let boldAttributes: [NSAttributedString.Key: Any] = [ - .font: boldFont, .foregroundColor: UIColor.braveLabel] - - let domainTitle = NSAttributedString(string: "\(Strings.Wallet.signatureRequestDomainTitle):\n", attributes: boldAttributes) - let domain = NSAttributedString(string: requestDomain, attributes: regularAttributes) - let messageTitle = NSAttributedString(string: "\n\(Strings.Wallet.signatureRequestMessageTitle):\n", attributes: boldAttributes) - let message = NSAttributedString(string: requestMessage, attributes: regularAttributes) - - let attrString = NSMutableAttributedString(attributedString: domainTitle) - attrString.append(domain) - attrString.append(messageTitle) - attrString.append(message) - return attrString - } - - private var currentRequestDomain: String? { - currentRequest.signData.ethSignTypedData?.domain - } - - private var requestDomain: String { - guard let domain = currentRequestDomain else { return "" } - if showOrignalMessage[requestIndex] == true { - return domain - } else { - let uuid = UUID() - var result = domain - if needPilcrowFormatted[requestIndex] == true { - var copy = domain - while copy.range(of: "\\n{2,}", options: .regularExpression) != nil { - if let range = copy.range(of: "\\n{2,}", options: .regularExpression) { - let newlines = String(copy[range]) - result.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") - copy.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") - } - } - } - if domain.hasUnknownUnicode { - result = result.printableWithUnknownUnicode - } - - return result.replacingOccurrences(of: uuid.uuidString, with: "\u{00B6}") - } - } - - private var currentRequestMessage: String? { - if let ethSignTypedData = currentRequest.signData.ethSignTypedData { - return ethSignTypedData.message - } else if let ethStandardSignData = currentRequest.signData.ethStandardSignData { - return ethStandardSignData.message - } else if let solanaSignData = currentRequest.signData.solanaSignData { - return solanaSignData.message - } else if let ethSiweData = currentRequest.signData.ethSiweData { - // TODO: Replace with custom UI: https://github.com/brave/brave-ios/issues/7827 - var message = "Origin: \(ethSiweData.origin.host)" - message += "\nAddress: \(ethSiweData.address)" - if let statement = ethSiweData.statement { - message += "\nStatement: \(statement)" - } - message += "\nURI: \(ethSiweData.uri.absoluteString)" - message += "\nVersion: \(ethSiweData.version)" - message += "\nChain Id: \(ethSiweData.chainId)" - message += "\nNonce: \(ethSiweData.nonce)" - message += "\nIssued At: \(ethSiweData.issuedAt)" - if let expirationTime = ethSiweData.expirationTime { - message += "\nExpiration Time: \(expirationTime)" - } - return message - } else { // ethSiweData will have separate UI - return nil - } - } - - private var requestMessage: String { - guard let message = currentRequestMessage else { - return "" - } - if showOrignalMessage[requestIndex] == true { - return message - } else { - let uuid = UUID() - var result = message - if needPilcrowFormatted[requestIndex] == true { - var copy = message - while copy.range(of: "\\n{3,}", options: .regularExpression) != nil { - if let range = copy.range(of: "\\n{3,}", options: .regularExpression) { - let newlines = String(copy[range]) - result.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") - copy.replaceSubrange(range, with: "\n\(uuid.uuidString) <\(newlines.count)>\n") - } - } - } - if message.hasUnknownUnicode { - result = result.printableWithUnknownUnicode - } - - return result.replacingOccurrences(of: uuid.uuidString, with: "\u{00B6}") - } - } - - init( - requests: [BraveWallet.SignMessageRequest], - keyringStore: KeyringStore, - cryptoStore: CryptoStore, - networkStore: NetworkStore, - onDismiss: @escaping () -> Void - ) { - assert(!requests.isEmpty) - self.requests = requests - self.keyringStore = keyringStore - self.cryptoStore = cryptoStore - self.networkStore = networkStore - self.onDismiss = onDismiss - } - - var body: some View { - ScrollView(.vertical) { - VStack { - HStack { - if let network { - Text(network.chainName) - .font(.callout) - .foregroundColor(Color(.braveLabel)) - } - Spacer() - if requests.count > 1 { - Text(String.localizedStringWithFormat(Strings.Wallet.transactionCount, requestIndex + 1, requests.count)) - .fontWeight(.semibold) - Button(action: next) { - Text(Strings.Wallet.next) - .fontWeight(.semibold) - .foregroundColor(Color(.braveBlurpleTint)) - } - } - } - VStack(spacing: 12) { - VStack(spacing: 8) { - Blockie(address: account.address) - .frame(width: min(blockieSize, maxBlockieSize), height: min(blockieSize, maxBlockieSize)) - AddressView(address: account.address) { - VStack(spacing: 4) { - Text(account.name) - .font(.subheadline.weight(.semibold)) - .foregroundColor(Color(.braveLabel)) - Text(account.address.truncatedAddress) - .font(.subheadline.weight(.semibold)) - .foregroundColor(Color(.secondaryBraveLabel)) - } - } - Text(originInfo: currentRequest.originInfo) - .font(.caption) - .foregroundColor(Color(.braveLabel)) - .multilineTextAlignment(.center) - } - .accessibilityElement(children: .combine) - Text(Strings.Wallet.signatureRequestSubtitle) - .font(.headline) - .foregroundColor(Color(.bravePrimary)) - if needPilcrowFormatted[requestIndex] == true || currentRequestMessage?.hasUnknownUnicode == true { - VStack(spacing: 8) { - if needPilcrowFormatted[requestIndex] == true { - Text("\(Image(systemName: "exclamationmark.triangle.fill")) \(Strings.Wallet.signMessageConsecutiveNewlineWarning)") - .font(.subheadline.weight(.medium)) - .foregroundColor(Color(.braveLabel)) - .multilineTextAlignment(.center) - } - if currentRequestMessage?.hasUnknownUnicode == true { - Text("\(Image(systemName: "exclamationmark.triangle.fill")) \(Strings.Wallet.signMessageRequestUnknownUnicodeWarning)") - .font(.subheadline.weight(.medium)) - .foregroundColor(Color(.braveLabel)) - .multilineTextAlignment(.center) - } - Button { - let value = showOrignalMessage[requestIndex] ?? false - showOrignalMessage[requestIndex] = !value - } label: { - Text(showOrignalMessage[requestIndex] == true ? Strings.Wallet.signMessageShowUnknownUnicode : Strings.Wallet.signMessageShowOriginalMessage) - .font(.subheadline) - .foregroundColor(Color(.braveBlurpleTint)) - } - } - .padding(12) - .frame(maxWidth: .infinity) - .background( - Color(.braveWarningBackground) - .overlay( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .strokeBorder(Color(.braveWarningBorder), style: StrokeStyle(lineWidth: pixelLength)) - ) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) - ) - } - } - .padding(.vertical, 32) - StaticTextView(text: requestDisplayText, attributedText: requestDisplayAttributedText, isMonospaced: false) - .frame(maxWidth: .infinity) - .frame(height: staticTextViewHeight) - .background(Color(.tertiaryBraveGroupedBackground)) - .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) - .padding() - .background( - Color(.secondaryBraveGroupedBackground) - ) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) - buttonsContainer - .padding(.top) - .opacity(sizeCategory.isAccessibilityCategory ? 0 : 1) - .accessibility(hidden: sizeCategory.isAccessibilityCategory) - } - .padding() - } - .overlay( - Group { - if sizeCategory.isAccessibilityCategory { - buttonsContainer - .frame(maxWidth: .infinity) - .padding(.top) - .background( - LinearGradient( - stops: [ - .init(color: Color(.braveGroupedBackground).opacity(0), location: 0), - .init(color: Color(.braveGroupedBackground).opacity(1), location: 0.05), - .init(color: Color(.braveGroupedBackground).opacity(1), location: 1), - ], - startPoint: .top, - endPoint: .bottom - ) - .ignoresSafeArea() - .allowsHitTesting(false) - ) - } - }, - alignment: .bottom - ) - .frame(maxWidth: .infinity) - .navigationTitle(navigationTitle) - .navigationBarTitleDisplayMode(.inline) - .foregroundColor(Color(.braveLabel)) - .background(Color(.braveGroupedBackground).edgesIgnoringSafeArea(.all)) - .introspectTextView { textView in - // A flash to show users message is overflowing the text view (related to issue https://github.com/brave/brave-ios/issues/6277) - if showOrignalMessage[requestIndex] == true { - let currentRequestHasConsecutiveNewLines = currentRequestDomain?.hasConsecutiveNewLines == true || currentRequestMessage?.hasConsecutiveNewLines == true - if textView.contentSize.height > staticTextViewHeight && currentRequestHasConsecutiveNewLines { - needPilcrowFormatted[requestIndex] = true - textView.flashScrollIndicators() - } else { - needPilcrowFormatted[requestIndex] = false - } - } - } - } - - private var navigationTitle: String { - guard let _ = currentRequest.signData.ethSiweData else { - return Strings.Wallet.signatureRequestTitle - } - return Strings.Wallet.signInWithBraveWallet - } - - private var isButtonsDisabled: Bool { - requestIndex != 0 - } - - @ViewBuilder private var buttonsContainer: some View { - if sizeCategory.isAccessibilityCategory { - VStack { - buttons - } - } else { - HStack { - buttons - } - } - } - - @ViewBuilder private var buttons: some View { - Button(action: { // cancel - cryptoStore.handleWebpageRequestResponse(.signMessage(approved: false, id: currentRequest.id)) - updateState() - if requests.count == 1 { - onDismiss() - } - }) { - Label(Strings.cancelButtonTitle, systemImage: "xmark") - .imageScale(.large) - } - .buttonStyle(BraveOutlineButtonStyle(size: .large)) - .disabled(isButtonsDisabled) - Button(action: { // approve - cryptoStore.handleWebpageRequestResponse(.signMessage(approved: true, id: currentRequest.id)) - updateState() - if requests.count == 1 { - onDismiss() - } - }) { - Label(Strings.Wallet.sign, braveSystemImage: "leo.key") - .imageScale(.large) - } - .buttonStyle(BraveFilledButtonStyle(size: .large)) - .disabled(isButtonsDisabled) - } - - private func updateState() { - var newShowOrignalMessage: [Int: Bool] = [:] - showOrignalMessage.forEach { key, value in - if key != 0 { - newShowOrignalMessage[key - 1] = value - } - } - showOrignalMessage = newShowOrignalMessage - - var newNeedPilcrowFormatted: [Int: Bool] = [:] - needPilcrowFormatted.forEach { key, value in - if key != 0 { - newNeedPilcrowFormatted[key - 1] = value - } - } - needPilcrowFormatted = newNeedPilcrowFormatted - } - - private func next() { - if requestIndex + 1 < requests.count { - let value = requestIndex + 1 - if showOrignalMessage[value] == nil { - showOrignalMessage[value] = true - } - requestIndex = value - } else { - requestIndex = 0 - } - } -} - -extension String { - var hasUnknownUnicode: Bool { - // same requirement as desktop. Valid: [0, 127] - for c in unicodeScalars { - let ci = Int(c.value) - if ci > 127 { - return true - } - } - return false - } - - var hasConsecutiveNewLines: Bool { - // return true if string has two or more consecutive newline chars - return range(of: "\\n{3,}", options: .regularExpression) != nil - } - - var printableWithUnknownUnicode: String { - var result = "" - for c in unicodeScalars { - let ci = Int(c.value) - if let unicodeScalar = Unicode.Scalar(ci) { - if ci == 10 { // will keep newline char as it is - result += "\n" - } else { - // ascii char will be displayed as it is - // unknown (> 127) will be displayed as hex-encoded - result += unicodeScalar.escaped(asASCII: true) - } - } - } - return result - } -} - -#if DEBUG -struct SignatureRequestView_Previews: PreviewProvider { - static var previews: some View { - SignatureRequestView( - requests: [.previewRequest], - keyringStore: .previewStoreWithWalletCreated, - cryptoStore: .previewStore, - networkStore: .previewStore, - onDismiss: { } - ) - } -} -#endif diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index f3dd9eac01e..151575da519 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -4647,5 +4647,110 @@ extension Strings { value: "Security Risk Detected", comment: "The title of the view shown when a security issue is detected with a Sign In With Ethereum request." ) + public static let signInWithBraveWalletMessage = NSLocalizedString( + "wallet.signInWithBraveWalletMessage", + tableName: "BraveWallet", + bundle: .module, + value: "You are signing into %@. Brave Wallet will share your wallet address with %@.", + comment: "The title of the view shown when a security issue is detected with a Sign In With Ethereum request." + ) + public static let seeDetailsButtonTitle = NSLocalizedString( + "wallet.seeDetailsButtonTitle", + tableName: "BraveWallet", + bundle: .module, + value: "See details", + comment: "The title of the button to show details." + ) + public static let siweMessageLabel = NSLocalizedString( + "wallet.siweMessageLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Message", + comment: "The label displayed above the Sign In With Ethereum message." + ) + public static let siweResourcesLabel = NSLocalizedString( + "wallet.siweResourcesLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Resources", + comment: "The label displayed above the Sign In With Ethereum resources." + ) + public static let siweSignInButtonTitle = NSLocalizedString( + "wallet.siweSignInButtonTitle", + tableName: "BraveWallet", + bundle: .module, + value: "Sign In", + comment: "The label displayed on the button to sign in for Sign In With Ethereum/Brave Wallet requests." + ) + public static let siweOriginLabel = NSLocalizedString( + "wallet.siweOriginLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Origin", + comment: "The label displayed in details for Sign In With Ethereum/Brave Wallet requests beside the origin." + ) + public static let siweAddressLabel = NSLocalizedString( + "wallet.siweAddressLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Address", + comment: "The label displayed in details for Sign In With Ethereum/Brave Wallet requests beside the address." + ) + public static let siweURILabel = NSLocalizedString( + "wallet.siweURILabel", + tableName: "BraveWallet", + bundle: .module, + value: "URI", + comment: "The label displayed in details for Sign In With Ethereum/Brave Wallet requests beside the URI." + ) + public static let siweVersionLabel = NSLocalizedString( + "wallet.siweVersionLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Version", + comment: "The label displayed in details for Sign In With Ethereum/Brave Wallet requests beside the version." + ) + public static let siweChainIDLabel = NSLocalizedString( + "wallet.siweChainIDLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Chain ID", + comment: "The label displayed in details for Sign In With Ethereum/Brave Wallet requests beside the chain ID." + ) + public static let siweIssuedAtLabel = NSLocalizedString( + "wallet.siweIssuedAtLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Issued At", + comment: "The label displayed in details for Sign In With Ethereum/Brave Wallet requests beside the issued at date." + ) + public static let siweExpirationTimeLabel = NSLocalizedString( + "wallet.siweExpirationTimeLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Expiration Time", + comment: "The label displayed in details for Sign In With Ethereum/Brave Wallet requests beside the expiration time." + ) + public static let siweNonceLabel = NSLocalizedString( + "wallet.siweNonceLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Nonce", + comment: "The label displayed in details view for Sign In With Ethereum/Brave Wallet requests beside the nonce." + ) + public static let siweStatementLabel = NSLocalizedString( + "wallet.siweStatementLabel", + tableName: "BraveWallet", + bundle: .module, + value: "Statement", + comment: "The label displayed in details view for Sign In With Ethereum/Brave Wallet requests beside the statement." + ) + public static let siweDetailsTitle = NSLocalizedString( + "wallet.siweDetailsTitle", + tableName: "BraveWallet", + bundle: .module, + value: "Details", + comment: "The title of the details view for Sign In With Ethereum/Brave Wallet requests." + ) } } From fa6fafe540b6a268cd630944b4050d0929948c9f Mon Sep 17 00:00:00 2001 From: Stephen Heaps Date: Thu, 26 Oct 2023 11:45:10 -0400 Subject: [PATCH 2/6] Use `OriginInfo` to display origin in SIWE details to preserve port & in case removed from mojo interface in future. --- .../Signature Request/SignInWithEthereumView.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift b/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift index 05877cfd534..1890ba14466 100644 --- a/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift +++ b/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift @@ -87,7 +87,12 @@ struct SignInWithEthereumView: View { originInfo.eTldPlusOne, originInfo.eTldPlusOne )) - NavigationLink(destination: SignInWithEthereumDetailsView(message: message)) { + NavigationLink( + destination: SignInWithEthereumDetailsView( + originInfo: originInfo, + message: message + ) + ) { Text(Strings.Wallet.seeDetailsButtonTitle) .fontWeight(.semibold) .foregroundColor(Color(braveSystemName: .textInteractive)) @@ -156,6 +161,7 @@ struct SignInWithEthereumView: View { /// The view pushed when user taps to view request details. private struct SignInWithEthereumDetailsView: View { + let originInfo: BraveWallet.OriginInfo let message: BraveWallet.SIWEMessage var body: some View { @@ -163,7 +169,7 @@ private struct SignInWithEthereumDetailsView: View { LazyVStack { LazyVStack { LazyVStack { // Max view count on `LazyVStack` - detailRow(title: Strings.Wallet.siweOriginLabel, value: Text(urlOrigin: message.origin)) + detailRow(title: Strings.Wallet.siweOriginLabel, value: Text(originInfo: originInfo)) Divider() detailRow(title: Strings.Wallet.siweAddressLabel, value: Text(verbatim: message.address)) if let statement = message.statement { From d253b6f63dff50a034c11c31b3df2d1baa261f18 Mon Sep 17 00:00:00 2001 From: Stephen Heaps Date: Thu, 26 Oct 2023 12:11:23 -0400 Subject: [PATCH 3/6] Title case --- Sources/BraveWallet/WalletStrings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index 151575da519..68c252c9e47 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -4637,7 +4637,7 @@ extension Strings { "wallet.signInWithBraveWallet", tableName: "BraveWallet", bundle: .module, - value: "Sign in with Brave Wallet", + value: "Sign In with Brave Wallet", comment: "The title of the view shown above a Sign In With Ethereum request." ) public static let securityRiskDetectedTitle = NSLocalizedString( From 246ea51e04fcec7b603166f559bfa4fcee971e8d Mon Sep 17 00:00:00 2001 From: Stephen Heaps Date: Wed, 1 Nov 2023 13:19:35 -0400 Subject: [PATCH 4/6] Address PR comment; casing for 'Sign in with Brave Wallet' navigation title. --- Sources/BraveWallet/WalletStrings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index 68c252c9e47..151575da519 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -4637,7 +4637,7 @@ extension Strings { "wallet.signInWithBraveWallet", tableName: "BraveWallet", bundle: .module, - value: "Sign In with Brave Wallet", + value: "Sign in with Brave Wallet", comment: "The title of the view shown above a Sign In With Ethereum request." ) public static let securityRiskDetectedTitle = NSLocalizedString( From f2266bff52d4dea593b1ad6f164c6a6c172b96d9 Mon Sep 17 00:00:00 2001 From: Stephen Heaps Date: Wed, 1 Nov 2023 16:17:31 -0400 Subject: [PATCH 5/6] Address PR comments; rounded rectangle blockie, ViewBuilder overlay modifier, `headline`/`subheadline` fonts, localize domain/message colons, handle action function --- .../Crypto/Accounts/AccountView.swift | 15 ++++- .../SignInWithEthereumView.swift | 64 ++++++++---------- .../SignMessageRequestContainerView.swift | 21 +++--- .../SignMessageRequestView.swift | 65 +++++++++---------- Sources/BraveWallet/WalletStrings.swift | 4 +- 5 files changed, 82 insertions(+), 87 deletions(-) diff --git a/Sources/BraveWallet/Crypto/Accounts/AccountView.swift b/Sources/BraveWallet/Crypto/Accounts/AccountView.swift index 3954eef002f..aeece6962e4 100644 --- a/Sources/BraveWallet/Crypto/Accounts/AccountView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/AccountView.swift @@ -16,11 +16,22 @@ struct AccountView: View { @ScaledMetric private var avatarSize = 40.0 private let maxAvatarSize: CGFloat = 80.0 + /// Corner radius only applied when `blockShape` is `rectangle`. + @ScaledMetric var cornerRadius = 4 var body: some View { HStack { - Blockie(address: address, shape: blockieShape) - .frame(width: min(avatarSize, maxAvatarSize), height: min(avatarSize, maxAvatarSize)) + Group { + if blockieShape == .rectangle { + Blockie(address: address, shape: blockieShape) + .frame(width: min(avatarSize, maxAvatarSize), height: min(avatarSize, maxAvatarSize)) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + } else { + Blockie(address: address, shape: blockieShape) + .frame(width: min(avatarSize, maxAvatarSize), height: min(avatarSize, maxAvatarSize)) + .clipShape(Circle()) + } + } VStack(alignment: .leading, spacing: 2) { Text(name) .fontWeight(.semibold) diff --git a/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift b/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift index 1890ba14466..d161c8fb408 100644 --- a/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift +++ b/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift @@ -33,29 +33,26 @@ struct SignInWithEthereumView: View { } .padding() } - .overlay( - Group { - if sizeCategory.isAccessibilityCategory { - buttonsContainer - .frame(maxWidth: .infinity) - .padding(.top) - .background( - LinearGradient( - stops: [ - .init(color: Color(.braveGroupedBackground).opacity(0), location: 0), - .init(color: Color(.braveGroupedBackground).opacity(1), location: 0.05), - .init(color: Color(.braveGroupedBackground).opacity(1), location: 1), - ], - startPoint: .top, - endPoint: .bottom - ) - .ignoresSafeArea() - .allowsHitTesting(false) + .overlay(alignment: .bottom) { + if sizeCategory.isAccessibilityCategory { + buttonsContainer + .frame(maxWidth: .infinity) + .padding(.top) + .background( + LinearGradient( + stops: [ + .init(color: Color(.braveGroupedBackground).opacity(0), location: 0), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 0.05), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 1), + ], + startPoint: .top, + endPoint: .bottom ) - } - }, - alignment: .bottom - ) + .ignoresSafeArea() + .allowsHitTesting(false) + ) + } + } .background(Color(braveSystemName: .containerHighlight)) .navigationTitle(Strings.Wallet.signInWithBraveWallet) } @@ -104,18 +101,20 @@ struct SignInWithEthereumView: View { VStack(alignment: .leading, spacing: 6) { Text(Strings.Wallet.siweMessageLabel) - .fontWeight(.semibold) + .font(.headline) Text(verbatim: statement) .textSelection(.enabled) + .font(.subheadline) } VStack(alignment: .leading, spacing: 6) { Text(Strings.Wallet.siweResourcesLabel) - .fontWeight(.semibold) + .font(.headline) ForEach(resources.indices, id: \.self) { index in if let resource = resources[safe: index] { Text(verbatim: resource.absoluteString) .textSelection(.enabled) + .font(.subheadline) } } } @@ -168,7 +167,7 @@ private struct SignInWithEthereumDetailsView: View { ScrollView { LazyVStack { LazyVStack { - LazyVStack { // Max view count on `LazyVStack` + Group { // Max view count on `LazyVStack` detailRow(title: Strings.Wallet.siweOriginLabel, value: Text(originInfo: originInfo)) Divider() detailRow(title: Strings.Wallet.siweAddressLabel, value: Text(verbatim: message.address)) @@ -179,7 +178,7 @@ private struct SignInWithEthereumDetailsView: View { Divider() detailRow(title: Strings.Wallet.siweURILabel, value: Text(verbatim: message.uri.absoluteString)) } - LazyVStack { // Max view count on `LazyVStack` + Group { // Max view count on `LazyVStack` Divider() detailRow(title: Strings.Wallet.siweVersionLabel, value: Text(verbatim: "\(message.version)")) Divider() @@ -212,18 +211,7 @@ private struct SignInWithEthereumDetailsView: View { } private func detailRow(title: String, value: String) -> some View { - HStack(spacing: 12) { - Text(title) - .fontWeight(.semibold) - .foregroundColor(Color(braveSystemName: .textSecondary)) - .frame(width: 100, alignment: .leading) - Text(verbatim: value) - .foregroundColor(Color(braveSystemName: .textPrimary)) - .textSelection(.enabled) - Spacer() - } - .padding(.vertical, 8) - .frame(maxWidth: .infinity) + detailRow(title: title, value: Text(verbatim: value)) } private func detailRow(title: String, value: Text) -> some View { diff --git a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift index 8f69b858209..e35d0a151ea 100644 --- a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift @@ -47,12 +47,7 @@ struct SignMessageRequestContainerView: View { account: currentRequestAccount, originInfo: currentRequest.originInfo, message: ethSiweData, - action: { approved in - cryptoStore.handleWebpageRequestResponse(.signMessage(approved: approved, id: currentRequest.id)) - if requests.count <= 1 { - onDismiss() - } - } + action: handleAction(approved:) ) } else { // ethSignTypedData, ethStandardSignData, solanaSignData SignMessageRequestView( @@ -64,12 +59,7 @@ struct SignMessageRequestContainerView: View { needPilcrowFormatted: $needPilcrowFormatted, showOrignalMessage: $showOrignalMessage, nextTapped: next, - action: { approved in - cryptoStore.handleWebpageRequestResponse(.signMessage(approved: approved, id: currentRequest.id)) - if requests.count <= 1 { - onDismiss() - } - } + action: handleAction(approved:) ) } } @@ -92,4 +82,11 @@ struct SignMessageRequestContainerView: View { requestIndex = 0 } } + + private func handleAction(approved: Bool) { + cryptoStore.handleWebpageRequestResponse(.signMessage(approved: approved, id: currentRequest.id)) + if requests.count <= 1 { + onDismiss() + } + } } diff --git a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift index f104b217ceb..9745add1a5a 100644 --- a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift @@ -36,10 +36,10 @@ struct SignMessageRequestView: View { return requestMessage } return """ - \(Strings.Wallet.signatureRequestDomainTitle): + \(Strings.Wallet.signatureRequestDomainTitle) \(requestDomain) - \(Strings.Wallet.signatureRequestMessageTitle): + \(Strings.Wallet.signatureRequestMessageTitle) \(requestMessage) """ } @@ -60,10 +60,10 @@ struct SignMessageRequestView: View { let boldAttributes: [NSAttributedString.Key: Any] = [ .font: boldFont, .foregroundColor: UIColor.braveLabel] - let domainTitle = NSAttributedString(string: "\(Strings.Wallet.signatureRequestDomainTitle):\n", attributes: boldAttributes) - let domain = NSAttributedString(string: requestDomain, attributes: regularAttributes) - let messageTitle = NSAttributedString(string: "\n\(Strings.Wallet.signatureRequestMessageTitle):\n", attributes: boldAttributes) - let message = NSAttributedString(string: requestMessage, attributes: regularAttributes) + let domainTitle = NSAttributedString(string: Strings.Wallet.signatureRequestDomainTitle, attributes: boldAttributes) + let domain = NSAttributedString(string: "\n\(requestDomain)\n\n", attributes: regularAttributes) + let messageTitle = NSAttributedString(string: Strings.Wallet.signatureRequestMessageTitle, attributes: boldAttributes) + let message = NSAttributedString(string: "\n\(requestMessage)", attributes: regularAttributes) let attrString = NSMutableAttributedString(attributedString: domainTitle) attrString.append(domain) @@ -209,13 +209,15 @@ struct SignMessageRequestView: View { StaticTextView(text: requestDisplayText, attributedText: requestDisplayAttributedText, isMonospaced: false) .frame(maxWidth: .infinity) .frame(height: staticTextViewHeight) - .background(Color(.tertiaryBraveGroupedBackground)) - .clipShape(RoundedRectangle(cornerRadius: 5, style: .continuous)) + .background( + Color(.tertiaryBraveGroupedBackground), + in: RoundedRectangle(cornerRadius: 5, style: .continuous) + ) .padding() .background( - Color(.secondaryBraveGroupedBackground) + Color(.secondaryBraveGroupedBackground), + in: RoundedRectangle(cornerRadius: 10, style: .continuous) ) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .introspectTextView { textView in // A flash to show users message is overflowing the text view (related to issue https://github.com/brave/brave-ios/issues/6277) if showOrignalMessage[request.id] == true { @@ -237,29 +239,26 @@ struct SignMessageRequestView: View { .padding() } .foregroundColor(Color(.braveLabel)) - .overlay( - Group { - if sizeCategory.isAccessibilityCategory { - buttonsContainer - .frame(maxWidth: .infinity) - .padding(.top) - .background( - LinearGradient( - stops: [ - .init(color: Color(.braveGroupedBackground).opacity(0), location: 0), - .init(color: Color(.braveGroupedBackground).opacity(1), location: 0.05), - .init(color: Color(.braveGroupedBackground).opacity(1), location: 1), - ], - startPoint: .top, - endPoint: .bottom - ) - .ignoresSafeArea() - .allowsHitTesting(false) + .overlay(alignment: .bottom) { + if sizeCategory.isAccessibilityCategory { + buttonsContainer + .frame(maxWidth: .infinity) + .padding(.top) + .background( + LinearGradient( + stops: [ + .init(color: Color(.braveGroupedBackground).opacity(0), location: 0), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 0.05), + .init(color: Color(.braveGroupedBackground).opacity(1), location: 1), + ], + startPoint: .top, + endPoint: .bottom ) - } - }, - alignment: .bottom - ) + .ignoresSafeArea() + .allowsHitTesting(false) + ) + } + } .navigationTitle(Strings.Wallet.signatureRequestTitle) } @@ -364,7 +363,7 @@ private struct NextIndexButton: View { } } -extension String { +fileprivate extension String { var hasUnknownUnicode: Bool { // same requirement as desktop. Valid: [0, 127] for c in unicodeScalars { diff --git a/Sources/BraveWallet/WalletStrings.swift b/Sources/BraveWallet/WalletStrings.swift index 151575da519..f7ca3a6759a 100644 --- a/Sources/BraveWallet/WalletStrings.swift +++ b/Sources/BraveWallet/WalletStrings.swift @@ -2659,14 +2659,14 @@ extension Strings { "wallet.signatureRequestDomainTitle", tableName: "BraveWallet", bundle: .module, - value: "Domain", + value: "Domain:", comment: "A title displayed inside the text view in Signature Request View above the request's domain information." ) public static let signatureRequestMessageTitle = NSLocalizedString( "wallet.signatureRequestMessageTitle", tableName: "BraveWallet", bundle: .module, - value: "Message", + value: "Message:", comment: "A title displayed inside the text view in Signature Request View above the request's message." ) public static let sign = NSLocalizedString( From 4ee60d6d621ab4aaf604f9a0e5192aaabd36407b Mon Sep 17 00:00:00 2001 From: Stephen Heaps Date: Wed, 1 Nov 2023 16:41:15 -0400 Subject: [PATCH 6/6] Resolve unit test; move String extensions to separate file. --- ...ormatter.swift => String+Extensions.swift} | 33 +++++++++++++++++ .../SignMessageRequestView.swift | 35 ------------------- 2 files changed, 33 insertions(+), 35 deletions(-) rename Sources/BraveWallet/Extensions/{String+NumberFormatter.swift => String+Extensions.swift} (53%) diff --git a/Sources/BraveWallet/Extensions/String+NumberFormatter.swift b/Sources/BraveWallet/Extensions/String+Extensions.swift similarity index 53% rename from Sources/BraveWallet/Extensions/String+NumberFormatter.swift rename to Sources/BraveWallet/Extensions/String+Extensions.swift index 06e4376f97b..aa6427c1cc9 100644 --- a/Sources/BraveWallet/Extensions/String+NumberFormatter.swift +++ b/Sources/BraveWallet/Extensions/String+Extensions.swift @@ -22,4 +22,37 @@ extension String { guard let number = String.numberFormatterWithCurrentLocale.number(from: self) else { return self } return String.numberFormatterUsLocale.string(from: number) ?? self } + + var hasUnknownUnicode: Bool { + // same requirement as desktop. Valid: [0, 127] + for c in unicodeScalars { + let ci = Int(c.value) + if ci > 127 { + return true + } + } + return false + } + + var hasConsecutiveNewLines: Bool { + // return true if string has two or more consecutive newline chars + return range(of: "\\n{3,}", options: .regularExpression) != nil + } + + var printableWithUnknownUnicode: String { + var result = "" + for c in unicodeScalars { + let ci = Int(c.value) + if let unicodeScalar = Unicode.Scalar(ci) { + if ci == 10 { // will keep newline char as it is + result += "\n" + } else { + // ascii char will be displayed as it is + // unknown (> 127) will be displayed as hex-encoded + result += unicodeScalar.escaped(asASCII: true) + } + } + } + return result + } } diff --git a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift index 9745add1a5a..45987d16a7b 100644 --- a/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift @@ -362,38 +362,3 @@ private struct NextIndexButton: View { } } } - -fileprivate extension String { - var hasUnknownUnicode: Bool { - // same requirement as desktop. Valid: [0, 127] - for c in unicodeScalars { - let ci = Int(c.value) - if ci > 127 { - return true - } - } - return false - } - - var hasConsecutiveNewLines: Bool { - // return true if string has two or more consecutive newline chars - return range(of: "\\n{3,}", options: .regularExpression) != nil - } - - var printableWithUnknownUnicode: String { - var result = "" - for c in unicodeScalars { - let ci = Int(c.value) - if let unicodeScalar = Unicode.Scalar(ci) { - if ci == 10 { // will keep newline char as it is - result += "\n" - } else { - // ascii char will be displayed as it is - // unknown (> 127) will be displayed as hex-encoded - result += unicodeScalar.escaped(asASCII: true) - } - } - } - return result - } -}