From 235ce880b4a871965170ca4a96634aee9d4e1eaa Mon Sep 17 00:00:00 2001 From: StephenHeaps <5314553+StephenHeaps@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:46:36 -0500 Subject: [PATCH] Fix #7827: Sign In With Ethereum (#8319) * Add view for displaying Sign In With Ethereum / Brave Wallet requests. Add container view for `SignMessageRequest`s. * Use `OriginInfo` to display origin in SIWE details to preserve port & in case removed from mojo interface in future. * Title case * Address PR comment; casing for 'Sign in with Brave Wallet' navigation title. * Address PR comments; rounded rectangle blockie, ViewBuilder overlay modifier, `headline`/`subheadline` fonts, localize domain/message colons, handle action function * Resolve unit test; move String extensions to separate file. --- .../Crypto/Accounts/AccountView.swift | 17 +- ...ormatter.swift => String+Extensions.swift} | 33 ++ Sources/BraveWallet/OriginInfoFavicon.swift | 52 ++ .../Panels/RequestContainerView.swift | 2 +- .../SignInWithEthereumView.swift | 231 +++++++++ .../SignMessageErrorView.swift | 2 +- .../SignMessageRequestContainerView.swift | 92 ++++ .../SignMessageRequestView.swift | 364 ++++++++++++++ .../SignatureRequestView.swift | 451 ------------------ Sources/BraveWallet/WalletStrings.swift | 109 ++++- 10 files changed, 896 insertions(+), 457 deletions(-) rename Sources/BraveWallet/Extensions/{String+NumberFormatter.swift => String+Extensions.swift} (53%) 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..aeece6962e4 100644 --- a/Sources/BraveWallet/Crypto/Accounts/AccountView.swift +++ b/Sources/BraveWallet/Crypto/Accounts/AccountView.swift @@ -11,14 +11,27 @@ 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 + /// Corner radius only applied when `blockShape` is `rectangle`. + @ScaledMetric var cornerRadius = 4 var body: some View { HStack { - Blockie(address: address) - .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/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/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..d161c8fb408 --- /dev/null +++ b/Sources/BraveWallet/Panels/Signature Request/SignInWithEthereumView.swift @@ -0,0 +1,231 @@ +// 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(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 + ) + .ignoresSafeArea() + .allowsHitTesting(false) + ) + } + } + .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( + originInfo: originInfo, + 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) + .font(.headline) + Text(verbatim: statement) + .textSelection(.enabled) + .font(.subheadline) + } + + VStack(alignment: .leading, spacing: 6) { + Text(Strings.Wallet.siweResourcesLabel) + .font(.headline) + ForEach(resources.indices, id: \.self) { index in + if let resource = resources[safe: index] { + Text(verbatim: resource.absoluteString) + .textSelection(.enabled) + .font(.subheadline) + } + } + } + } + } + .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 originInfo: BraveWallet.OriginInfo + let message: BraveWallet.SIWEMessage + + var body: some View { + ScrollView { + LazyVStack { + 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)) + 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)) + } + Group { // 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 { + detailRow(title: title, value: Text(verbatim: value)) + } + + 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..e35d0a151ea --- /dev/null +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestContainerView.swift @@ -0,0 +1,92 @@ +// 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: handleAction(approved:) + ) + } else { // ethSignTypedData, ethStandardSignData, solanaSignData + SignMessageRequestView( + account: currentRequestAccount, + request: currentRequest, + network: currentRequestNetwork, + requestIndex: requestIndex, + requestCount: requests.count, + needPilcrowFormatted: $needPilcrowFormatted, + showOrignalMessage: $showOrignalMessage, + nextTapped: next, + action: handleAction(approved:) + ) + } + } + .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 + } + } + + 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 new file mode 100644 index 00000000000..45987d16a7b --- /dev/null +++ b/Sources/BraveWallet/Panels/Signature Request/SignMessageRequestView.swift @@ -0,0 +1,364 @@ +// 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, 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) + 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), + in: RoundedRectangle(cornerRadius: 5, style: .continuous) + ) + .padding() + .background( + Color(.secondaryBraveGroupedBackground), + in: 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(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 + ) + .ignoresSafeArea() + .allowsHitTesting(false) + ) + } + } + .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)) + } + } + } +} 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..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( @@ -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." + ) } }