Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #7794, #7709: Add Stripe on ramp, buy token tweaks (#7879)
Browse files Browse the repository at this point in the history
* Add support for Stripe OnRamp (US-only)

* Add unit test for `supportedProviders` after moving into `BuyTokenStore` from the view.

* Select USD by default, remove currency code from placeholder

* Use region identifier / code instead of language code to restrict to US.
  • Loading branch information
StephenHeaps authored Sep 19, 2023
1 parent 9434cb6 commit 77eb1d8
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "link-by-stripe-icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,14 @@ struct BuyProviderSelectionView: View {
@ScaledMetric private var iconSize = 40.0
private let maxIconSize: CGFloat = 80.0

private var supportedProviders: OrderedSet<BraveWallet.OnRampProvider> {
return OrderedSet(buyTokenStore.orderedSupportedBuyOptions
.filter { provider in
guard let tokens = buyTokenStore.buyTokens[provider],
let selectedBuyToken = buyTokenStore.selectedBuyToken
else { return false }
return tokens.includes(selectedBuyToken)
})
}

var body: some View {
List {
Section(
header: WalletListHeaderView(
title: Text(Strings.Wallet.providerSelectionSectionHeader)
)
) {
ForEach(supportedProviders) { provider in
ForEach(buyTokenStore.supportedProviders) { provider in
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
Image(provider.iconName, bundle: .module)
Expand All @@ -57,13 +47,10 @@ struct BuyProviderSelectionView: View {
.frame(width: min(iconSize, maxIconSize), height: min(iconSize, maxIconSize))
Button {
Task { @MainActor in
let urlString = await buyTokenStore.fetchBuyUrl(
guard let url = await buyTokenStore.fetchBuyUrl(
provider: provider,
account: keyringStore.selectedAccount
)
guard let urlString = urlString, let url = URL(string: urlString) else {
return
}
) else { return }
openWalletURL(url)
}
} label: {
Expand Down
30 changes: 16 additions & 14 deletions Sources/BraveWallet/Crypto/BuySendSwap/BuyTokenView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct BuyTokenView: View {
@ObservedObject var networkStore: NetworkStore
@ObservedObject var buyTokenStore: BuyTokenStore

@State private var showProviderSelection = false
@State private var isShowingProviderSelection = false

var onDismiss: (() -> Void)

Expand Down Expand Up @@ -52,13 +52,17 @@ struct BuyTokenView: View {
) {
HStack {
Menu {
ForEach(buyTokenStore.supportedCurrencies) { currency in
Button {
buyTokenStore.selectedCurrency = currency
} label: {
Text(currency.currencyCode)
}
}
Picker(
selection: $buyTokenStore.selectedCurrency,
content: {
ForEach(buyTokenStore.supportedCurrencies) { currency in
Text(currency.currencyCode)
.foregroundColor(Color(.secondaryBraveLabel))
.tag(currency)
}
},
label: { EmptyView() } // `Menu` label is used instead
)
} label: {
HStack(spacing: 4) {
Text(buyTokenStore.selectedCurrency.symbol)
Expand All @@ -68,19 +72,17 @@ struct BuyTokenView: View {
.imageScale(.small)
.foregroundColor(Color(.secondaryBraveLabel))
}
.padding(.vertical, 4)
}
TextField(
String.localizedStringWithFormat(Strings.Wallet.amountInCurrency, buyTokenStore.selectedCurrency.currencyCode),
text: $buyTokenStore.buyAmount
)
TextField("0", text: $buyTokenStore.buyAmount)
.keyboardType(.decimalPad)
}
.listRowBackground(Color(.secondaryBraveGroupedBackground))
}
Section(
header: HStack {
Button(action: {
showProviderSelection = true
isShowingProviderSelection = true
}) {
Text(Strings.Wallet.purchaseMethodButtonTitle)
}
Expand Down Expand Up @@ -120,7 +122,7 @@ struct BuyTokenView: View {
)
.background(
NavigationLink(
isActive: $showProviderSelection,
isActive: $isShowingProviderSelection,
destination: {
BuyProviderSelectionView(
buyTokenStore: buyTokenStore,
Expand Down
2 changes: 1 addition & 1 deletion Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class AssetDetailStore: ObservableObject {
}

@MainActor private func isBuyButtonSupported(in network: BraveWallet.NetworkInfo, for symbol: String) async -> Bool {
let buyOptions: [BraveWallet.OnRampProvider] = [.ramp, .sardine, .transak]
let buyOptions: [BraveWallet.OnRampProvider] = Array(BraveWallet.OnRampProvider.allSupportedOnRampProviders)
self.allBuyTokensAllOptions = await blockchainRegistry.allBuyTokens(in: network, for: buyOptions)
let buyTokens = allBuyTokensAllOptions.flatMap { $0.value }
return buyTokens.first(where: { $0.symbol.caseInsensitiveCompare(symbol) == .orderedSame }) != nil
Expand Down
94 changes: 85 additions & 9 deletions Sources/BraveWallet/Crypto/Stores/BuyTokenStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,31 @@ public class BuyTokenStore: ObservableObject {
@Published var selectedCurrency: BraveWallet.OnRampCurrency = .init()

/// A map of list of available tokens to a certain on ramp provider
var buyTokens: [BraveWallet.OnRampProvider: [BraveWallet.BlockchainToken]] = [.ramp: [], .sardine: [], .transak: []]
var buyTokens: [BraveWallet.OnRampProvider: [BraveWallet.BlockchainToken]]
/// A list of all available tokens for all providers
var allTokens: [BraveWallet.BlockchainToken] = []

/// The supported `OnRampProvider`s for the currently selected currency and device locale.
var supportedProviders: OrderedSet<BraveWallet.OnRampProvider> {
return OrderedSet(orderedSupportedBuyOptions
.filter { provider in
guard let tokens = buyTokens[provider],
let selectedBuyToken = selectedBuyToken
else { return false }
// verify selected currency code is supported for this provider
guard supportedCurrencies.contains(where: { supportedOnRampCurrency in
guard supportedOnRampCurrency.providers.contains(.init(integerLiteral: provider.rawValue)) else {
return false
}
let selectedCurrencyCode = selectedCurrency.currencyCode
return supportedOnRampCurrency.currencyCode.caseInsensitiveCompare(selectedCurrencyCode) == .orderedSame
}) else {
return false
}
// verify selected token is supported for this provider
return tokens.includes(selectedBuyToken)
})
}

private let blockchainRegistry: BraveWalletBlockchainRegistry
private let keyringService: BraveWalletKeyringService
Expand Down Expand Up @@ -63,8 +85,14 @@ public class BuyTokenStore: ObservableObject {
self.walletService = walletService
self.assetRatioService = assetRatioService
self.prefilledToken = prefilledToken
self.buyTokens = WalletConstants.supportedOnRampProviders.reduce(
into: [BraveWallet.OnRampProvider: [BraveWallet.BlockchainToken]]()
) {
$0[$1] = []
}

self.rpcService.add(self)
self.keyringService.add(self)

Task {
await updateInfo()
Expand Down Expand Up @@ -98,29 +126,35 @@ public class BuyTokenStore: ObservableObject {
func fetchBuyUrl(
provider: BraveWallet.OnRampProvider,
account: BraveWallet.AccountInfo
) async -> String? {
) async -> URL? {
guard let token = selectedBuyToken else { return nil }

let symbol: String
let currencyCode: String
switch provider {
case .ramp:
symbol = token.rampNetworkSymbol
case .sardine:
symbol = token.symbol
currencyCode = selectedCurrency.currencyCode
case .stripe:
symbol = token.symbol.lowercased()
currencyCode = selectedCurrency.currencyCode.lowercased()
default:
symbol = token.symbol
currencyCode = selectedCurrency.currencyCode
}

let (url, error) = await assetRatioService.buyUrlV1(
let (urlString, error) = await assetRatioService.buyUrlV1(
provider,
chainId: selectedNetwork.chainId,
address: account.address,
symbol: symbol,
amount: buyAmount,
currencyCode: selectedCurrency.currencyCode
currencyCode: currencyCode
)

guard error == nil else { return nil }
guard error == nil, let url = URL(string: urlString) else {
return nil
}

return url
}
Expand Down Expand Up @@ -161,7 +195,7 @@ public class BuyTokenStore: ObservableObject {

@MainActor
func updateInfo() async {
orderedSupportedBuyOptions = [.ramp, .sardine, .transak]
orderedSupportedBuyOptions = BraveWallet.OnRampProvider.allSupportedOnRampProviders

guard let selectedAccount = await keyringService.allAccounts().selectedAccount else {
assertionFailure("selectedAccount should never be nil.")
Expand All @@ -182,7 +216,11 @@ public class BuyTokenStore: ObservableObject {

// fetch all available currencies for on ramp providers
supportedCurrencies = await blockchainRegistry.onRampCurrencies()
if let firstCurrency = supportedCurrencies.first {
if let usdCurrency = supportedCurrencies.first(where: {
$0.currencyCode.caseInsensitiveCompare(CurrencyCode.usd.code) == .orderedSame
}) {
selectedCurrency = usdCurrency
} else if let firstCurrency = supportedCurrencies.first {
selectedCurrency = firstCurrency
}
}
Expand All @@ -202,6 +240,44 @@ extension BuyTokenStore: BraveWalletJsonRpcServiceObserver {
}
}

extension BuyTokenStore: BraveWalletKeyringServiceObserver {
public func keyringCreated(_ keyringId: BraveWallet.KeyringId) {
}

public func keyringRestored(_ keyringId: BraveWallet.KeyringId) {
}

public func keyringReset() {
}

public func locked() {
}

public func unlocked() {
}

public func backedUp() {
}

public func accountsChanged() {
}

public func accountsAdded(_ addedAccounts: [BraveWallet.AccountInfo]) {
}

public func autoLockMinutesChanged() {
}

public func selectedWalletAccountChanged(_ account: BraveWallet.AccountInfo) {
Task { @MainActor in
await updateInfo()
}
}

public func selectedDappAccountChanged(_ coin: BraveWallet.CoinType, account: BraveWallet.AccountInfo?) {
}
}

private extension BraveWallet.BlockchainToken {
var isGasToken: Bool {
guard let gasTokensByChain = BuyTokenStore.gasTokens[chainId] else { return false }
Expand Down
46 changes: 46 additions & 0 deletions Sources/BraveWallet/Extensions/BraveWalletExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import Foundation
import BraveCore
import OrderedCollections

extension BraveWallet.TransactionInfo {
var isSwap: Bool {
Expand Down Expand Up @@ -309,6 +310,9 @@ extension BraveWallet.OnRampProvider {
return Strings.Wallet.sardineProviderName
case .transak:
return Strings.Wallet.transakProviderName
case .stripe:
// Product names not localized
return String.localizedStringWithFormat(Strings.Wallet.stripeNetworkProviderName, "Link", "Stripe")
default:
return ""
}
Expand All @@ -322,6 +326,9 @@ extension BraveWallet.OnRampProvider {
return Strings.Wallet.sardineProviderShortName
case .transak:
return Strings.Wallet.transakProviderShortName
case .stripe:
// Product name is not localized
return "Link"
default:
return ""
}
Expand All @@ -335,6 +342,8 @@ extension BraveWallet.OnRampProvider {
return Strings.Wallet.sardineProviderDescription
case .transak:
return Strings.Wallet.transakProviderDescription
case .stripe:
return Strings.Wallet.stripeNetworkProviderDescription
default:
return ""
}
Expand All @@ -348,10 +357,47 @@ extension BraveWallet.OnRampProvider {
return "sardine-icon"
case .transak:
return "transak-icon"
case .stripe:
return "link-by-stripe-icon"
default:
return ""
}
}

/// Supported local region identifiers / codes for the `OnRampProvider`. Will return nil if all locale region identifiers / codes are supported.
private var supportedLocaleRegionIdentifiers: [String]? {
switch self {
case .stripe:
return ["us"]
default:
return nil
}
}

/// All supported `OnRampProvider`s for users Locale.
static var allSupportedOnRampProviders: OrderedSet<BraveWallet.OnRampProvider> {
.init(WalletConstants.supportedOnRampProviders.filter { onRampProvider in
if let supportedLocaleRegionIdentifiers = onRampProvider.supportedLocaleRegionIdentifiers {
// Check if `Locale` contains any of the `supportedLocaleRegionIdentifiers`
return supportedLocaleRegionIdentifiers.contains(where: { code in
Locale.current.safeRegionCode?.caseInsensitiveCompare(code) == .orderedSame
})
}
// all locale codes/identifiers are supported for this `OnRampProvider`
return true
})
}
}

extension Locale {
/// The region identifier (iOS 16+) or region code for the `Locale`.
var safeRegionCode: String? {
if #available(iOS 16, *) {
return Locale.current.region?.identifier ?? Locale.current.regionCode
} else {
return Locale.current.regionCode
}
}
}

extension BraveWallet.CoinMarket {
Expand Down
6 changes: 6 additions & 0 deletions Sources/BraveWallet/WalletConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public struct WalletConstants {
}
}

/// All of currently supported `OnRampProvider`s.
/// Use `OnRampProvider.allSupportedOnRampProviders` to get providers available for current device locale.
static let supportedOnRampProviders: OrderedSet<BraveWallet.OnRampProvider> = [
.ramp, .sardine, .transak, .stripe
]

/// The supported Ethereum Name Service (ENS) extensions
static let supportedENSExtensions = [".eth"]
/// The supported Solana Name Service (SNS) extensions
Expand Down
Loading

0 comments on commit 77eb1d8

Please sign in to comment.