Smart Banner draft
- SmartBanner object for the Home page basic logic prepared Smart banner on a home page - UI done for the widget itself - Sheets done for the help for the states - Business logic done for all except auto-shielding and shield button - Local notifications dependency implemented - Network checker dependency implemented
|
@ -40,9 +40,11 @@ let package = Package(
|
|||
.library(name: "Home", targets: ["Home"]),
|
||||
.library(name: "KeystoneHandler", targets: ["KeystoneHandler"]),
|
||||
.library(name: "LocalAuthenticationHandler", targets: ["LocalAuthenticationHandler"]),
|
||||
.library(name: "LocalNotification", targets: ["LocalNotification"]),
|
||||
.library(name: "LogsHandler", targets: ["LogsHandler"]),
|
||||
.library(name: "MnemonicClient", targets: ["MnemonicClient"]),
|
||||
.library(name: "Models", targets: ["Models"]),
|
||||
.library(name: "NetworkMonitor", targets: ["NetworkMonitor"]),
|
||||
.library(name: "NotEnoughFreeSpace", targets: ["NotEnoughFreeSpace"]),
|
||||
.library(name: "NumberFormatter", targets: ["NumberFormatter"]),
|
||||
.library(name: "OnboardingFlow", targets: ["OnboardingFlow"]),
|
||||
|
@ -67,6 +69,7 @@ let package = Package(
|
|||
.library(name: "SendForm", targets: ["SendForm"]),
|
||||
.library(name: "ServerSetup", targets: ["ServerSetup"]),
|
||||
.library(name: "Settings", targets: ["Settings"]),
|
||||
.library(name: "SmartBanner", targets: ["SmartBanner"]),
|
||||
.library(name: "SupportDataGenerator", targets: ["SupportDataGenerator"]),
|
||||
.library(name: "SyncProgress", targets: ["SyncProgress"]),
|
||||
.library(name: "ReadTransactionsStorage", targets: ["ReadTransactionsStorage"]),
|
||||
|
@ -414,6 +417,7 @@ let package = Package(
|
|||
"Scan",
|
||||
"Settings",
|
||||
"SDKSynchronizer",
|
||||
"SmartBanner",
|
||||
"SyncProgress",
|
||||
"TransactionList",
|
||||
"UIComponents",
|
||||
|
@ -442,6 +446,14 @@ let package = Package(
|
|||
],
|
||||
path: "Sources/Dependencies/LocalAuthenticationHandler"
|
||||
),
|
||||
.target(
|
||||
name: "LocalNotification",
|
||||
dependencies: [
|
||||
"Generated",
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
|
||||
],
|
||||
path: "Sources/Dependencies/LocalNotification"
|
||||
),
|
||||
.target(
|
||||
name: "LogsHandler",
|
||||
dependencies: [
|
||||
|
@ -468,6 +480,14 @@ let package = Package(
|
|||
],
|
||||
path: "Sources/Models"
|
||||
),
|
||||
.target(
|
||||
name: "NetworkMonitor",
|
||||
dependencies: [
|
||||
"Utils",
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
|
||||
],
|
||||
path: "Sources/Dependencies/NetworkMonitor"
|
||||
),
|
||||
.target(
|
||||
name: "NotEnoughFreeSpace",
|
||||
dependencies: [
|
||||
|
@ -655,6 +675,7 @@ let package = Package(
|
|||
"Generated",
|
||||
"Home",
|
||||
"LocalAuthenticationHandler",
|
||||
"LocalNotification",
|
||||
"MnemonicClient",
|
||||
"Models",
|
||||
"NotEnoughFreeSpace",
|
||||
|
@ -838,6 +859,24 @@ let package = Package(
|
|||
],
|
||||
path: "Sources/Features/Settings"
|
||||
),
|
||||
.target(
|
||||
name: "SmartBanner",
|
||||
dependencies: [
|
||||
"Generated",
|
||||
"LocalNotification",
|
||||
"Models",
|
||||
"NetworkMonitor",
|
||||
"SDKSynchronizer",
|
||||
"UIComponents",
|
||||
"UserPreferencesStorage",
|
||||
"Utils",
|
||||
"WalletStorage",
|
||||
"ZcashSDKEnvironment",
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||
.product(name: "ZcashLightClientKit", package: "ZcashLightClientKit"),
|
||||
],
|
||||
path: "Sources/Features/SmartBanner"
|
||||
),
|
||||
.target(
|
||||
name: "SupportDataGenerator",
|
||||
dependencies: [
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// LocalNotificationInterface.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 04-07-2025.
|
||||
//
|
||||
|
||||
import ComposableArchitecture
|
||||
|
||||
extension DependencyValues {
|
||||
public var localNotification: LocalNotificationClient {
|
||||
get { self[LocalNotificationClient.self] }
|
||||
set { self[LocalNotificationClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct LocalNotificationClient {
|
||||
public enum Constants {
|
||||
public static let walletBackupUUID = "com.zashi.wallet-backup-local-notification"
|
||||
public static let shieldingUUID = "com.zashi.shielding-local-notification"
|
||||
}
|
||||
|
||||
public let clearNotifications: () -> Void
|
||||
public let isShieldingScheduled: () async -> Bool
|
||||
public let isWalletBackupScheduled: () async -> Bool
|
||||
public let scheduleShielding: () -> Void
|
||||
public let scheduleWalletBackup: () -> Void
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// LocalNotificationLiveKey.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 04-07-2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
import ComposableArchitecture
|
||||
import Generated
|
||||
|
||||
extension LocalNotificationClient: DependencyKey {
|
||||
public static let liveValue: LocalNotificationClient = Self.live()
|
||||
|
||||
public static func live() -> Self {
|
||||
return LocalNotificationClient(
|
||||
clearNotifications: {
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(
|
||||
withIdentifiers: [
|
||||
Constants.walletBackupUUID,
|
||||
Constants.shieldingUUID
|
||||
]
|
||||
)
|
||||
},
|
||||
isShieldingScheduled: {
|
||||
await withCheckedContinuation { continuation in
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
let res = requests.contains { $0.identifier == Constants.shieldingUUID }
|
||||
continuation.resume(returning: res)
|
||||
}
|
||||
}
|
||||
},
|
||||
isWalletBackupScheduled: {
|
||||
await withCheckedContinuation { continuation in
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
let res = requests.contains { $0.identifier == Constants.walletBackupUUID }
|
||||
continuation.resume(returning: res)
|
||||
}
|
||||
}
|
||||
},
|
||||
scheduleShielding: {
|
||||
guard let futureDate = Calendar.current.date(byAdding: .hour, value: 48, to: Date()) else {
|
||||
return
|
||||
}
|
||||
|
||||
scheduleNotification(
|
||||
title: L10n.SmartBanner.Content.Shield.title,
|
||||
body: "",
|
||||
date: futureDate,
|
||||
identifier: Constants.shieldingUUID
|
||||
)
|
||||
},
|
||||
scheduleWalletBackup: {
|
||||
guard let futureDate = Calendar.current.date(byAdding: .hour, value: 48, to: Date()) else {
|
||||
return
|
||||
}
|
||||
|
||||
scheduleNotification(
|
||||
title: L10n.SmartBanner.Content.Backup.title,
|
||||
body: L10n.SmartBanner.Content.Backup.info,
|
||||
date: futureDate,
|
||||
identifier: Constants.walletBackupUUID
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension LocalNotificationClient {
|
||||
static func scheduleNotification(
|
||||
title: String,
|
||||
body: String,
|
||||
date: Date,
|
||||
identifier: String
|
||||
) {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in
|
||||
if !granted {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let dateComponents = Calendar.current.dateComponents(
|
||||
[.year, .month, .day, .hour, .minute, .second],
|
||||
from: date
|
||||
)
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = title
|
||||
content.body = body
|
||||
content.userInfo = ["customData": "fizzbuzz"]
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
notificationCenter.add(request)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// NetworkMonitorInterface.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 04-07-2025.
|
||||
//
|
||||
|
||||
import ComposableArchitecture
|
||||
import Combine
|
||||
|
||||
extension DependencyValues {
|
||||
public var networkMonitor: NetworkMonitorClient {
|
||||
get { self[NetworkMonitorClient.self] }
|
||||
set { self[NetworkMonitorClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct NetworkMonitorClient {
|
||||
public let networkMonitorStream: () -> AnyPublisher<Bool, Never>
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// NetworkMonitorLiveKey.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 04-07-2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
import Combine
|
||||
import ComposableArchitecture
|
||||
|
||||
extension NetworkMonitorClient: DependencyKey {
|
||||
public static let liveValue: NetworkMonitorClient = Self.live()
|
||||
|
||||
public static func live() -> Self {
|
||||
let monitor = NWPathMonitor()
|
||||
let queue = DispatchQueue.global(qos: .background)
|
||||
let subject = PassthroughSubject<Bool, Never>()
|
||||
|
||||
return NetworkMonitorClient(
|
||||
networkMonitorStream: {
|
||||
monitor.pathUpdateHandler = {
|
||||
print("__LD status \($0)")
|
||||
subject.send($0.status == .satisfied)
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
|
||||
return subject.eraseToAnyPublisher()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -47,8 +47,7 @@ extension SDKSynchronizerClient: DependencyKey {
|
|||
let synchronizer = SDKSynchronizer(initializer: initializer)
|
||||
|
||||
return SDKSynchronizerClient(
|
||||
stateStream: { synchronizer.stateStream
|
||||
},
|
||||
stateStream: { synchronizer.stateStream },
|
||||
eventStream: { synchronizer.eventStream },
|
||||
exchangeRateUSDStream: { synchronizer.exchangeRateUSDStream },
|
||||
latestState: { synchronizer.latestState },
|
||||
|
|
|
@ -166,8 +166,6 @@ public struct Balances {
|
|||
let storedWallet = try walletStorage.exportWallet()
|
||||
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value())
|
||||
let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, zip32AccountIndex, zcashSDKEnvironment.network.networkType)
|
||||
|
||||
guard let uAddress = try await sdkSynchronizer.getUnifiedAddress(account.id) else { throw "sdkSynchronizer.getUnifiedAddress" }
|
||||
|
||||
let proposal = try await sdkSynchronizer.proposeShielding(account.id, zcashSDKEnvironment.shieldingThreshold, .empty, nil)
|
||||
|
||||
|
|
|
@ -128,13 +128,13 @@ public struct CurrencyConversionSetupView: View {
|
|||
note()
|
||||
.padding(.bottom, 20)
|
||||
|
||||
primaryButton(L10n.CurrencyConversion.enable) {
|
||||
store.send(.enableTapped)
|
||||
}
|
||||
|
||||
secondaryButton(L10n.CurrencyConversion.skipBtn) {
|
||||
store.send(.skipTapped)
|
||||
}
|
||||
|
||||
primaryButton(L10n.CurrencyConversion.enable) {
|
||||
store.send(.enableTapped)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import PartnerKeys
|
|||
import UserPreferencesStorage
|
||||
import Utils
|
||||
import BalanceBreakdown
|
||||
import SmartBanner
|
||||
|
||||
@Reducer
|
||||
public struct Home {
|
||||
|
@ -36,6 +37,7 @@ public struct Home {
|
|||
public var isRateTooltipEnabled = false
|
||||
public var migratingDatabase = true
|
||||
public var moreRequest = false
|
||||
public var smartBannerState = SmartBanner.State.initial
|
||||
public var syncProgressState: SyncProgress.State
|
||||
public var walletConfig: WalletConfig
|
||||
// public var scanBinding = false
|
||||
|
@ -67,7 +69,7 @@ public struct Home {
|
|||
public var inAppBrowserURLKeystone: String {
|
||||
"https://keyst.one/shop/products/keystone-3-pro?discount=Zashi"
|
||||
}
|
||||
|
||||
|
||||
public init(
|
||||
canRequestReview: Bool = false,
|
||||
migratingDatabase: Bool = true,
|
||||
|
@ -86,6 +88,8 @@ public struct Home {
|
|||
}
|
||||
|
||||
public enum Action: BindableAction, Equatable {
|
||||
case debug
|
||||
|
||||
case accountSwitchTapped
|
||||
case addKeystoneHWWalletTapped
|
||||
case alert(PresentationAction<Action>)
|
||||
|
@ -112,6 +116,7 @@ public struct Home {
|
|||
case sendTapped
|
||||
case settingsTapped
|
||||
case showSynchronizerErrorAlert(ZcashError)
|
||||
case smartBanner(SmartBanner.Action)
|
||||
case synchronizerStateChanged(RedactableSynchronizerState)
|
||||
case syncFailed(ZcashError)
|
||||
case syncProgress(SyncProgress.Action)
|
||||
|
@ -156,8 +161,15 @@ public struct Home {
|
|||
Balances()
|
||||
}
|
||||
|
||||
Scope(state: \.smartBannerState, action: \.smartBanner) {
|
||||
SmartBanner()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .debug:
|
||||
return .send(.smartBanner(.debug))
|
||||
|
||||
case .onAppear:
|
||||
// state.scanState.checkers = [.zcashAddressScanChecker, .requestZecScanChecker]
|
||||
state.appId = PartnerKeys.cbProjectId
|
||||
|
@ -334,6 +346,9 @@ public struct Home {
|
|||
// )
|
||||
return .none
|
||||
|
||||
case .smartBanner(.currencyConversionScreenRequested):
|
||||
return .send(.currencyConversionSetupTapped)
|
||||
|
||||
// More actions
|
||||
case .coinbaseTapped:
|
||||
state.moreRequest = false
|
||||
|
@ -356,6 +371,9 @@ public struct Home {
|
|||
|
||||
case .walletBalances:
|
||||
return .none
|
||||
|
||||
case .smartBanner:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import Models
|
|||
import WalletBalances
|
||||
import Scan
|
||||
import BalanceBreakdown
|
||||
import SmartBanner
|
||||
|
||||
public struct HomeView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
@ -42,21 +43,24 @@ public struct HomeView: View {
|
|||
couldBeHidden: true
|
||||
)
|
||||
.padding(.top, 1)
|
||||
|
||||
if walletStatus == .restoring {
|
||||
SyncProgressView(
|
||||
store: store.scope(
|
||||
state: \.syncProgressState,
|
||||
action: \.syncProgress
|
||||
)
|
||||
)
|
||||
.frame(height: 94)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Asset.Colors.syncProgresBcg.color)
|
||||
.padding(.top, 7)
|
||||
.padding(.bottom, 20)
|
||||
.onTapGesture {
|
||||
store.send(.debug)
|
||||
}
|
||||
|
||||
// if walletStatus == .restoring {
|
||||
// SyncProgressView(
|
||||
// store: store.scope(
|
||||
// state: \.syncProgressState,
|
||||
// action: \.syncProgress
|
||||
// )
|
||||
// )
|
||||
// .frame(height: 94)
|
||||
// .frame(maxWidth: .infinity)
|
||||
// .background(Asset.Colors.syncProgresBcg.color)
|
||||
// .padding(.top, 7)
|
||||
// .padding(.bottom, 20)
|
||||
// }
|
||||
|
||||
HStack(spacing: 8) {
|
||||
button(
|
||||
L10n.Tabs.receive,
|
||||
|
@ -88,9 +92,15 @@ public struct HomeView: View {
|
|||
}
|
||||
.zFont(.medium, size: 12, style: Design.Text.primary)
|
||||
.padding(.top, 24)
|
||||
.padding(.bottom, 32)
|
||||
// .padding(.bottom, 32)
|
||||
.screenHorizontalPadding()
|
||||
|
||||
SmartBannerView(
|
||||
store: store.scope(
|
||||
state: \.smartBannerState,
|
||||
action: \.smartBanner
|
||||
)
|
||||
)
|
||||
// SmartBanner(isOpen: true) {
|
||||
//// EmptyView()
|
||||
// HStack(spacing: 0) {
|
||||
|
@ -156,6 +166,7 @@ public struct HomeView: View {
|
|||
}
|
||||
//.padding(.top, 12)
|
||||
}
|
||||
|
||||
// .popover(
|
||||
// isPresented:
|
||||
// Binding(
|
||||
|
@ -223,75 +234,75 @@ public struct HomeView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.overlayPreferenceValue(ExchangeRateFeaturePreferenceKey.self) { preferences in
|
||||
WithPerceptionTracking {
|
||||
if store.isRateEducationEnabled {
|
||||
GeometryReader { geometry in
|
||||
preferences.map {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
Asset.Assets.coinsSwap.image
|
||||
.zImage(size: 20, style: Design.Text.primary)
|
||||
.padding(10)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Design.Surfaces.bgTertiary.color(colorScheme))
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(L10n.CurrencyConversion.cardTitle)
|
||||
.zFont(size: 14, style: Design.Text.tertiary)
|
||||
|
||||
Text(L10n.CurrencyConversion.title)
|
||||
.zFont(.semiBold, size: 16, style: Design.Text.primary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button {
|
||||
store.send(.currencyConversionCloseTapped)
|
||||
} label: {
|
||||
Asset.Assets.buttonCloseX.image
|
||||
.zImage(size: 20, style: Design.HintTooltips.defaultFg)
|
||||
}
|
||||
.padding(20)
|
||||
.offset(x: 20, y: -20)
|
||||
}
|
||||
|
||||
Button {
|
||||
store.send(.currencyConversionSetupTapped)
|
||||
} label: {
|
||||
Text(L10n.CurrencyConversion.cardButton)
|
||||
.zFont(.semiBold, size: 16, style: Design.Btns.Tertiary.fg)
|
||||
.frame(height: 24)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Design.Btns.Tertiary.bg.color(colorScheme))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Design.Surfaces.bgPrimary.color(colorScheme))
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(Design.Surfaces.strokeSecondary.color(colorScheme))
|
||||
}
|
||||
}
|
||||
.frame(width: geometry.size.width - 40)
|
||||
.offset(x: 20, y: geometry[$0].minY + geometry[$0].height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// .overlayPreferenceValue(ExchangeRateFeaturePreferenceKey.self) { preferences in
|
||||
// WithPerceptionTracking {
|
||||
// if store.isRateEducationEnabled {
|
||||
// GeometryReader { geometry in
|
||||
// preferences.map {
|
||||
// VStack(alignment: .leading, spacing: 0) {
|
||||
// HStack(alignment: .top, spacing: 0) {
|
||||
// Asset.Assets.Icons.coinsSwap.image
|
||||
// .zImage(size: 20, style: Design.Text.primary)
|
||||
// .padding(10)
|
||||
// .background {
|
||||
// Circle()
|
||||
// .fill(Design.Surfaces.bgTertiary.color(colorScheme))
|
||||
// }
|
||||
// .padding(.trailing, 16)
|
||||
//
|
||||
// VStack(alignment: .leading, spacing: 5) {
|
||||
// Text(L10n.CurrencyConversion.cardTitle)
|
||||
// .zFont(size: 14, style: Design.Text.tertiary)
|
||||
//
|
||||
// Text(L10n.CurrencyConversion.title)
|
||||
// .zFont(.semiBold, size: 16, style: Design.Text.primary)
|
||||
// .lineLimit(1)
|
||||
// .minimumScaleFactor(0.5)
|
||||
// }
|
||||
// .padding(.trailing, 16)
|
||||
//
|
||||
// Spacer(minLength: 0)
|
||||
//
|
||||
// Button {
|
||||
// store.send(.currencyConversionCloseTapped)
|
||||
// } label: {
|
||||
// Asset.Assets.buttonCloseX.image
|
||||
// .zImage(size: 20, style: Design.HintTooltips.defaultFg)
|
||||
// }
|
||||
// .padding(20)
|
||||
// .offset(x: 20, y: -20)
|
||||
// }
|
||||
//
|
||||
// Button {
|
||||
// store.send(.currencyConversionSetupTapped)
|
||||
// } label: {
|
||||
// Text(L10n.CurrencyConversion.cardButton)
|
||||
// .zFont(.semiBold, size: 16, style: Design.Btns.Tertiary.fg)
|
||||
// .frame(height: 24)
|
||||
// .frame(maxWidth: .infinity)
|
||||
// .padding(.vertical, 12)
|
||||
// .background {
|
||||
// RoundedRectangle(cornerRadius: 12)
|
||||
// .fill(Design.Btns.Tertiary.bg.color(colorScheme))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .padding(24)
|
||||
// .background {
|
||||
// RoundedRectangle(cornerRadius: 12)
|
||||
// .fill(Design.Surfaces.bgPrimary.color(colorScheme))
|
||||
// .background {
|
||||
// RoundedRectangle(cornerRadius: 12)
|
||||
// .stroke(Design.Surfaces.strokeSecondary.color(colorScheme))
|
||||
// }
|
||||
// }
|
||||
// .frame(width: geometry.size.width - 40)
|
||||
// .offset(x: 20, y: geometry[$0].minY + geometry[$0].height)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//..walletstatusPanel()
|
||||
.applyScreenBackground()
|
||||
.onAppear {
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
//
|
||||
// SmartBanner.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 2025-03-14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Generated
|
||||
|
||||
struct BottomRoundedRectangle: Shape {
|
||||
var radius: CGFloat
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let width = rect.width
|
||||
let height = rect.height
|
||||
|
||||
return Path { path in
|
||||
path.move(to: CGPoint(x: 0, y: 0))
|
||||
path.addLine(to: CGPoint(x: width, y: 0))
|
||||
path.addLine(to: CGPoint(x: width, y: height - radius))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: width - radius, y: height),
|
||||
control: CGPoint(x: width, y: height)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: radius, y: height))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: 0, y: height - radius),
|
||||
control: CGPoint(x: 0, y: height)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: 0, y: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TopRoundedRectangle: Shape {
|
||||
var radius: CGFloat
|
||||
|
||||
var animatableData: CGFloat {
|
||||
get { radius }
|
||||
set { radius = newValue }
|
||||
}
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let width = rect.width
|
||||
let height = rect.height
|
||||
|
||||
return Path { path in
|
||||
path.move(to: CGPoint(x: 0, y: height))
|
||||
path.addLine(to: CGPoint(x: width, y: height))
|
||||
path.addLine(to: CGPoint(x: width, y: radius))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: width - radius, y: 0),
|
||||
control: CGPoint(x: width, y: 0)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: radius, y: 0))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: 0, y: radius),
|
||||
control: CGPoint(x: 0, y: 0)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: 0, y: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Constants {
|
||||
static let fixedHeight: CGFloat = 32
|
||||
static let fixedHeightWithShadow: CGFloat = 36
|
||||
static let shadowHeight: CGFloat = 4
|
||||
}
|
||||
|
||||
public struct SmartBanner<Content: View>: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@State private var realHeight: CGFloat = 100
|
||||
@State private var isOpen = false
|
||||
@State private var isUnhidden = false
|
||||
@State private var height: CGFloat = 0
|
||||
let content: () -> Content?
|
||||
|
||||
var test = false
|
||||
|
||||
public init(isOpen: Bool = false, content: @escaping () -> Content?) {
|
||||
self.content = content
|
||||
// if isOpen {
|
||||
// withAnimation {
|
||||
test = isOpen
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
BottomRoundedRectangle(radius: Constants.fixedHeight)
|
||||
.frame(height: Constants.fixedHeight)
|
||||
.foregroundColor(Design.screenBackground.color(colorScheme))
|
||||
.shadow(color: Design.Text.primary.color(colorScheme).opacity(0.25), radius: 1)
|
||||
.zIndex(1)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
if isOpen {
|
||||
content()
|
||||
.padding(.vertical, 24)
|
||||
.padding(.top, Constants.fixedHeight)
|
||||
.screenHorizontalPadding()
|
||||
}
|
||||
|
||||
TopRoundedRectangle(radius: isOpen ? Constants.fixedHeight : 0)
|
||||
.frame(height: Constants.fixedHeightWithShadow)
|
||||
.foregroundColor(Design.screenBackground.color(colorScheme))
|
||||
.shadow(color: Design.Text.primary.color(colorScheme).opacity(0.1), radius: isOpen ? 1 : 0, y: -1)
|
||||
}
|
||||
.frame(minHeight: Constants.fixedHeight + Constants.shadowHeight)
|
||||
}
|
||||
.background {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
Gradient.Stop(
|
||||
color: colorScheme == .dark
|
||||
? Color(UIColor(red: 0.06, green: 0.06, blue: 0.06, alpha: 1))
|
||||
: Design.Utility.Gray._300.color(colorScheme),
|
||||
location: 0.00
|
||||
),
|
||||
Gradient.Stop(color: Design.screenBackground.color(colorScheme), location: 1.00),
|
||||
],
|
||||
startPoint: UnitPoint(x: 0.5, y: 0.0),
|
||||
endPoint: UnitPoint(x: 0.5, y: 0.8)
|
||||
)
|
||||
}
|
||||
.clipShape( Rectangle() )
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
isOpen.toggle()
|
||||
}
|
||||
}
|
||||
// .task { @MainActor in
|
||||
// try? await Task.sleep(for: .seconds(2))
|
||||
// if test {
|
||||
// withAnimation(.easeInOut(duration: 0.5)) {
|
||||
// isOpen.toggle()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@ public struct ReceiveView: View {
|
|||
|
||||
if let transparentAddress = store.selectedWalletAccount?.transparentAddress {
|
||||
addressBlock(
|
||||
prefixIcon: Asset.Assets.Brandmarks.brandmarkLow.image,
|
||||
prefixIcon: Asset.Assets.Brandmarks.brandmarkKeystone.image,
|
||||
title: L10n.Accounts.Keystone.transparentAddress,
|
||||
address: transparentAddress,
|
||||
iconFg: Design.Text.primary,
|
||||
|
@ -120,9 +120,9 @@ public struct ReceiveView: View {
|
|||
.padding(.top, 24)
|
||||
|
||||
addressBlock(
|
||||
prefixIcon: Asset.Assets.Brandmarks.brandmarkKeystone.image,
|
||||
title: L10n.Accounts.Keystone.transparentAddress,
|
||||
address: transparentAddress,
|
||||
prefixIcon: Asset.Assets.Brandmarks.brandmarkLow.image,
|
||||
title: L10n.Accounts.Zashi.transparentAddress,
|
||||
address: store.transparentAddress,
|
||||
iconFg: Design.Text.primary,
|
||||
iconBg: Design.Surfaces.bgTertiary,
|
||||
bcgColor: Design.Utility.Gray._50.color(colorScheme),
|
||||
|
|
|
@ -75,8 +75,8 @@ extension Root {
|
|||
case .currencyConversionSetup(.skipTapped), .currencyConversionSetup(.enableTapped):
|
||||
state.path = nil
|
||||
state.homeState.isRateEducationEnabled = false
|
||||
return .none
|
||||
|
||||
return .send(.home(.smartBanner(.closeAndCleanupBanner)))
|
||||
|
||||
// MARK: - Home
|
||||
|
||||
case .home(.settingsTapped):
|
||||
|
|
|
@ -195,13 +195,16 @@ extension Root {
|
|||
}
|
||||
|
||||
case .initialization(.registerForSynchronizersUpdate):
|
||||
return .publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.map { $0.redacted }
|
||||
.map(Root.Action.synchronizerStateChanged)
|
||||
}
|
||||
.cancellable(id: CancelStateId, cancelInFlight: true)
|
||||
return .merge(
|
||||
.publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.map { $0.redacted }
|
||||
.map(Root.Action.synchronizerStateChanged)
|
||||
}
|
||||
.cancellable(id: CancelStateId, cancelInFlight: true),
|
||||
.send(.home(.smartBanner(.evaluatePriority1)))
|
||||
)
|
||||
|
||||
case .initialization(.checkWalletConfig):
|
||||
return .publisher {
|
||||
|
@ -494,6 +497,7 @@ extension Root {
|
|||
}
|
||||
state.splashAppeared = true
|
||||
state.isRestoringWallet = false
|
||||
localNotification.clearNotifications()
|
||||
userDefaults.remove(Constants.udIsRestoringWallet)
|
||||
userDefaults.remove(Constants.udLeavesScreenOpen)
|
||||
flexaHandler.signOut()
|
||||
|
|
|
@ -30,6 +30,7 @@ import OSStatusError
|
|||
import AddressBookClient
|
||||
import UserMetadataProvider
|
||||
import AudioServices
|
||||
import LocalNotification
|
||||
|
||||
// Screens
|
||||
//import About
|
||||
|
@ -238,6 +239,7 @@ public struct Root {
|
|||
case flexaTransactionFailed(String)
|
||||
case home(Home.Action)
|
||||
case initialization(InitializationAction)
|
||||
case localNotificationTapped(String)
|
||||
case notEnoughFreeSpace(NotEnoughFreeSpace.Action)
|
||||
// case path(StackActionOf<Path>)
|
||||
case resetZashiFinishProcessing
|
||||
|
@ -299,6 +301,7 @@ public struct Root {
|
|||
@Dependency(\.exchangeRate) var exchangeRate
|
||||
@Dependency(\.flexaHandler) var flexaHandler
|
||||
@Dependency(\.localAuthentication) var localAuthentication
|
||||
@Dependency(\.localNotification) var localNotification
|
||||
@Dependency(\.mainQueue) var mainQueue
|
||||
@Dependency(\.mnemonic) var mnemonic
|
||||
@Dependency(\.numberFormatter) var numberFormatter
|
||||
|
@ -484,6 +487,9 @@ public struct Root {
|
|||
//return
|
||||
// return .none
|
||||
|
||||
case .localNotificationTapped(let identifier):
|
||||
return .send(.home(.smartBanner(.localNotificationTapped(identifier))))
|
||||
|
||||
case .serverSetup:
|
||||
return .none
|
||||
|
||||
|
|
|
@ -0,0 +1,232 @@
|
|||
//
|
||||
// SmartBannerContent.swift
|
||||
// modules
|
||||
//
|
||||
// Created by Lukáš Korba on 04-03-2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Generated
|
||||
import UIComponents
|
||||
|
||||
extension SmartBannerView {
|
||||
func titleStyle() -> Color {
|
||||
Design.Utility.Purple._50.color(.light)
|
||||
}
|
||||
|
||||
func infoStyle() -> Color {
|
||||
Design.Utility.Purple._200.color(.light)
|
||||
}
|
||||
|
||||
@ViewBuilder func priorityContent() -> some View {
|
||||
WithPerceptionTracking {
|
||||
switch store.priorityContent {
|
||||
case .priority1: disconnectedContent()
|
||||
case .priority2: syncingErrorContent()
|
||||
case .priority3: restoringContent()
|
||||
case .priority4: syncingContent()
|
||||
case .priority5: updatingBalanceContent()
|
||||
case .priority6: walletBackupContent()
|
||||
case .priority7: shieldingContent()
|
||||
case .priority8: currencyConversionContent()
|
||||
case .priority9: autoShieldingContent()
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func disconnectedContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Asset.Assets.Icons.wifiOff.image
|
||||
.zImage(size: 20, color: titleStyle())
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.Disconnected.title)
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.Disconnected.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func syncingErrorContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Asset.Assets.Icons.alertTriangle.image
|
||||
.zImage(size: 20, color: titleStyle())
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.SyncingError.title)
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.SyncingError.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func restoringContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
CircularProgressView(progress: store.lastKnownSyncPercentage)
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.Restore.title(String(format: "%0.1f%%", store.lastKnownSyncPercentage * 100)))
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.Restore.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func syncingContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
CircularProgressView(progress: store.lastKnownSyncPercentage)
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.Sync.title(String(format: "%0.1f%%", store.lastKnownSyncPercentage * 100)))
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.Sync.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func updatingBalanceContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Asset.Assets.Icons.loading.image
|
||||
.zImage(size: 20, color: titleStyle())
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.UpdatingBalance.title)
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.UpdatingBalance.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func walletBackupContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Asset.Assets.Icons.alertTriangle.image
|
||||
.zImage(size: 20, color: titleStyle())
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.Backup.title)
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.Backup.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ZashiButton(
|
||||
L10n.SmartBanner.Content.Backup.button,
|
||||
type: .ghost,
|
||||
infinityWidth: false
|
||||
) {
|
||||
store.send(.walletBackupTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func shieldingContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Asset.Assets.Icons.shieldOff.image
|
||||
.zImage(size: 20, color: titleStyle())
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.Shield.title)
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text("\(store.transparentBalance.decimalString()) \(store.tokenName)")
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ZashiButton(
|
||||
L10n.SmartBanner.Content.Shield.button,
|
||||
type: .ghost,
|
||||
infinityWidth: false
|
||||
) {
|
||||
store.send(.shieldTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func currencyConversionContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Asset.Assets.Icons.coinsSwap.image
|
||||
.zImage(size: 20, color: titleStyle())
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.CurrencyConversion.title)
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.CurrencyConversion.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ZashiButton(
|
||||
L10n.SmartBanner.Content.CurrencyConversion.button,
|
||||
type: .ghost,
|
||||
infinityWidth: false
|
||||
) {
|
||||
store.send(.currencyConversionTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func autoShieldingContent() -> some View {
|
||||
HStack(spacing: 0) {
|
||||
Asset.Assets.Icons.shieldZap.image
|
||||
.zImage(size: 20, color: titleStyle())
|
||||
.padding(.trailing, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.SmartBanner.Content.AutoShielding.title)
|
||||
.zFont(.medium, size: 14, color: titleStyle())
|
||||
|
||||
Text(L10n.SmartBanner.Content.AutoShielding.info)
|
||||
.zFont(.medium, size: 12, color: infoStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ZashiButton(
|
||||
L10n.SmartBanner.Content.AutoShielding.button,
|
||||
type: .ghost,
|
||||
infinityWidth: false
|
||||
) {
|
||||
store.send(.autoShieldingTapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
//
|
||||
// SmartBannerHelpSheet.swift
|
||||
// modules
|
||||
//
|
||||
// Created by Lukáš Korba on 04-03-2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Generated
|
||||
import UIComponents
|
||||
|
||||
extension SmartBannerView {
|
||||
@ViewBuilder func helpSheetContent() -> some View {
|
||||
WithPerceptionTracking {
|
||||
switch store.priorityContent {
|
||||
case .priority1: disconnectedHelpContent()
|
||||
case .priority2: syncingErrorHelpContent()
|
||||
case .priority3: restoringHelpContent()
|
||||
case .priority4: syncingHelpContent()
|
||||
case .priority5: updatingBalanceHelpContent()
|
||||
case .priority6: walletBackupHelpContent()
|
||||
case .priority7: shieldingHelpContent()
|
||||
case .priority9: autoShieldingHelpContent()
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func disconnectedHelpContent() -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Asset.Assets.Icons.wifiOff.image
|
||||
.zImage(size: 20, color: Design.Text.primary.color(colorScheme))
|
||||
.padding(10)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Design.Surfaces.bgTertiary.color(colorScheme))
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Diconnected.title)
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Diconnected.info)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 32)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
ZashiButton(L10n.General.ok.uppercased()) {
|
||||
store.send(.closeSheetTapped)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func syncingErrorHelpContent() -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text("Error during sync")
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(store.lastKnownErrorMessage)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 32)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
ZashiButton(
|
||||
"Report",
|
||||
type: .ghost
|
||||
) {
|
||||
|
||||
}
|
||||
.padding(.bottom, 12)
|
||||
|
||||
ZashiButton(L10n.General.ok.uppercased()) {
|
||||
store.send(.closeSheetTapped)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func restoringHelpContent() -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(L10n.SmartBanner.Help.Restore.title)
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Restore.info)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 12)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
bulletpoint(L10n.SmartBanner.Help.Restore.point1)
|
||||
bulletpoint(L10n.SmartBanner.Help.Restore.point2)
|
||||
.padding(.bottom, 32)
|
||||
|
||||
note(L10n.SmartBanner.Help.Restore.warning)
|
||||
.padding(.bottom, 24)
|
||||
|
||||
ZashiButton(L10n.General.ok.uppercased()) {
|
||||
store.send(.closeSheetTapped)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func syncingHelpContent() -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(L10n.SmartBanner.Help.Sync.title)
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Sync.info)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 32)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
ZashiButton(L10n.General.ok.uppercased()) {
|
||||
store.send(.closeSheetTapped)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func updatingBalanceHelpContent() -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Asset.Assets.Icons.loading.image
|
||||
.zImage(size: 20, color: Design.Text.primary.color(colorScheme))
|
||||
.padding(10)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Design.Surfaces.bgTertiary.color(colorScheme))
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(L10n.SmartBanner.Help.UpdatingBalance.title)
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Text(L10n.SmartBanner.Help.UpdatingBalance.info)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 32)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
ZashiButton(L10n.General.ok.uppercased()) {
|
||||
store.send(.closeSheetTapped)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func walletBackupHelpContent() -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Asset.Assets.Icons.alertTriangle.image
|
||||
.zImage(size: 20, color: Design.Text.primary.color(colorScheme))
|
||||
.padding(10)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Design.Surfaces.bgTertiary.color(colorScheme))
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Backup.title)
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Backup.info1)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 12)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Backup.info2)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 12)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
bulletpoint(L10n.SmartBanner.Help.Backup.point1)
|
||||
bulletpoint(L10n.SmartBanner.Help.Backup.point2)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Backup.info3)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 24)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Backup.info4)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 32)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
ZashiButton(
|
||||
L10n.SmartBanner.Help.remindMe,
|
||||
type: .ghost
|
||||
) {
|
||||
store.send(.remindMeLaterTapped(.priority6))
|
||||
}
|
||||
.padding(.bottom, 12)
|
||||
|
||||
ZashiButton(L10n.SmartBanner.Content.Backup.button) {
|
||||
store.send(.walletBackupTapped)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func shieldingHelpContent() -> some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Asset.Assets.shieldTick.image
|
||||
.zImage(size: 20, color: Design.Text.primary.color(colorScheme))
|
||||
.padding(10)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Design.Surfaces.bgTertiary.color(colorScheme))
|
||||
.frame(width: 40, height: 40)
|
||||
}
|
||||
.padding(.top, 32)
|
||||
.padding(.bottom, 12)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Shield.title)
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Shield.info1)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 12)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(L10n.SmartBanner.Help.Shield.info2)
|
||||
.zFont(size: 16, style: Design.Text.tertiary)
|
||||
.padding(.bottom, 32)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text(L10n.SmartBanner.Help.Shield.transparent)
|
||||
.zFont(.medium, size: 16, style: Design.Text.primary)
|
||||
.padding(.trailing, 4)
|
||||
|
||||
Asset.Assets.Icons.shieldOff.image
|
||||
.zImage(size: 16, style: Design.Text.primary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Text("\(store.transparentBalance.decimalString()) \(store.tokenName)")
|
||||
.zFont(.semiBold, size: 20, style: Design.Text.primary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 20)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(Design.Surfaces.bgSecondary.color(colorScheme))
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.stroke(Design.Surfaces.strokeSecondary.color(colorScheme))
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 24)
|
||||
|
||||
ZashiButton(
|
||||
L10n.SmartBanner.Help.remindMe,
|
||||
type: .ghost
|
||||
) {
|
||||
store.send(.remindMeLaterTapped(.priority7))
|
||||
}
|
||||
.padding(.bottom, 12)
|
||||
|
||||
ZashiButton(L10n.SmartBanner.Content.Shield.button) {
|
||||
store.send(.shieldTapped)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func autoShieldingHelpContent() -> some View {
|
||||
Text("autoShieldingHelpContent")
|
||||
.zFont(size: 14, style: Design.Text.primary)
|
||||
.padding(.vertical, 50)
|
||||
}
|
||||
|
||||
@ViewBuilder private func bulletpoint(_ text: String) -> some View {
|
||||
HStack(alignment: .top) {
|
||||
Circle()
|
||||
.fill(Design.Text.tertiary.color(colorScheme))
|
||||
.frame(width: 4, height: 4)
|
||||
.padding(.top, 7)
|
||||
.padding(.leading, 8)
|
||||
|
||||
Text(text)
|
||||
.zFont(size: 14, style: Design.Text.tertiary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
|
||||
@ViewBuilder private func note(_ text: String) -> some View {
|
||||
VStack {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
Asset.Assets.infoCircle.image
|
||||
.zImage(size: 20, style: Design.Text.tertiary)
|
||||
.padding(.trailing, 12)
|
||||
|
||||
Text(text)
|
||||
.zFont(size: 12, style: Design.Text.tertiary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
//
|
||||
// SmartBannerStore.swift
|
||||
// modules
|
||||
//
|
||||
// Created by Lukáš Korba on 03.04.2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
import ZcashLightClientKit
|
||||
|
||||
import Generated
|
||||
import SDKSynchronizer
|
||||
import Utils
|
||||
import Models
|
||||
import WalletStorage
|
||||
import UserPreferencesStorage
|
||||
import UIComponents
|
||||
import NetworkMonitor
|
||||
import ZcashSDKEnvironment
|
||||
import LocalNotification
|
||||
|
||||
@Reducer
|
||||
public struct SmartBanner {
|
||||
enum Constants: Equatable {
|
||||
static let easeInOutDuration = 0.85
|
||||
}
|
||||
|
||||
@ObservableState
|
||||
public struct State: Equatable {
|
||||
public enum PriorityContent: Int {
|
||||
case priority1 = 0 // disconnected
|
||||
case priority2 // syncing error
|
||||
case priority3 // restoring
|
||||
case priority4 // syncing
|
||||
case priority5 // updating balance
|
||||
case priority6 // wallet backup
|
||||
case priority7 // shielding
|
||||
case priority8 // currency conversion
|
||||
case priority9 // auto-shielding
|
||||
|
||||
public func next() -> PriorityContent {
|
||||
PriorityContent.init(rawValue: self.rawValue - 1) ?? .priority9
|
||||
}
|
||||
}
|
||||
|
||||
public var CancelNetworkMonitorId = UUID()
|
||||
public var CancelStateStreamId = UUID()
|
||||
|
||||
public var delay = 1.5
|
||||
public var isOpen = false
|
||||
public var isSmartBannerSheetPresented = false
|
||||
public var lastKnownErrorMessage = ""
|
||||
public var lastKnownSyncPercentage = 0.0
|
||||
public var priorityContent: PriorityContent? = nil
|
||||
public var priorityContentRequested: PriorityContent? = nil
|
||||
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
|
||||
public var synchronizerStatusSnapshot: SyncStatusSnapshot = .snapshotFor(state: .unprepared)
|
||||
public var tokenName = "ZEC"
|
||||
public var transparentBalance = Zatoshi(0)
|
||||
@Shared(.inMemory(.walletStatus)) public var walletStatus: WalletStatus = .none
|
||||
|
||||
public init() { }
|
||||
}
|
||||
|
||||
public enum Action: BindableAction, Equatable {
|
||||
case debug
|
||||
|
||||
case binding(BindingAction<SmartBanner.State>)
|
||||
case closeAndCleanupBanner
|
||||
case closeBanner(Bool)
|
||||
case closeSheetTapped
|
||||
case onAppear
|
||||
case onDisappear
|
||||
case evaluatePriority1
|
||||
case evaluatePriority2
|
||||
case evaluatePriority3
|
||||
case evaluatePriority4
|
||||
case evaluatePriority5
|
||||
case evaluatePriority6
|
||||
case evaluatePriority7
|
||||
case evaluatePriority8
|
||||
case evaluatePriority9
|
||||
case localNotificationTapped(String)
|
||||
case networkMonitorChanged(Bool)
|
||||
case openBanner
|
||||
case openBannerRequest
|
||||
case remindMeLaterTapped(State.PriorityContent)
|
||||
case smartBannerContentTapped
|
||||
case synchronizerStateChanged(RedactableSynchronizerState)
|
||||
case transparentBalanceUpdated(Zatoshi)
|
||||
case triggerPriority(State.PriorityContent)
|
||||
|
||||
// Action buttons
|
||||
case autoShieldingTapped
|
||||
case currencyConversionScreenRequested
|
||||
case currencyConversionTapped
|
||||
case shieldTapped
|
||||
case walletBackupTapped
|
||||
}
|
||||
|
||||
@Dependency(\.localNotification) var localNotification
|
||||
@Dependency(\.mainQueue) var mainQueue
|
||||
@Dependency(\.networkMonitor) var networkMonitor
|
||||
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
|
||||
@Dependency(\.userStoredPreferences) var userStoredPreferences
|
||||
@Dependency(\.walletStorage) var walletStorage
|
||||
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
|
||||
|
||||
public init() { }
|
||||
|
||||
public var body: some Reducer<State, Action> {
|
||||
BindingReducer()
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .debug:
|
||||
if state.priorityContentRequested == nil {
|
||||
state.priorityContentRequested = .priority9
|
||||
} else {
|
||||
state.priorityContentRequested = state.priorityContentRequested?.next()
|
||||
if state.priorityContentRequested == .priority9 {
|
||||
state.priorityContent = nil
|
||||
}
|
||||
}
|
||||
return .send(.openBannerRequest)
|
||||
|
||||
case .onAppear:
|
||||
state.tokenName = zcashSDKEnvironment.tokenName
|
||||
return .merge(
|
||||
.publisher {
|
||||
networkMonitor.networkMonitorStream()
|
||||
.map(Action.networkMonitorChanged)
|
||||
.receive(on: mainQueue)
|
||||
}
|
||||
.cancellable(id: state.CancelNetworkMonitorId, cancelInFlight: true),
|
||||
.publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.map { $0.redacted }
|
||||
.map(Action.synchronizerStateChanged)
|
||||
}
|
||||
.cancellable(id: state.CancelStateStreamId, cancelInFlight: true)
|
||||
)
|
||||
|
||||
case .onDisappear:
|
||||
return .merge(
|
||||
.cancel(id: state.CancelNetworkMonitorId),
|
||||
.cancel(id: state.CancelStateStreamId)
|
||||
)
|
||||
|
||||
case .binding:
|
||||
return .none
|
||||
|
||||
case .networkMonitorChanged(let isConnected):
|
||||
if state.priorityContent == .priority1 && isConnected {
|
||||
return .run { send in
|
||||
await send(.closeAndCleanupBanner)
|
||||
try? await mainQueue.sleep(for: .seconds(2))
|
||||
await send(.evaluatePriority2)
|
||||
}
|
||||
} else if state.priorityContent != .priority1 && !isConnected {
|
||||
return .send(.triggerPriority(.priority1))
|
||||
}
|
||||
return .none
|
||||
|
||||
case .smartBannerContentTapped:
|
||||
if state.priorityContent == .priority8 {
|
||||
return .send(.currencyConversionScreenRequested)
|
||||
}
|
||||
state.isSmartBannerSheetPresented = true
|
||||
return .none
|
||||
|
||||
case .closeSheetTapped:
|
||||
state.isSmartBannerSheetPresented = false
|
||||
return .none
|
||||
|
||||
case .remindMeLaterTapped(let priority):
|
||||
state.isSmartBannerSheetPresented = false
|
||||
state.priorityContentRequested = nil
|
||||
if priority == .priority6 {
|
||||
localNotification.scheduleWalletBackup()
|
||||
} else if priority == .priority7 {
|
||||
localNotification.scheduleShielding()
|
||||
}
|
||||
return .run { send in
|
||||
try? await mainQueue.sleep(for: .seconds(1))
|
||||
await send(.closeBanner(false), animation: .easeInOut(duration: Constants.easeInOutDuration))
|
||||
}
|
||||
|
||||
case .synchronizerStateChanged(let latestState):
|
||||
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.data.syncStatus)
|
||||
if snapshot.syncStatus != state.synchronizerStatusSnapshot.syncStatus {
|
||||
state.synchronizerStatusSnapshot = snapshot
|
||||
|
||||
if case .syncing(let progress) = snapshot.syncStatus {
|
||||
state.lastKnownSyncPercentage = Double(progress)
|
||||
|
||||
if state.priorityContent == .priority2 {
|
||||
return .send(.closeAndCleanupBanner)
|
||||
}
|
||||
}
|
||||
|
||||
// error syncing check
|
||||
switch snapshot.syncStatus {
|
||||
case .upToDate:
|
||||
if state.priorityContent == .priority3 {
|
||||
return .send(.closeAndCleanupBanner)
|
||||
}
|
||||
case .error, .unprepared:
|
||||
if state.lastKnownErrorMessage != snapshot.message {
|
||||
state.lastKnownErrorMessage = snapshot.message
|
||||
return .send(.triggerPriority(.priority2))
|
||||
}
|
||||
default: break
|
||||
}
|
||||
|
||||
// unavailable balance check
|
||||
let accountsBalances = latestState.data.accountsBalances
|
||||
if let account = state.selectedWalletAccount, let accountBalance = accountsBalances[account.id] {
|
||||
state.transparentBalance = accountBalance.unshielded
|
||||
|
||||
if accountBalance.orchardBalance.spendableValue.amount == 0 && state.priorityContent != .priority5 {
|
||||
return .send(.triggerPriority(.priority5))
|
||||
} else if accountBalance.orchardBalance.spendableValue.amount > 0 && state.priorityContent == .priority5 {
|
||||
return .send(.closeAndCleanupBanner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .none
|
||||
|
||||
case .localNotificationTapped(let identifier):
|
||||
if LocalNotificationClient.Constants.walletBackupUUID == identifier {
|
||||
return .send(.triggerPriority(.priority6))
|
||||
} else if LocalNotificationClient.Constants.shieldingUUID == identifier {
|
||||
return .send(.triggerPriority(.priority7))
|
||||
}
|
||||
return .none
|
||||
|
||||
// disconnected
|
||||
case .evaluatePriority1:
|
||||
return .send(.evaluatePriority2)
|
||||
|
||||
// syncing error
|
||||
case .evaluatePriority2:
|
||||
return .send(.evaluatePriority3)
|
||||
|
||||
// restoring
|
||||
case .evaluatePriority3:
|
||||
if state.walletStatus == .restoring {
|
||||
return .send(.triggerPriority(.priority3))
|
||||
}
|
||||
return .send(.evaluatePriority4)
|
||||
|
||||
// syncing
|
||||
case .evaluatePriority4:
|
||||
return .send(.evaluatePriority5)
|
||||
|
||||
// updating balance
|
||||
case .evaluatePriority5:
|
||||
|
||||
return .send(.evaluatePriority6)
|
||||
|
||||
// wallet backup
|
||||
case .evaluatePriority6:
|
||||
if let storedWallet = try? walletStorage.exportWallet(), !storedWallet.hasUserPassedPhraseBackupTest {
|
||||
return .run { send in
|
||||
guard await !localNotification.isWalletBackupScheduled() else {
|
||||
await send(.evaluatePriority7)
|
||||
return
|
||||
}
|
||||
await send(.triggerPriority(.priority6))
|
||||
}
|
||||
}
|
||||
return .send(.evaluatePriority7)
|
||||
|
||||
// shielding
|
||||
case .evaluatePriority7:
|
||||
guard let account = state.selectedWalletAccount else {
|
||||
return .none
|
||||
}
|
||||
return .run { send in
|
||||
if let accountBalance = try? await sdkSynchronizer.getAccountsBalances()[account.id],
|
||||
accountBalance.unshielded > zcashSDKEnvironment.shieldingThreshold {
|
||||
await send(.transparentBalanceUpdated(accountBalance.unshielded))
|
||||
if await !localNotification.isShieldingScheduled() {
|
||||
await send(.triggerPriority(.priority7))
|
||||
}
|
||||
} else {
|
||||
await send(.evaluatePriority8)
|
||||
}
|
||||
}
|
||||
|
||||
// currency conversion
|
||||
case .evaluatePriority8:
|
||||
if userStoredPreferences.exchangeRate() == nil {
|
||||
return .send(.triggerPriority(.priority8))
|
||||
}
|
||||
return .send(.evaluatePriority9)
|
||||
|
||||
// auto-shielding
|
||||
case .evaluatePriority9:
|
||||
return .none
|
||||
|
||||
case .triggerPriority(let priority):
|
||||
state.priorityContentRequested = priority
|
||||
return .send(.openBannerRequest)
|
||||
|
||||
case .transparentBalanceUpdated(let balance):
|
||||
state.transparentBalance = balance
|
||||
return .none
|
||||
|
||||
case .openBannerRequest:
|
||||
guard let priorityContentRequested = state.priorityContentRequested else {
|
||||
return .none
|
||||
}
|
||||
if let priorityContent = state.priorityContent, priorityContentRequested.rawValue >= priorityContent.rawValue {
|
||||
return .none
|
||||
}
|
||||
if state.isOpen {
|
||||
return .run { send in
|
||||
await send(.closeBanner(false), animation: .easeInOut(duration: Constants.easeInOutDuration))
|
||||
}
|
||||
}
|
||||
state.priorityContent = priorityContentRequested
|
||||
return .run { [delay = state.delay] send in
|
||||
try? await mainQueue.sleep(for: .seconds(delay))
|
||||
await send(.openBanner, animation: .easeInOut(duration: Constants.easeInOutDuration))
|
||||
}
|
||||
|
||||
case .closeBanner(let clean):
|
||||
state.isOpen = false
|
||||
if clean {
|
||||
state.priorityContentRequested = nil
|
||||
state.priorityContent = nil
|
||||
}
|
||||
return .send(.openBannerRequest)
|
||||
|
||||
case .closeAndCleanupBanner:
|
||||
// state.priorityContentRequested = nil
|
||||
// state.priorityContent = nil
|
||||
return .run { send in
|
||||
await send(.closeBanner(true), animation: .easeInOut(duration: Constants.easeInOutDuration))
|
||||
}
|
||||
|
||||
case .openBanner:
|
||||
state.delay = 1.0
|
||||
state.isOpen = true
|
||||
return .none
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
case .autoShieldingTapped:
|
||||
return .none
|
||||
|
||||
case .currencyConversionScreenRequested:
|
||||
return .none
|
||||
|
||||
case .currencyConversionTapped:
|
||||
return .send(.smartBannerContentTapped)
|
||||
|
||||
case .shieldTapped:
|
||||
return .none
|
||||
|
||||
case .walletBackupTapped:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
//
|
||||
// SmartBannerView.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 2025-03-14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
import Generated
|
||||
import Generated
|
||||
import UIComponents
|
||||
|
||||
enum SBConstants {
|
||||
static let fixedHeight: CGFloat = 32
|
||||
static let fixedHeightWithShadow: CGFloat = 36
|
||||
static let shadowHeight: CGFloat = 4
|
||||
}
|
||||
|
||||
public struct SmartBannerView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@Perception.Bindable var store: StoreOf<SmartBanner>
|
||||
|
||||
@State private var realHeight: CGFloat = 100
|
||||
@State private var isUnhidden = false
|
||||
@State private var height: CGFloat = 0
|
||||
|
||||
public init(store: StoreOf<SmartBanner>) {
|
||||
self.store = store
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
WithPerceptionTracking {
|
||||
ZStack(alignment: .top) {
|
||||
BottomRoundedRectangle(radius: SBConstants.fixedHeight)
|
||||
.frame(height: SBConstants.fixedHeight)
|
||||
.foregroundColor(Design.screenBackground.color(colorScheme))
|
||||
.shadow(color: Design.Text.primary.color(colorScheme).opacity(0.25), radius: 1)
|
||||
.zIndex(1)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
if store.isOpen {
|
||||
priorityContent()
|
||||
.padding(.vertical, 16)
|
||||
.padding(.top, SBConstants.fixedHeight)
|
||||
.onTapGesture {
|
||||
store.send(.smartBannerContentTapped)
|
||||
}
|
||||
.screenHorizontalPadding()
|
||||
}
|
||||
|
||||
TopRoundedRectangle(radius: store.isOpen ? SBConstants.fixedHeight : 0)
|
||||
.frame(height: SBConstants.fixedHeightWithShadow)
|
||||
.foregroundColor(Design.screenBackground.color(colorScheme))
|
||||
.shadow(color: Design.Text.primary.color(colorScheme).opacity(0.1), radius: store.isOpen ? 1 : 0, y: -1)
|
||||
}
|
||||
.frame(minHeight: SBConstants.fixedHeight + SBConstants.shadowHeight)
|
||||
}
|
||||
.zashiSheet(isPresented: $store.isSmartBannerSheetPresented) {
|
||||
helpSheetContent()
|
||||
.screenHorizontalPadding()
|
||||
}
|
||||
.onAppear { store.send(.onAppear) }
|
||||
.onDisappear { store.send(.onDisappear) }
|
||||
.background {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
Gradient.Stop(color: Design.Utility.Purple._700.color(.light), location: 0.00),
|
||||
Gradient.Stop(color: Design.Utility.Purple._950.color(.light), location: 1.00)
|
||||
],
|
||||
startPoint: UnitPoint(x: 0.5, y: 0.0),
|
||||
endPoint: UnitPoint(x: 0.5, y: 1.0)
|
||||
)
|
||||
}
|
||||
.clipShape( Rectangle() )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Store
|
||||
|
||||
extension SmartBanner {
|
||||
public static var initial = StoreOf<SmartBanner>(
|
||||
initialState: .initial
|
||||
) {
|
||||
SmartBanner()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Placeholders
|
||||
|
||||
extension SmartBanner.State {
|
||||
public static let initial = SmartBanner.State()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
struct BottomRoundedRectangle: Shape {
|
||||
var radius: CGFloat
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let width = rect.width
|
||||
let height = rect.height
|
||||
|
||||
return Path { path in
|
||||
path.move(to: CGPoint(x: 0, y: 0))
|
||||
path.addLine(to: CGPoint(x: width, y: 0))
|
||||
path.addLine(to: CGPoint(x: width, y: height - radius))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: width - radius, y: height),
|
||||
control: CGPoint(x: width, y: height)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: radius, y: height))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: 0, y: height - radius),
|
||||
control: CGPoint(x: 0, y: height)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: 0, y: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TopRoundedRectangle: Shape {
|
||||
var radius: CGFloat
|
||||
|
||||
var animatableData: CGFloat {
|
||||
get { radius }
|
||||
set { radius = newValue }
|
||||
}
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let width = rect.width
|
||||
let height = rect.height
|
||||
|
||||
return Path { path in
|
||||
path.move(to: CGPoint(x: 0, y: height))
|
||||
path.addLine(to: CGPoint(x: width, y: height))
|
||||
path.addLine(to: CGPoint(x: width, y: radius))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: width - radius, y: 0),
|
||||
control: CGPoint(x: width, y: 0)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: radius, y: 0))
|
||||
|
||||
path.addQuadCurve(
|
||||
to: CGPoint(x: 0, y: radius),
|
||||
control: CGPoint(x: 0, y: 0)
|
||||
)
|
||||
|
||||
path.addLine(to: CGPoint(x: 0, y: 0))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -281,7 +281,7 @@ public struct TabsView: View {
|
|||
preferences.map {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
Asset.Assets.coinsSwap.image
|
||||
Asset.Assets.Icons.coinsSwap.image
|
||||
.zImage(size: 20, style: Design.Text.primary)
|
||||
.padding(10)
|
||||
.background {
|
||||
|
|
|
@ -303,6 +303,7 @@ public enum Design {
|
|||
case _700
|
||||
case _800
|
||||
case _900
|
||||
case _950
|
||||
}
|
||||
|
||||
public enum Brand: Colorable {
|
||||
|
@ -702,6 +703,7 @@ public extension Design.Utility.Purple {
|
|||
case ._700: return Design.col(Asset.Colors.ZDesign.purple700.color, Asset.Colors.ZDesign.purple300.color, colorScheme)
|
||||
case ._800: return Design.col(Asset.Colors.ZDesign.purple800.color, Asset.Colors.ZDesign.purple200.color, colorScheme)
|
||||
case ._900: return Design.col(Asset.Colors.ZDesign.purple900.color, Asset.Colors.ZDesign.purple100.color, colorScheme)
|
||||
case ._950: return Design.col(Asset.Colors.ZDesign.purple950.color, Asset.Colors.ZDesign.purple50.color, colorScheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -203,8 +203,8 @@ public enum L10n {
|
|||
public static let saveBtn = L10n.tr("Localizable", "currencyConversion.saveBtn", fallback: "Save changes")
|
||||
/// Display your balance and payment amounts in USD. Zashi’s currency conversion feature protects your IP address at all times.
|
||||
public static let settingsDesc = L10n.tr("Localizable", "currencyConversion.settingsDesc", fallback: "Display your balance and payment amounts in USD. Zashi’s currency conversion feature protects your IP address at all times.")
|
||||
/// Skip for now
|
||||
public static let skipBtn = L10n.tr("Localizable", "currencyConversion.skipBtn", fallback: "Skip for now")
|
||||
/// Skip
|
||||
public static let skipBtn = L10n.tr("Localizable", "currencyConversion.skipBtn", fallback: "Skip")
|
||||
/// Currency Conversion
|
||||
public static let title = L10n.tr("Localizable", "currencyConversion.title", fallback: "Currency Conversion")
|
||||
}
|
||||
|
@ -1095,6 +1095,134 @@ public enum L10n {
|
|||
}
|
||||
}
|
||||
}
|
||||
public enum SmartBanner {
|
||||
public enum Content {
|
||||
public enum AutoShielding {
|
||||
/// Start
|
||||
public static let button = L10n.tr("Localizable", "smartBanner.content.autoShielding.button", fallback: "Start")
|
||||
/// Enable automatic shielding
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.autoShielding.info", fallback: "Enable automatic shielding")
|
||||
/// Auto-Shielding
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.content.autoShielding.title", fallback: "Auto-Shielding")
|
||||
}
|
||||
public enum Backup {
|
||||
/// Start
|
||||
public static let button = L10n.tr("Localizable", "smartBanner.content.backup.button", fallback: "Start")
|
||||
/// Prevent potential loss of funds
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.backup.info", fallback: "Prevent potential loss of funds")
|
||||
/// Wallet Backup Required
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.content.backup.title", fallback: "Wallet Backup Required")
|
||||
}
|
||||
public enum CurrencyConversion {
|
||||
/// Start
|
||||
public static let button = L10n.tr("Localizable", "smartBanner.content.currencyConversion.button", fallback: "Start")
|
||||
/// Enable to display USD values
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.currencyConversion.info", fallback: "Enable to display USD values")
|
||||
/// Currency Conversion
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.content.currencyConversion.title", fallback: "Currency Conversion")
|
||||
}
|
||||
public enum Disconnected {
|
||||
/// Check your connection and reload Zashi
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.disconnected.info", fallback: "Check your connection and reload Zashi")
|
||||
/// Wallet Disconnected
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.content.disconnected.title", fallback: "Wallet Disconnected")
|
||||
}
|
||||
public enum Restore {
|
||||
/// Keep Zashi open on active phone screen
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.restore.info", fallback: "Keep Zashi open on active phone screen")
|
||||
/// Restoring Wallet • %@ Complete
|
||||
public static func title(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "smartBanner.content.restore.title", String(describing: p1), fallback: "Restoring Wallet • %@ Complete")
|
||||
}
|
||||
}
|
||||
public enum Shield {
|
||||
/// Shield
|
||||
public static let button = L10n.tr("Localizable", "smartBanner.content.shield.button", fallback: "Shield")
|
||||
/// Transparent Balance Detected
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.content.shield.title", fallback: "Transparent Balance Detected")
|
||||
}
|
||||
public enum Sync {
|
||||
/// Your wallet is getting updated
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.sync.info", fallback: "Your wallet is getting updated")
|
||||
/// Syncing • %@ Complete
|
||||
public static func title(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "smartBanner.content.sync.title", String(describing: p1), fallback: "Syncing • %@ Complete")
|
||||
}
|
||||
}
|
||||
public enum SyncingError {
|
||||
/// Attempting to resolve
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.syncingError.info", fallback: "Attempting to resolve")
|
||||
/// Error encountered while syncing
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.content.syncingError.title", fallback: "Error encountered while syncing")
|
||||
}
|
||||
public enum UpdatingBalance {
|
||||
/// Waiting for last transaction to process
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.content.updatingBalance.info", fallback: "Waiting for last transaction to process")
|
||||
/// Updating Balance...
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.content.updatingBalance.title", fallback: "Updating Balance...")
|
||||
}
|
||||
}
|
||||
public enum Help {
|
||||
/// Remind me later
|
||||
public static let remindMe = L10n.tr("Localizable", "smartBanner.help.remindMe", fallback: "Remind me later")
|
||||
public enum Backup {
|
||||
/// Prevent potential loss of funds by securely backing up your wallet.
|
||||
public static let info1 = L10n.tr("Localizable", "smartBanner.help.backup.info1", fallback: "Prevent potential loss of funds by securely backing up your wallet.")
|
||||
/// Back up access to your funds by backing up:
|
||||
public static let info2 = L10n.tr("Localizable", "smartBanner.help.backup.info2", fallback: "Back up access to your funds by backing up:")
|
||||
/// In case you lose access to the app or to your device, knowing the Secret Recovery Phrase is the only way to recover your funds. We cannot see it and cannot help you recover it.
|
||||
public static let info3 = L10n.tr("Localizable", "smartBanner.help.backup.info3", fallback: "In case you lose access to the app or to your device, knowing the Secret Recovery Phrase is the only way to recover your funds. We cannot see it and cannot help you recover it.")
|
||||
/// Anyone with access to your Secret Recovery Phrase will have full control of your wallet, so keep it secure and never show it to anyone.
|
||||
public static let info4 = L10n.tr("Localizable", "smartBanner.help.backup.info4", fallback: "Anyone with access to your Secret Recovery Phrase will have full control of your wallet, so keep it secure and never show it to anyone.")
|
||||
/// Secret Recovery Phrase (a unique set of 24 words in a precise order)
|
||||
public static let point1 = L10n.tr("Localizable", "smartBanner.help.backup.point1", fallback: "Secret Recovery Phrase (a unique set of 24 words in a precise order) ")
|
||||
/// Wallet Birthday Height (block height at which your wallet was created)
|
||||
public static let point2 = L10n.tr("Localizable", "smartBanner.help.backup.point2", fallback: "Wallet Birthday Height (block height at which your wallet was created)")
|
||||
/// Backup Required
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.help.backup.title", fallback: "Backup Required")
|
||||
}
|
||||
public enum Diconnected {
|
||||
/// You appear to be offline. Please restore your internet connection to use Zashi wallet.
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.help.diconnected.info", fallback: "You appear to be offline. Please restore your internet connection to use Zashi wallet.")
|
||||
/// Wallet Disconnected
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.help.diconnected.title", fallback: "Wallet Disconnected")
|
||||
}
|
||||
public enum Restore {
|
||||
/// Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption:
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.help.restore.info", fallback: "Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption:")
|
||||
/// Keep the Zashi app open on an active phone screen.
|
||||
public static let point1 = L10n.tr("Localizable", "smartBanner.help.restore.point1", fallback: "Keep the Zashi app open on an active phone screen.")
|
||||
/// To prevent your phone screen from going dark, turn off power-saving mode and keep your phone plugged in.
|
||||
public static let point2 = L10n.tr("Localizable", "smartBanner.help.restore.point2", fallback: "To prevent your phone screen from going dark, turn off power-saving mode and keep your phone plugged in.")
|
||||
/// Wallet Restore in Progress
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.help.restore.title", fallback: "Wallet Restore in Progress")
|
||||
/// Funds cannot be spent until your wallet is restored.
|
||||
public static let warning = L10n.tr("Localizable", "smartBanner.help.restore.warning", fallback: "Funds cannot be spent until your wallet is restored.")
|
||||
}
|
||||
public enum Shield {
|
||||
/// To protect user privacy, Zashi doesn't support spending transparent ZEC. Tap the "Shield" button below to make your transparent funds spendable and your total Zashi balance private.
|
||||
public static let info1 = L10n.tr("Localizable", "smartBanner.help.shield.info1", fallback: "To protect user privacy, Zashi doesn't support spending transparent ZEC. Tap the \"Shield\" button below to make your transparent funds spendable and your total Zashi balance private. ")
|
||||
/// This will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)
|
||||
public static let info2 = L10n.tr("Localizable", "smartBanner.help.shield.info2", fallback: "This will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)")
|
||||
/// Always Shield Transparent Funds
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.help.shield.title", fallback: "Always Shield Transparent Funds")
|
||||
/// Transparent
|
||||
public static let transparent = L10n.tr("Localizable", "smartBanner.help.shield.transparent", fallback: "Transparent")
|
||||
}
|
||||
public enum Sync {
|
||||
/// Zashi is scanning the blockchain for your latest transactions to make sure your balances are displayed correctly. Keep the Zashi app open on an active phone screen to avoid interruptions.
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.help.sync.info", fallback: "Zashi is scanning the blockchain for your latest transactions to make sure your balances are displayed correctly. Keep the Zashi app open on an active phone screen to avoid interruptions.")
|
||||
/// Wallet Sync in Progress
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.help.sync.title", fallback: "Wallet Sync in Progress")
|
||||
}
|
||||
public enum UpdatingBalance {
|
||||
/// Your last transaction is getting mined and confirmed.
|
||||
public static let info = L10n.tr("Localizable", "smartBanner.help.updatingBalance.info", fallback: "Your last transaction is getting mined and confirmed.")
|
||||
/// Updating Balance
|
||||
public static let title = L10n.tr("Localizable", "smartBanner.help.updatingBalance.title", fallback: "Updating Balance")
|
||||
}
|
||||
}
|
||||
}
|
||||
public enum Splash {
|
||||
/// Tap the face icon to use Face ID and unlock it.
|
||||
public static let authFaceID = L10n.tr("Localizable", "splash.authFaceID", fallback: "Tap the face icon to use Face ID and unlock it.")
|
||||
|
|
12
modules/Sources/Generated/Resources/Assets.xcassets/icons/alertTriangle.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "alertTriangle.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
modules/Sources/Generated/Resources/Assets.xcassets/icons/alertTriangle.imageset/alertTriangle.png
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
12
modules/Sources/Generated/Resources/Assets.xcassets/icons/loading.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "loading.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
modules/Sources/Generated/Resources/Assets.xcassets/icons/loading.imageset/loading.png
vendored
Normal file
After Width: | Height: | Size: 8.4 KiB |
12
modules/Sources/Generated/Resources/Assets.xcassets/icons/shieldOff.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "shieldOff.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
modules/Sources/Generated/Resources/Assets.xcassets/icons/shieldOff.imageset/shieldOff.png
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
12
modules/Sources/Generated/Resources/Assets.xcassets/icons/shieldZap.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "shieldZap.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
modules/Sources/Generated/Resources/Assets.xcassets/icons/shieldZap.imageset/shieldZap.png
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
12
modules/Sources/Generated/Resources/Assets.xcassets/icons/wifiOff.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wifiOff.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
modules/Sources/Generated/Resources/Assets.xcassets/icons/wifiOff.imageset/wifiOff.png
vendored
Normal file
After Width: | Height: | Size: 12 KiB |
|
@ -428,7 +428,6 @@
|
|||
"currencyConversion.ipDesc" = "Zashi’s currency conversion feature doesn’t compromise your IP address.";
|
||||
"currencyConversion.refresh" = "Rate Refresh";
|
||||
"currencyConversion.refreshDesc" = "The rate is refreshed automatically and can also be refreshed manually.";
|
||||
"currencyConversion.skipBtn" = "Skip for now";
|
||||
|
||||
// MARK: - Deeplink Warning
|
||||
"deeplinkWarning.title" = "Looks like you used a third-party app to scan for payment.";
|
||||
|
@ -578,3 +577,66 @@
|
|||
"restoreWallet.birthday.estimated.title" = "Estimated Block Height";
|
||||
"restoreWallet.birthday.estimated.info" = "Zashi will scan and recover all transactions made after the following block number.";
|
||||
"restoreInfo.checkbox" = "Keep screen on while restoring";
|
||||
|
||||
// Smart Banner
|
||||
"smartBanner.help.diconnected.title" = "Wallet Disconnected";
|
||||
"smartBanner.help.diconnected.info" = "You appear to be offline. Please restore your internet connection to use Zashi wallet.";
|
||||
|
||||
"smartBanner.help.restore.title" = "Wallet Restore in Progress";
|
||||
"smartBanner.help.restore.info" = "Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption:";
|
||||
"smartBanner.help.restore.point1" = "Keep the Zashi app open on an active phone screen.";
|
||||
"smartBanner.help.restore.point2" = "To prevent your phone screen from going dark, turn off power-saving mode and keep your phone plugged in.";
|
||||
"smartBanner.help.restore.warning" = "Funds cannot be spent until your wallet is restored.";
|
||||
|
||||
"smartBanner.help.sync.title" = "Wallet Sync in Progress";
|
||||
"smartBanner.help.sync.info" = "Zashi is scanning the blockchain for your latest transactions to make sure your balances are displayed correctly. Keep the Zashi app open on an active phone screen to avoid interruptions.";
|
||||
|
||||
"smartBanner.help.updatingBalance.title" = "Updating Balance";
|
||||
"smartBanner.help.updatingBalance.info" = "Your last transaction is getting mined and confirmed.";
|
||||
|
||||
"smartBanner.help.backup.title" = "Backup Required";
|
||||
"smartBanner.help.backup.info1" = "Prevent potential loss of funds by securely backing up your wallet.";
|
||||
"smartBanner.help.backup.info2" = "Back up access to your funds by backing up:";
|
||||
"smartBanner.help.backup.point1" = "Secret Recovery Phrase (a unique set of 24 words in a precise order) ";
|
||||
"smartBanner.help.backup.point2" = "Wallet Birthday Height (block height at which your wallet was created)";
|
||||
"smartBanner.help.backup.info3" = "In case you lose access to the app or to your device, knowing the Secret Recovery Phrase is the only way to recover your funds. We cannot see it and cannot help you recover it.";
|
||||
"smartBanner.help.backup.info4" = "Anyone with access to your Secret Recovery Phrase will have full control of your wallet, so keep it secure and never show it to anyone.";
|
||||
|
||||
"smartBanner.help.shield.title" = "Always Shield Transparent Funds";
|
||||
"smartBanner.help.shield.info1" = "To protect user privacy, Zashi doesn't support spending transparent ZEC. Tap the \"Shield\" button below to make your transparent funds spendable and your total Zashi balance private. ";
|
||||
"smartBanner.help.shield.info2" = "This will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)";
|
||||
"smartBanner.help.shield.transparent" = "Transparent";
|
||||
|
||||
"smartBanner.help.remindMe" = "Remind me later";
|
||||
|
||||
"smartBanner.content.disconnected.title" = "Wallet Disconnected";
|
||||
"smartBanner.content.disconnected.info" = "Check your connection and reload Zashi";
|
||||
|
||||
"smartBanner.content.syncingError.title" = "Error encountered while syncing";
|
||||
"smartBanner.content.syncingError.info" = "Attempting to resolve";
|
||||
|
||||
"smartBanner.content.restore.title" = "Restoring Wallet • %@ Complete";
|
||||
"smartBanner.content.restore.info" = "Keep Zashi open on active phone screen";
|
||||
|
||||
"smartBanner.content.sync.title" = "Syncing • %@ Complete";
|
||||
"smartBanner.content.sync.info" = "Your wallet is getting updated";
|
||||
|
||||
"smartBanner.content.updatingBalance.title" = "Updating Balance...";
|
||||
"smartBanner.content.updatingBalance.info" = "Waiting for last transaction to process";
|
||||
|
||||
"smartBanner.content.backup.title" = "Wallet Backup Required";
|
||||
"smartBanner.content.backup.info" = "Prevent potential loss of funds";
|
||||
"smartBanner.content.backup.button" = "Start";
|
||||
|
||||
"smartBanner.content.shield.title" = "Transparent Balance Detected";
|
||||
"smartBanner.content.shield.button" = "Shield";
|
||||
|
||||
"smartBanner.content.currencyConversion.title" = "Currency Conversion";
|
||||
"smartBanner.content.currencyConversion.info" = "Enable to display USD values";
|
||||
"smartBanner.content.currencyConversion.button" = "Start";
|
||||
|
||||
"smartBanner.content.autoShielding.title" = "Auto-Shielding";
|
||||
"smartBanner.content.autoShielding.info" = "Enable automatic shielding";
|
||||
"smartBanner.content.autoShielding.button" = "Start";
|
||||
|
||||
"currencyConversion.skipBtn" = "Skip";
|
||||
|
|
|
@ -67,7 +67,6 @@ public enum Asset {
|
|||
public static let chevronDown = ImageAsset(name: "chevronDown")
|
||||
public static let chevronRight = ImageAsset(name: "chevronRight")
|
||||
public static let chevronUp = ImageAsset(name: "chevronUp")
|
||||
public static let coinsSwap = ImageAsset(name: "coinsSwap")
|
||||
public static let convertIcon = ImageAsset(name: "convertIcon")
|
||||
public static let copy = ImageAsset(name: "copy")
|
||||
public static let eyeOff = ImageAsset(name: "eyeOff")
|
||||
|
@ -75,11 +74,13 @@ public enum Asset {
|
|||
public static let flyReceivedFilled = ImageAsset(name: "flyReceivedFilled")
|
||||
public enum Icons {
|
||||
public static let alertCircle = ImageAsset(name: "alertCircle")
|
||||
public static let alertTriangle = ImageAsset(name: "alertTriangle")
|
||||
public static let arrowUp = ImageAsset(name: "arrowUp")
|
||||
public static let authKey = ImageAsset(name: "authKey")
|
||||
public static let bookmark = ImageAsset(name: "bookmark")
|
||||
public static let bookmarkCheck = ImageAsset(name: "bookmarkCheck")
|
||||
public static let coinsHand = ImageAsset(name: "coinsHand")
|
||||
public static let coinsSwap = ImageAsset(name: "coinsSwap")
|
||||
public static let connectWallet = ImageAsset(name: "connectWallet")
|
||||
public static let currencyDollar = ImageAsset(name: "currencyDollar")
|
||||
public static let currencyZec = ImageAsset(name: "currencyZec")
|
||||
|
@ -94,6 +95,7 @@ public enum Asset {
|
|||
public static let imageLibrary = ImageAsset(name: "imageLibrary")
|
||||
public static let integrations = ImageAsset(name: "integrations")
|
||||
public static let key = ImageAsset(name: "key")
|
||||
public static let loading = ImageAsset(name: "loading")
|
||||
public static let lockUnlocked = ImageAsset(name: "lockUnlocked")
|
||||
public static let magicWand = ImageAsset(name: "magicWand")
|
||||
public static let menu = ImageAsset(name: "menu")
|
||||
|
@ -112,11 +114,14 @@ public enum Asset {
|
|||
public static let server = ImageAsset(name: "server")
|
||||
public static let settings = ImageAsset(name: "settings")
|
||||
public static let share = ImageAsset(name: "share")
|
||||
public static let shieldOff = ImageAsset(name: "shieldOff")
|
||||
public static let shieldTickFilled = ImageAsset(name: "shieldTickFilled")
|
||||
public static let shieldZap = ImageAsset(name: "shieldZap")
|
||||
public static let switchHorizontal = ImageAsset(name: "switchHorizontal")
|
||||
public static let textInput = ImageAsset(name: "textInput")
|
||||
public static let user = ImageAsset(name: "user")
|
||||
public static let userPlus = ImageAsset(name: "userPlus")
|
||||
public static let wifiOff = ImageAsset(name: "wifiOff")
|
||||
public static let xClose = ImageAsset(name: "xClose")
|
||||
public static let zashiLogoSq = ImageAsset(name: "zashiLogoSq")
|
||||
public static let zashiLogoSqBold = ImageAsset(name: "zashiLogoSqBold")
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// CircularProgressView.swift
|
||||
// Zashi
|
||||
//
|
||||
// Created by Lukáš Korba on 04-03-2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Generated
|
||||
|
||||
public struct CircularProgressView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var progress: Double
|
||||
|
||||
public init(progress: Double) {
|
||||
self.progress = progress
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Design.Utility.Purple._400.color(.light), lineWidth: 4)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.stroke(Design.Utility.Purple._50.color(.light), style: StrokeStyle(lineWidth: 4, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeInOut(duration: 0.2), value: progress)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,12 +34,23 @@ public struct ZashiFontModifier: ViewModifier {
|
|||
let weight: FontWeight
|
||||
let addressFont: Bool
|
||||
let size: CGFloat
|
||||
let style: Colorable
|
||||
let color: Color?
|
||||
let style: Colorable?
|
||||
|
||||
public func body(content: Content) -> some View {
|
||||
content
|
||||
.font(.custom(fontName(weight, addressFont: addressFont), size: size))
|
||||
.zForegroundColor(style)
|
||||
if let color {
|
||||
content
|
||||
.font(.custom(fontName(weight, addressFont: addressFont), size: size))
|
||||
//.zForegroundColor(style)
|
||||
.foregroundColor(color)
|
||||
} else if let style {
|
||||
content
|
||||
.font(.custom(fontName(weight, addressFont: addressFont), size: size))
|
||||
.zForegroundColor(style)
|
||||
// .foregroundColor(color)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
private func fontName(_ weight: FontWeight, addressFont: Bool = false) -> String {
|
||||
|
@ -82,8 +93,21 @@ public extension View {
|
|||
size: CGFloat,
|
||||
style: Colorable
|
||||
) -> some View {
|
||||
// zFont(weight, addressFont: addressFont, size: size, color: style.color(.light))
|
||||
|
||||
self.modifier(
|
||||
ZashiFontModifier(weight: weight, addressFont: addressFont, size: size, style: style)
|
||||
ZashiFontModifier(weight: weight, addressFont: addressFont, size: size, color: nil, style: style)
|
||||
)
|
||||
}
|
||||
|
||||
func zFont(
|
||||
_ weight: ZashiFontModifier.FontWeight = .regular,
|
||||
addressFont: Bool = false,
|
||||
size: CGFloat,
|
||||
color: Color
|
||||
) -> some View {
|
||||
self.modifier(
|
||||
ZashiFontModifier(weight: weight, addressFont: addressFont, size: size, color: color, style: nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -460,12 +460,12 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"identity" : "zcash-swift-wallet-sdk",
|
||||
"identity" : "zcashlightclientkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Electric-Coin-Company/zcash-swift-wallet-sdk",
|
||||
"location" : "https://github.com/LukasKorba/ZcashLightClientKit",
|
||||
"state" : {
|
||||
"revision" : "e5e826d13349746eabf7b481ccf7c6adcbc32bce",
|
||||
"version" : "2.2.11"
|
||||
"branch" : "1537-Birthday-estimate-based-on-a-date",
|
||||
"revision" : "04ca05428f469767725ba421383fca31cf225855"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -13,9 +13,10 @@ import Network
|
|||
import Utils
|
||||
import Root
|
||||
import BackgroundTasks
|
||||
import UserNotifications
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
final class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
private let bcgTaskId = "co.electriccoin.power_wifi_sync"
|
||||
private let bcgSchedulerTaskId = "co.electriccoin.scheduler"
|
||||
private var monitor: NWPathMonitor?
|
||||
|
@ -33,6 +34,9 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.delegate = self
|
||||
|
||||
#if DEBUG
|
||||
// Short-circuit if running unit tests to avoid side-effects from the app running.
|
||||
guard !_XCTIsTesting else { return true }
|
||||
|
@ -54,6 +58,13 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
) -> Bool {
|
||||
return extensionPointIdentifier != UIApplication.ExtensionPointIdentifier.keyboard
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
|
||||
rootStore.send(.localNotificationTapped(response.notification.request.identifier))
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BackgroundTasks
|
||||
|
|
|
@ -428,7 +428,6 @@
|
|||
"currencyConversion.ipDesc" = "Zashi’s currency conversion feature doesn’t compromise your IP address.";
|
||||
"currencyConversion.refresh" = "Rate Refresh";
|
||||
"currencyConversion.refreshDesc" = "The rate is refreshed automatically and can also be refreshed manually.";
|
||||
"currencyConversion.skipBtn" = "Skip for now";
|
||||
|
||||
// MARK: - Deeplink Warning
|
||||
"deeplinkWarning.title" = "Looks like you used a third-party app to scan for payment.";
|
||||
|
@ -578,3 +577,66 @@
|
|||
"restoreWallet.birthday.estimated.title" = "Estimated Block Height";
|
||||
"restoreWallet.birthday.estimated.info" = "Zashi will scan and recover all transactions made after the following block number.";
|
||||
"restoreInfo.checkbox" = "Keep screen on while restoring";
|
||||
|
||||
// Smart Banner
|
||||
"smartBanner.help.diconnected.title" = "Wallet Disconnected";
|
||||
"smartBanner.help.diconnected.info" = "You appear to be offline. Please restore your internet connection to use Zashi wallet.";
|
||||
|
||||
"smartBanner.help.restore.title" = "Wallet Restore in Progress";
|
||||
"smartBanner.help.restore.info" = "Zashi is scanning the blockchain to retrieve your transactions. Older wallets can take hours to restore. Follow these steps to prevent interruption:";
|
||||
"smartBanner.help.restore.point1" = "Keep the Zashi app open on an active phone screen.";
|
||||
"smartBanner.help.restore.point2" = "To prevent your phone screen from going dark, turn off power-saving mode and keep your phone plugged in.";
|
||||
"smartBanner.help.restore.warning" = "Funds cannot be spent until your wallet is restored.";
|
||||
|
||||
"smartBanner.help.sync.title" = "Wallet Sync in Progress";
|
||||
"smartBanner.help.sync.info" = "Zashi is scanning the blockchain for your latest transactions to make sure your balances are displayed correctly. Keep the Zashi app open on an active phone screen to avoid interruptions.";
|
||||
|
||||
"smartBanner.help.updatingBalance.title" = "Updating Balance";
|
||||
"smartBanner.help.updatingBalance.info" = "Your last transaction is getting mined and confirmed.";
|
||||
|
||||
"smartBanner.help.backup.title" = "Backup Required";
|
||||
"smartBanner.help.backup.info1" = "Prevent potential loss of funds by securely backing up your wallet.";
|
||||
"smartBanner.help.backup.info2" = "Back up access to your funds by backing up:";
|
||||
"smartBanner.help.backup.point1" = "Secret Recovery Phrase (a unique set of 24 words in a precise order) ";
|
||||
"smartBanner.help.backup.point2" = "Wallet Birthday Height (block height at which your wallet was created)";
|
||||
"smartBanner.help.backup.info3" = "In case you lose access to the app or to your device, knowing the Secret Recovery Phrase is the only way to recover your funds. We cannot see it and cannot help you recover it.";
|
||||
"smartBanner.help.backup.info4" = "Anyone with access to your Secret Recovery Phrase will have full control of your wallet, so keep it secure and never show it to anyone.";
|
||||
|
||||
"smartBanner.help.shield.title" = "Always Shield Transparent Funds";
|
||||
"smartBanner.help.shield.info1" = "To protect user privacy, Zashi doesn't support spending transparent ZEC. Tap the \"Shield\" button below to make your transparent funds spendable and your total Zashi balance private. ";
|
||||
"smartBanner.help.shield.info2" = "This will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)";
|
||||
"smartBanner.help.shield.transparent" = "Transparent";
|
||||
|
||||
"smartBanner.help.remindMe" = "Remind me later";
|
||||
|
||||
"smartBanner.content.disconnected.title" = "Wallet Disconnected";
|
||||
"smartBanner.content.disconnected.info" = "Check your connection and reload Zashi";
|
||||
|
||||
"smartBanner.content.syncingError.title" = "Error encountered while syncing";
|
||||
"smartBanner.content.syncingError.info" = "Attempting to resolve";
|
||||
|
||||
"smartBanner.content.restore.title" = "Restoring Wallet • %@ Complete";
|
||||
"smartBanner.content.restore.info" = "Keep Zashi open on active phone screen";
|
||||
|
||||
"smartBanner.content.sync.title" = "Syncing • %@ Complete";
|
||||
"smartBanner.content.sync.info" = "Your wallet is getting updated";
|
||||
|
||||
"smartBanner.content.updatingBalance.title" = "Updating Balance...";
|
||||
"smartBanner.content.updatingBalance.info" = "Waiting for last transaction to process";
|
||||
|
||||
"smartBanner.content.backup.title" = "Wallet Backup Required";
|
||||
"smartBanner.content.backup.info" = "Prevent potential loss of funds";
|
||||
"smartBanner.content.backup.button" = "Start";
|
||||
|
||||
"smartBanner.content.shield.title" = "Transparent Balance Detected";
|
||||
"smartBanner.content.shield.button" = "Shield";
|
||||
|
||||
"smartBanner.content.currencyConversion.title" = "Currency Conversion";
|
||||
"smartBanner.content.currencyConversion.info" = "Enable to display USD values";
|
||||
"smartBanner.content.currencyConversion.button" = "Start";
|
||||
|
||||
"smartBanner.content.autoShielding.title" = "Auto-Shielding";
|
||||
"smartBanner.content.autoShielding.info" = "Enable automatic shielding";
|
||||
"smartBanner.content.autoShielding.button" = "Start";
|
||||
|
||||
"currencyConversion.skipBtn" = "Skip";
|
||||
|
|