Skip to content

Commit

Permalink
Fix brave/brave-ios#7827: Sign In With Ethereum (brave/brave-ios#8319)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
StephenHeaps authored Nov 10, 2023
1 parent 91e8056 commit ed7fa1e
Show file tree
Hide file tree
Showing 10 changed files with 896 additions and 457 deletions.
17 changes: 15 additions & 2 deletions Sources/BraveWallet/Crypto/Accounts/AccountView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
52 changes: 52 additions & 0 deletions Sources/BraveWallet/OriginInfoFavicon.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
2 changes: 1 addition & 1 deletion Sources/BraveWallet/Panels/RequestContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct RequestContainerView<DismissContent: ToolbarContent>: View {
onDismiss: onDismiss
)
case let .signMessage(requests):
SignatureRequestView(
SignMessageRequestContainerView(
requests: requests,
keyringStore: keyringStore,
cryptoStore: cryptoStore,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit ed7fa1e

Please sign in to comment.