[#1368] Update Settings flows UI

- About
- Private data export
- Reset Zashi
- What’s New
- Recovery Phrase

[#1373] update buttons across the app and remove the grid pattern

- Send feedback finished

[#1373] update buttons across the app and remove the grid pattern

- All settings screens localized

[#1373] update buttons across the app and remove the grid pattern

- Recovery phrase screen prepared for onboarding

[#1368] Update Settings flows UI

- changelog updated

[#1368] Update Settings flows UI

- SDK bumped up to 2.2.5

[#1368] Update Settings flows UI

- Zasji testnet version bumped up

[#1368] Update Settings flows UI

- Rebased

[#1368] Update Settings flows UI

- tooltip for the birthday added

[#1368] Update Settings flows UI

- spacing

[#1368] Update Settings flows UI

- regenerated cleanup
This commit is contained in:
Lukas Korba 2024-10-11 19:46:04 +02:00
parent 821976d068
commit 9871a1ad61
38 changed files with 1003 additions and 481 deletions

View File

@ -14,6 +14,7 @@ directly impact users rather than highlighting other crucial architectural updat
### Changed
- Not enough free space screen has been redesigned.
- All settings flow screen have been redesigned
### Fixed
- Splash screen animation is blocked by the main thread on iOS 16 and older.

View File

@ -60,6 +60,7 @@ let package = Package(
.library(name: "SecItem", targets: ["SecItem"]),
.library(name: "SecurityWarning", targets: ["SecurityWarning"]),
.library(name: "SendConfirmation", targets: ["SendConfirmation"]),
.library(name: "SendFeedback", targets: ["SendFeedback"]),
.library(name: "SendFlow", targets: ["SendFlow"]),
.library(name: "ServerSetup", targets: ["ServerSetup"]),
.library(name: "Settings", targets: ["Settings"]),
@ -101,7 +102,6 @@ let package = Package(
"Generated",
"Models",
"UIComponents",
"WhatsNew",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
],
path: "Sources/Features/About"
@ -675,6 +675,17 @@ let package = Package(
],
path: "Sources/Features/SendConfirmation"
),
.target(
name: "SendFeedback",
dependencies: [
"Generated",
"SupportDataGenerator",
"UIComponents",
"Utils",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
],
path: "Sources/Features/SendFeedback"
),
.target(
name: "SendFlow",
dependencies: [
@ -724,9 +735,10 @@ let package = Package(
"Pasteboard",
"PrivateDataConsent",
"RecoveryPhraseDisplay",
"SendFeedback",
"ServerSetup",
"SupportDataGenerator",
"UIComponents",
"WhatsNew",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
.product(name: "ZcashLightClientKit", package: "zcash-swift-wallet-sdk"),
.product(name: "Flexa", package: "flexa-ios")
@ -906,6 +918,7 @@ let package = Package(
.target(
name: "WhatsNew",
dependencies: [
"AppVersion",
"Generated",
"UIComponents",
"WhatsNewProvider",

View File

@ -25,7 +25,7 @@ public enum SupportDataGenerator {
public static let subjectPPE = L10n.ProposalPartial.mailSubject
}
public static func generate() -> SupportData {
public static func generate(_ prefix: String? = nil) -> SupportData {
let items: [SupportDataGeneratorItem] = [
TimeItem(),
AppVersionItem(),
@ -42,7 +42,13 @@ public enum SupportDataGenerator {
.map { "\($0.0): \($0.1)" }
.joined(separator: "\n")
return SupportData(toAddress: Constants.email, subject: Constants.subject, message: message)
if let prefix {
let finalMessage = "\(prefix)\n\(message)"
return SupportData(toAddress: Constants.email, subject: Constants.subject, message: finalMessage)
} else {
return SupportData(toAddress: Constants.email, subject: Constants.subject, message: message)
}
}
public static func generatePartialProposalError(txIds: [String], statuses: [String]) -> SupportData {
@ -62,7 +68,7 @@ public enum SupportDataGenerator {
\(data.message)
Transaction statuses:
\(L10n.ProposalPartial.transactionStatuses)
\(statusStrings)
"""
@ -160,8 +166,8 @@ private struct LocaleItem: SupportDataGeneratorItem {
return [
(Constants.localeKey, locale.identifier),
(Constants.groupingSeparatorKey, locale.groupingSeparator ?? Constants.unknownSeparator),
(Constants.decimalSeparatorKey, locale.decimalSeparator ?? Constants.unknownSeparator)
(Constants.groupingSeparatorKey, "'\(locale.groupingSeparator ?? Constants.unknownSeparator)'"),
(Constants.decimalSeparatorKey, "'\(locale.decimalSeparator ?? Constants.unknownSeparator)'")
]
}
}

View File

@ -3,7 +3,6 @@ import ComposableArchitecture
import AppVersion
import Generated
import WhatsNew
@Reducer
public struct About {
@ -11,19 +10,13 @@ public struct About {
public struct State: Equatable {
public var appVersion = ""
public var appBuild = ""
public var whatsNewState: WhatsNew.State
public var whatsNewViewBinding: Bool = false
public init(
appVersion: String = "",
appBuild: String = "",
whatsNewState: WhatsNew.State,
whatsNewViewBinding: Bool = false
appBuild: String = ""
) {
self.appVersion = appVersion
self.appBuild = appBuild
self.whatsNewState = whatsNewState
self.whatsNewViewBinding = whatsNewViewBinding
}
}
@ -31,8 +24,6 @@ public struct About {
case binding(BindingAction<About.State>)
case onAppear
case privacyPolicyButtonTapped
case whatsNew(WhatsNew.Action)
case whatsNewButtonTapped
}
@Dependency(\.appVersion) var appVersion
@ -41,10 +32,6 @@ public struct About {
public var body: some Reducer<State, Action> {
BindingReducer()
Scope(state: \.whatsNewState, action: \.whatsNew) {
WhatsNew()
}
Reduce { state, action in
switch action {
@ -58,13 +45,6 @@ public struct About {
case .privacyPolicyButtonTapped:
return .none
case .whatsNew:
return .none
case .whatsNewButtonTapped:
state.whatsNewViewBinding = true
return .none
}
}
}

View File

@ -9,7 +9,6 @@ import SwiftUI
import ComposableArchitecture
import Generated
import UIComponents
import WhatsNew
public struct AboutView: View {
@Environment(\.openURL) var openURL
@ -22,55 +21,47 @@ public struct AboutView: View {
public var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading) {
VStack(alignment: .center) {
Asset.Assets.zashiTitle.image
.zImage(width: 63, height: 17, color: Asset.Colors.primary.color)
.padding(.top, 15)
.padding(.bottom, 8)
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
Text(L10n.About.title)
.zFont(.semiBold, size: 24, style: Design.Text.primary)
.padding(.top, 40)
Text(L10n.About.version(store.appVersion, store.appBuild))
.font(.custom(FontFamily.Inter.bold.name, size: 12))
.foregroundColor(Asset.Colors.primary.color)
.padding(.bottom, 25)
Text(L10n.About.info)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 12)
Text(L10n.About.additionalInfo)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 8)
}
.frame(maxWidth: .infinity)
Text(L10n.About.info)
.font(.custom(FontFamily.Inter.regular.name, size: 14))
.foregroundColor(Asset.Colors.shade30.color)
.padding(.bottom, 30)
ZashiButton(L10n.About.whatsNew) {
store.send(.whatsNewButtonTapped)
}
.padding(.bottom, 15)
ZashiButton(L10n.About.privacyPolicy) {
ActionRow(
icon: Asset.Assets.infoCircle.image,
title: L10n.About.privacyPolicy,
divider: false,
horizontalPadding: 4
) {
if let url = URL(string: "https://electriccoin.co/zashi-privacy-policy/") {
openURL(url)
}
}
.padding(.bottom, 25)
.padding(.top, 32)
Spacer()
Asset.Assets.zashiTitle.image
.zImage(width: 73, height: 20, color: Asset.Colors.primary.color)
.padding(.bottom, 16)
Text(L10n.Settings.version(store.appVersion, store.appBuild))
.zFont(size: 16, style: Design.Text.tertiary)
.padding(.bottom, 24)
}
.padding(.top, 20)
.onAppear { store.send(.onAppear) }
.zashiBack()
.screenTitle(L10n.Settings.about)
.walletStatusPanel(background: .transparent)
.navigationLinkEmpty(
isActive: $store.whatsNewViewBinding,
destination: {
WhatsNewView(
store: store.scope(
state: \.whatsNewState,
action: \.whatsNew
)
)
}
)
}
.navigationBarTitleDisplayMode(.inline)
.screenHorizontalPadding()
@ -81,7 +72,7 @@ public struct AboutView: View {
// MARK: Placeholders
extension About.State {
public static let initial = About.State(whatsNewState: .initial)
public static let initial = About.State()
}
extension About {

View File

@ -19,46 +19,43 @@ public struct DeleteWalletView: View {
public var body: some View {
WithPerceptionTracking {
ScrollView {
Group {
ZashiIcon()
Text(L10n.DeleteWallet.title)
.font(.custom(FontFamily.Inter.semiBold.name, size: 25))
.padding(.bottom, 15)
VStack(alignment: .leading) {
Text(L10n.DeleteWallet.message1)
.font(.custom(FontFamily.Inter.bold.name, size: 16))
Text(L10n.DeleteWallet.message2)
.font(.custom(FontFamily.Inter.medium.name, size: 16))
.padding(.top, 20)
}
HStack {
ZashiToggle(
isOn: $store.isAcknowledged,
label: L10n.DeleteWallet.iUnderstand
)
VStack(alignment: .leading, spacing: 0) {
Text(L10n.DeleteWallet.title)
.zFont(.semiBold, size: 24, style: Design.Text.primary)
.padding(.top, 40)
Spacer()
}
.padding(.top, 30)
ZashiButton(L10n.DeleteWallet.actionButtonTitle) {
store.send(.deleteTapped)
}
.disabled(!store.isAcknowledged || store.isProcessing)
.padding(.vertical, 50)
Text(L10n.DeleteWallet.message1)
.zFont(.semiBold, size: 16, style: Design.Text.primary)
.padding(.top, 12)
Text(L10n.DeleteWallet.message2)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 8)
.lineSpacing(1.5)
Spacer()
ZashiToggle(
isOn: $store.isAcknowledged,
label: L10n.DeleteWallet.iUnderstand
)
.padding(.bottom, 24)
ZashiButton(
L10n.DeleteWallet.actionButtonTitle,
type: .destructive1
) {
store.send(.deleteTapped)
}
.disabled(!store.isAcknowledged || store.isProcessing)
.padding(.bottom, 20)
}
.padding(.vertical, 1)
.zashiBack(store.isProcessing)
}
.navigationBarTitleDisplayMode(.inline)
.screenHorizontalPadding()
.applyScreenBackground()
.screenTitle(L10n.DeleteWallet.screenTitle.uppercased())
}
}

View File

@ -22,88 +22,79 @@ public struct PrivateDataConsentView: View {
public var body: some View {
WithPerceptionTracking {
ScrollView {
Group {
ZashiIcon()
.padding(.top, walletStatus != .none ? 30 : 0)
Text(L10n.PrivateDataConsent.title)
.font(.custom(FontFamily.Inter.semiBold.name, size: 25))
.multilineTextAlignment(.center)
.padding(.bottom, 35)
Text(L10n.PrivateDataConsent.message)
.font(.custom(FontFamily.Inter.regular.name, size: 14))
.padding(.bottom, 10)
.lineSpacing(3)
Text(L10n.PrivateDataConsent.note)
.font(.custom(FontFamily.Inter.regular.name, size: 12))
.lineSpacing(2)
HStack {
ZashiToggle(
isOn: $store.isAcknowledged,
label: L10n.PrivateDataConsent.confirmation
)
Spacer()
VStack(alignment: .leading, spacing: 0) {
Text(L10n.PrivateDataConsent.title)
.zFont(.semiBold, size: 24, style: Design.Text.primary)
.padding(.top, 40)
Text(L10n.PrivateDataConsent.message1)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 12)
Text(L10n.PrivateDataConsent.message2)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 8)
Text(L10n.PrivateDataConsent.message3)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 8)
Text(L10n.PrivateDataConsent.message4)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 8)
Spacer()
ZashiToggle(
isOn: $store.isAcknowledged,
label: L10n.PrivateDataConsent.confirmation
)
.padding(.bottom, 24)
if store.isExportingData {
ZashiButton(
L10n.Settings.exportPrivateData,
type: .secondary,
accessoryView: ProgressView()
) {
store.send(.exportRequested)
}
.padding(.top, 20)
.padding(.bottom, 40)
if store.isExportingData {
ZashiButton(
L10n.Settings.exportPrivateData,
type: .secondary,
accessoryView: ProgressView()
) {
store.send(.exportRequested)
}
.disabled(true)
.padding(.horizontal, 8)
.padding(.bottom, 8)
} else {
ZashiButton(
L10n.Settings.exportPrivateData,
type: .secondary
) {
store.send(.exportRequested)
}
.disabled(!store.isExportPossible)
.padding(.horizontal, 8)
.padding(.bottom, 8)
.disabled(true)
.padding(.bottom, 8)
} else {
ZashiButton(
L10n.Settings.exportPrivateData,
type: .secondary
) {
store.send(.exportRequested)
}
#if DEBUG
if store.isExportingLogs {
ZashiButton(
L10n.Settings.exportLogsOnly,
accessoryView: ProgressView()
) {
store.send(.exportLogsRequested)
}
.disabled(true)
.padding(.horizontal, 8)
.padding(.bottom, 50)
} else {
ZashiButton(
L10n.Settings.exportLogsOnly
) {
store.send(.exportLogsRequested)
}
.disabled(!store.isExportPossible)
.padding(.horizontal, 8)
.padding(.bottom, 50)
}
#endif
.disabled(!store.isExportPossible)
.padding(.bottom, 8)
}
#if DEBUG
if store.isExportingLogs {
ZashiButton(
L10n.Settings.exportLogsOnly,
accessoryView: ProgressView()
) {
store.send(.exportLogsRequested)
}
.disabled(true)
.padding(.bottom, 20)
} else {
ZashiButton(
L10n.Settings.exportLogsOnly
) {
store.send(.exportLogsRequested)
}
.disabled(!store.isExportPossible)
.padding(.bottom, 20)
}
#endif
}
.padding(.vertical, 1)
.zashiBack()
.onAppear {
store.send(.onAppear)
}
.onAppear { store.send(.onAppear)}
.walletStatusPanel()
shareLogsView()
@ -111,6 +102,7 @@ public struct PrivateDataConsentView: View {
.navigationBarTitleDisplayMode(.inline)
.screenHorizontalPadding()
.applyScreenBackground()
.screenTitle(L10n.PrivateDataConsent.screenTitle.uppercased())
}
}

View File

@ -19,21 +19,24 @@ public struct RecoveryPhraseDisplay {
@ObservableState
public struct State: Equatable {
@Presents public var alert: AlertState<Action>?
public var phrase: RecoveryPhrase?
public var showBackButton = false
public var birthday: Birthday?
public var birthdayValue: String?
public var isBirthdayHintVisible = false
public var isRecoveryPhraseHidden = true
public var phrase: RecoveryPhrase?
public var showBackButton = false
public init(
phrase: RecoveryPhrase? = nil,
showBackButton: Bool = false,
birthday: Birthday? = nil,
birthdayValue: String? = nil
birthdayValue: String? = nil,
phrase: RecoveryPhrase? = nil,
showBackButton: Bool = false
) {
self.phrase = phrase
self.showBackButton = showBackButton
self.birthday = birthday
self.birthdayValue = birthdayValue
self.phrase = phrase
self.showBackButton = showBackButton
}
}
@ -41,6 +44,8 @@ public struct RecoveryPhraseDisplay {
case alert(PresentationAction<Action>)
case finishedPressed
case onAppear
case recoveryPhraseTapped
case tooltipTapped
}
@Dependency(\.walletStorage) var walletStorage
@ -52,6 +57,7 @@ public struct RecoveryPhraseDisplay {
Reduce { state, action in
switch action {
case .onAppear:
state.isRecoveryPhraseHidden = true
do {
let storedWallet = try walletStorage.exportWallet()
state.birthday = storedWallet.birthday
@ -78,6 +84,14 @@ public struct RecoveryPhraseDisplay {
case .finishedPressed:
return .none
case .tooltipTapped:
state.isBirthdayHintVisible.toggle()
return .none
case .recoveryPhraseTapped:
state.isRecoveryPhraseHidden.toggle()
return .none
}
}
}

View File

@ -13,6 +13,11 @@ import MnemonicSwift
import Utils
public struct RecoveryPhraseDisplayView: View {
enum Constants {
static let blurValue = 15.0
static let blurBDValue = 10.0
}
@Perception.Bindable var store: StoreOf<RecoveryPhraseDisplay>
public init(store: StoreOf<RecoveryPhraseDisplay>) {
@ -20,85 +25,187 @@ public struct RecoveryPhraseDisplayView: View {
}
public var body: some View {
ScrollView {
WithPerceptionTracking {
VStack(alignment: .center) {
ZashiIcon()
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 0) {
if let groups = store.phrase?.toGroups() {
Text(L10n.RecoveryPhraseDisplay.title)
.zFont(.semiBold, size: 24, style: Design.Text.primary)
.padding(.top, 40)
if let groups = store.phrase?.toGroups() {
VStack {
Text(L10n.RecoveryPhraseDisplay.titlePart1)
.font(.custom(FontFamily.Inter.semiBold.name, size: 25))
Text(L10n.RecoveryPhraseDisplay.titlePart2)
.font(.custom(FontFamily.Inter.semiBold.name, size: 25))
}
.padding(.bottom, 15)
Text(L10n.RecoveryPhraseDisplay.description)
.zFont(size: 14, style: Design.Text.primary)
.lineSpacing(1.5)
.padding(.top, 8)
HStack(spacing: 0) {
Spacer()
Text(L10n.RecoveryPhraseDisplay.description)
.font(.custom(FontFamily.Inter.medium.name, size: 14))
.multilineTextAlignment(.center)
.padding(.bottom, 15)
HStack {
ForEach(groups, id: \.startIndex) { group in
VStack(alignment: .leading) {
HStack(spacing: 2) {
VStack(alignment: .trailing, spacing: 2) {
ForEach(Array(group.words.enumerated()), id: \.offset) { seedWord in
Text("\(seedWord.offset + group.startIndex + 1).")
.fixedSize()
.font(.custom(FontFamily.Inter.medium.name, size: 16))
}
}
VStack(alignment: .leading, spacing: 2) {
ForEach(Array(group.words.enumerated()), id: \.offset) { seedWord in
Text("\(seedWord.element.data)")
.fixedSize()
.font(.custom(FontFamily.Inter.medium.name, size: 16))
}
}
if group.startIndex == 0 {
ForEach(groups, id: \.startIndex) { group in
VStack(alignment: .leading) {
VStack(spacing: 5) {
ForEach(Array(group.words.enumerated()), id: \.offset) { seedWord in
HStack(spacing: 0) {
Text("\(seedWord.offset + group.startIndex + 1)")
.zFont(.semiBold, size: 14, style: Design.Text.tertiary)
.padding(.trailing, 8)
Text("\(seedWord.element.data)")
.zFont(size: 14, style: Design.Text.primary)
.minimumScaleFactor(0.35)
.lineLimit(1)
Spacer()
}
}
}
}
Spacer()
}
.frame(maxWidth: .infinity)
.padding(.horizontal, 15)
.padding(.bottom, 15)
if let birthdayValue = store.birthdayValue {
Text(L10n.RecoveryPhraseDisplay.birthdayHeight(birthdayValue))
.font(.custom(FontFamily.Inter.regular.name, size: 14))
.padding(.bottom, 15)
}
} else {
Text(L10n.RecoveryPhraseDisplay.noWords)
.font(.custom(FontFamily.Inter.regular.name, size: 14))
.multilineTextAlignment(.center)
.padding(.bottom, 35)
}
if !store.showBackButton {
ZashiButton(L10n.RecoveryPhraseDisplay.Button.wroteItDown) {
store.send(.finishedPressed)
.blur(radius: store.isRecoveryPhraseHidden ? Constants.blurValue : 0)
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
.padding(.horizontal, 16)
.background {
RoundedRectangle(cornerRadius: 24)
.fill(Design.Surfaces.bgSecondary.color)
}
.onTapGesture {
if !store.showBackButton {
store.send(.recoveryPhraseTapped, animation: .easeInOut)
}
}
.overlay {
if !store.showBackButton && store.isRecoveryPhraseHidden {
VStack(spacing: 0) {
Asset.Assets.eyeOn.image
.zImage(size: 26, style: Design.Text.primary)
Text(L10n.RecoveryPhraseDisplay.reveal)
.zFont(.semiBold, size: 20, style: Design.Text.primary)
.lineLimit(1)
}
}
}
.padding(.top, 20)
if let birthdayValue = store.birthdayValue {
HStack {
Button {
store.send(.tooltipTapped)
} label: {
HStack(spacing: 4) {
Text(L10n.RecoveryPhraseDisplay.birthdayTitle)
.zFont(.medium, size: 14, style: Design.Inputs.Filled.text)
Asset.Assets.infoOutline.image
.zImage(size: 16, style: Design.Inputs.Default.icon)
}
.padding(.top, 24)
}
Spacer()
}
.anchorPreference(
key: BirthdayPreferenceKey.self,
value: .bounds
) { $0 }
HStack {
Text("\(birthdayValue)")
.zFont(.medium, size: 16, style: Design.Inputs.Filled.text)
.padding(.horizontal, 14)
.padding(.vertical, 12)
.blur(radius: store.isRecoveryPhraseHidden ? Constants.blurBDValue : 0)
Spacer()
}
.background {
RoundedRectangle(cornerRadius: 10)
.fill(Design.Surfaces.bgSecondary.color)
}
.padding(.top, 6)
}
} else {
Text(L10n.RecoveryPhraseDisplay.noWords)
.zFont(.semiBold, size: 24, style: Design.Text.primary)
.padding(.top, 40)
.multilineTextAlignment(.center)
}
Spacer()
HStack(alignment: .top, spacing: 0) {
Asset.Assets.infoOutline.image
.zImage(size: 20, style: Design.Utility.WarningYellow._500)
.padding(.trailing, 12)
Text(L10n.RecoveryPhraseDisplay.warning)
.zFont(.medium, size: 12, style: Design.Utility.WarningYellow._700)
Spacer(minLength: 0)
}
.padding(.bottom, 24)
.padding(.horizontal, 20)
if !store.showBackButton {
ZashiButton(L10n.RecoveryPhraseDisplay.Button.wroteItDown) {
store.send(.finishedPressed)
}
.padding(.bottom, 20)
} else {
if store.isRecoveryPhraseHidden {
ZashiButton(
L10n.RecoveryPhraseDisplay.reveal,
prefixView:
Asset.Assets.eyeOn.image
.zImage(size: 20, style: Design.Btns.Primary.fg)
) {
store.send(.recoveryPhraseTapped, animation: .easeInOut)
}
.padding(.bottom, 20)
} else {
ZashiButton(
L10n.RecoveryPhraseDisplay.hide,
prefixView:
Asset.Assets.eyeOff.image
.zImage(size: 20, style: Design.Btns.Primary.fg)
) {
store.send(.recoveryPhraseTapped, animation: .easeInOut)
}
.padding(.bottom, 20)
}
}
}
.onAppear { store.send(.onAppear) }
.alert($store.scope(state: \.alert, action: \.alert))
.zashiBack(false, hidden: !store.showBackButton)
.overlayPreferenceValue(BirthdayPreferenceKey.self) { preferences in
if store.isBirthdayHintVisible {
GeometryReader { geometry in
preferences.map {
Tooltip(
title: L10n.RecoveryPhraseDisplay.birthdayTitle,
desc: L10n.RecoveryPhraseDisplay.birthdayDesc,
bottomMode: true
) {
store.send(.tooltipTapped)
}
.fixedSize(horizontal: false, vertical: true)
.frame(width: geometry.size.width)
.position(x: geometry[$0].midX, y: geometry[$0].minY)
.offset(x: 0, y: -geometry[$0].height - 10)
}
.padding(.bottom, 50)
}
}
.onAppear { store.send(.onAppear) }
.alert($store.scope(state: \.alert, action: \.alert))
.zashiBack(false, hidden: !store.showBackButton)
}
}
.navigationBarBackButtonHidden()
.navigationBarTitleDisplayMode(.inline)
.padding(.vertical, 1)
.screenHorizontalPadding()
.applyScreenBackground()
.screenTitle(L10n.RecoveryPhraseDisplay.screenTitle.uppercased())
}
}
@ -108,9 +215,9 @@ public struct RecoveryPhraseDisplayView: View {
store:
StoreOf<RecoveryPhraseDisplay>(
initialState: RecoveryPhraseDisplay.State(
birthdayValue: nil,
phrase: .placeholder,
showBackButton: true,
birthdayValue: nil
showBackButton: true
)
) {
RecoveryPhraseDisplay()
@ -123,8 +230,8 @@ public struct RecoveryPhraseDisplayView: View {
extension RecoveryPhraseDisplay.State {
public static let initial = RecoveryPhraseDisplay.State(
phrase: nil,
birthday: nil
birthday: nil,
phrase: nil
)
}

View File

@ -203,6 +203,10 @@ public struct Root {
Welcome()
}
Scope(state: \.phraseDisplayState, action: \.phraseDisplay) {
RecoveryPhraseDisplay()
}
initializationReduce()
destinationReduce()

View File

@ -22,10 +22,9 @@ public struct SecurityWarningView: View {
WithPerceptionTracking {
ScrollView {
Group {
ZashiIcon()
Text(L10n.SecurityWarning.title)
.font(.custom(FontFamily.Inter.semiBold.name, size: 25))
.padding(.top, 40)
.padding(.bottom, 15)
VStack(alignment: .leading) {
@ -54,6 +53,7 @@ public struct SecurityWarningView: View {
Spacer()
}
.padding(.top, 30)
.padding(.leading, 1)
ZashiButton(L10n.SecurityWarning.confirm) {
store.send(.confirmTapped)
@ -81,6 +81,7 @@ public struct SecurityWarningView: View {
.navigationBarTitleDisplayMode(.inline)
.screenHorizontalPadding()
.applyScreenBackground()
.screenTitle(L10n.SecurityWarning.screenTitle.uppercased())
}
}

View File

@ -0,0 +1,106 @@
//
// SendFeedbackStore.swift
// Zashi
//
// Created by Lukáš Korba on 10-11-2024.
//
import ComposableArchitecture
import UIComponents
import MessageUI
import SupportDataGenerator
import Generated
@Reducer
public struct SendFeedback {
@ObservableState
public struct State: Equatable {
public var canSendMail = false
public var memoState: MessageEditor.State = .initial
public var messageToBeShared: String?
public let ratings = ["😠", "😒", "🙂", "😄", "😍"]
public var selectedRating: Int?
public var supportData: SupportData?
public var invalidForm: Bool {
selectedRating == nil || memoState.text.isEmpty
}
public init(
) {
}
}
public enum Action: BindableAction, Equatable {
case binding(BindingAction<SendFeedback.State>)
case memo(MessageEditor.Action)
case onAppear
case ratingTapped(Int)
case sendTapped
case sendSupportMailFinished
case shareFinished
}
public init() { }
public var body: some Reducer<State, Action> {
BindingReducer()
Scope(state: \.memoState, action: \.memo) {
MessageEditor()
}
Reduce { state, action in
switch action {
case .onAppear:
state.memoState.text = ""
state.selectedRating = nil
state.canSendMail = MFMailComposeViewController.canSendMail()
return .none
case .sendTapped:
guard let selectedRating = state.selectedRating else {
return .none
}
var prefixMessage = "\(L10n.SendFeedback.ratingQuestion)\n\(state.ratings[selectedRating]) \(selectedRating + 1)/\(state.ratings.count)\n\n"
prefixMessage += "\(L10n.SendFeedback.howCanWeHelp)\n\(state.memoState.text)\n\n"
if state.canSendMail {
state.supportData = SupportDataGenerator.generate(prefixMessage)
return .none
} else {
var sharePrefix =
"""
===
\(L10n.SendFeedback.Share.notAppleMailInfo) \(SupportDataGenerator.Constants.email)
===
\(prefixMessage)
"""
let supportData = SupportDataGenerator.generate(sharePrefix)
state.messageToBeShared = supportData.message
}
return .none
case .sendSupportMailFinished:
state.supportData = nil
return .none
case .binding:
return .none
case .memo:
return .none
case .ratingTapped(let rating):
state.selectedRating = rating
return .none
case .shareFinished:
state.messageToBeShared = nil
return .none
}
}
}
}

View File

@ -0,0 +1,157 @@
//
// SendFeedbackView.swift
// Zashi
//
// Created by Lukáš Korba on 10-11-2024.
//
import SwiftUI
import ComposableArchitecture
import Generated
import UIComponents
import Utils
public struct SendFeedbackView: View {
@Perception.Bindable var store: StoreOf<SendFeedback>
public init(store: StoreOf<SendFeedback>) {
self.store = store
}
public var body: some View {
WithPerceptionTracking {
VStack(alignment: .leading, spacing: 0) {
Text(L10n.SendFeedback.title)
.zFont(.semiBold, size: 24, style: Design.Text.primary)
.padding(.top, 40)
Text(L10n.SendFeedback.desc)
.zFont(size: 14, style: Design.Text.primary)
.padding(.top, 8)
Text(L10n.SendFeedback.ratingQuestion)
.zFont(.medium, size: 14, style: Design.Text.primary)
.padding(.top, 32)
HStack(spacing: 12) {
ForEach(0..<5) { rating in
WithPerceptionTracking {
Button {
store.send(.ratingTapped(rating))
} label: {
Text(store.ratings[rating])
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(Design.Surfaces.bgSecondary.color)
}
.padding(3)
.overlay {
if let selectedRating = store.selectedRating, selectedRating == rating {
RoundedRectangle(cornerRadius: 14)
.stroke(Design.Text.primary.color)
}
}
}
}
}
}
.padding(.top, 12)
Text(L10n.SendFeedback.howCanWeHelp)
.zFont(.medium, size: 14, style: Design.Text.primary)
.padding(.top, 24)
MessageEditorView(
store: store.memoStore(),
title: "",
placeholder: L10n.SendFeedback.hcwhPlaceholder
)
.frame(height: 155)
if let supportData = store.supportData {
UIMailDialogView(
supportData: supportData,
completion: {
store.send(.sendSupportMailFinished)
}
)
// UIMailDialogView only wraps MFMailComposeViewController presentation
// so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0)
}
Spacer()
ZashiButton(
L10n.General.send
) {
store.send(.sendTapped)
}
.disabled(store.invalidForm)
.padding(.bottom, 20)
shareView()
}
.zashiBack()
.onAppear { store.send(.onAppear) }
}
.navigationBarTitleDisplayMode(.inline)
.screenHorizontalPadding()
.applyScreenBackground()
.screenTitle(L10n.SendFeedback.screenTitle.uppercased())
}
}
extension SendFeedbackView {
@ViewBuilder func shareView() -> some View {
if let message = store.messageToBeShared{
UIShareDialogView(activityItems: [
ShareableMessage(
title: L10n.SendFeedback.Share.title,
message: message,
desc: L10n.SendFeedback.Share.desc
),
]) {
store.send(.shareFinished)
}
// UIShareDialogView only wraps UIActivityViewController presentation
// so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0)
} else {
EmptyView()
}
}
}
// MARK: - Previews
#Preview {
SendFeedbackView(store: SendFeedback.initial)
}
// MARK: - Store
extension SendFeedback {
public static var initial = StoreOf<SendFeedback>(
initialState: .initial
) {
SendFeedback()
}
}
// MARK: - Placeholders
extension SendFeedback.State {
public static let initial = SendFeedback.State()
}
extension StoreOf<SendFeedback> {
func memoStore() -> StoreOf<MessageEditor> {
self.scope(
state: \.memoState,
action: \.memo
)
}
}

View File

@ -30,14 +30,14 @@ public struct AdvancedSettingsView: View {
VStack(spacing: 0) {
List {
Group {
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.key.image,
title: L10n.Settings.recoveryPhrase
) {
store.send(.protectedAccessRequest(.backupPhrase))
}
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.downloadCloud.image,
title: L10n.Settings.exportPrivateData
) {
@ -45,14 +45,14 @@ public struct AdvancedSettingsView: View {
}
if store.isEnoughFreeSpaceMode {
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.server.image,
title: L10n.Settings.chooseServer
) {
store.send(.updateDestination(.serverSetup))
}
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.currencyDollar.image,
title: L10n.CurrencyConversion.title
) {
@ -201,9 +201,9 @@ extension AdvancedSettings.State {
currencyConversionSetupState: .init(isSettingsView: true),
deleteWalletState: .initial,
phraseDisplayState: RecoveryPhraseDisplay.State(
birthday: nil,
phrase: nil,
showBackButton: false,
birthday: nil
showBackButton: false
),
privateDataConsentState: .initial,
serverSetupState: ServerSetup.State()
@ -222,8 +222,8 @@ extension StoreOf<AdvancedSettings> {
currencyConversionSetupState: .initial,
deleteWalletState: .initial,
phraseDisplayState: RecoveryPhraseDisplay.State(
phrase: nil,
birthday: nil
birthday: nil,
phrase: nil
),
privateDataConsentState: .initial,
serverSetupState: ServerSetup.State()

View File

@ -28,7 +28,7 @@ public struct IntegrationsView: View {
List {
Group {
if store.inAppBrowserURL != nil {
SettingsRow(
ActionRow(
icon: Asset.Assets.Partners.coinbase.image,
title: L10n.Settings.buyZecCB,
desc: L10n.Settings.coinbaseDesc,
@ -40,7 +40,7 @@ public struct IntegrationsView: View {
}
if store.featureFlags.flexa {
SettingsRow(
ActionRow(
icon: walletStatus == .restoring
? Asset.Assets.Partners.flexaDisabled.image
: Asset.Assets.Partners.flexa.image,

View File

@ -1,15 +1,14 @@
import SwiftUI
import ComposableArchitecture
import MessageUI
import About
import AppVersion
import Generated
import Models
import Pasteboard
import SupportDataGenerator
import ZcashLightClientKit
import AddressBook
import WhatsNew
import SendFeedback
@Reducer
public struct Settings {
@ -20,12 +19,13 @@ public struct Settings {
case addressBook
case advanced
case integrations
case sendFeedback
case whatsNew
}
public var aboutState: About.State
public var addressBookState: AddressBook.State
public var advancedSettingsState: AdvancedSettings.State
@Presents public var alert: AlertState<Action>?
public var appVersion = ""
public var appBuild = ""
public var destination: Destination?
@ -33,7 +33,9 @@ public struct Settings {
public var integrationsState: Integrations.State
public var isEnoughFreeSpaceMode = true
public var supportData: SupportData?
public var sendFeedbackState: SendFeedback.State = .initial
public var whatsNewState: WhatsNew.State = .initial
public init(
aboutState: About.State,
addressBookState: AddressBook.State,
@ -41,8 +43,7 @@ public struct Settings {
appVersion: String = "",
appBuild: String = "",
destination: Destination? = nil,
integrationsState: Integrations.State,
supportData: SupportData? = nil
integrationsState: Integrations.State
) {
self.aboutState = aboutState
self.addressBookState = addressBookState
@ -51,7 +52,6 @@ public struct Settings {
self.appBuild = appBuild
self.destination = destination
self.integrationsState = integrationsState
self.supportData = supportData
}
}
@ -60,19 +60,16 @@ public struct Settings {
case addressBook(AddressBook.Action)
case addressBookButtonTapped
case advancedSettings(AdvancedSettings.Action)
case alert(PresentationAction<Action>)
case copyEmail
case integrations(Integrations.Action)
case onAppear
case protectedAccessRequest(State.Destination)
case sendSupportMail
case sendSupportMailFinished
case sendFeedback(SendFeedback.Action)
case updateDestination(Settings.State.Destination?)
case whatsNew(WhatsNew.Action)
}
@Dependency(\.appVersion) var appVersion
@Dependency(\.localAuthentication) var localAuthentication
@Dependency(\.pasteboard) var pasteboard
public init() { }
@ -93,6 +90,14 @@ public struct Settings {
Integrations()
}
Scope(state: \.sendFeedbackState, action: \.sendFeedback) {
SendFeedback()
}
Scope(state: \.whatsNewState, action: \.whatsNew) {
WhatsNew()
}
Reduce { state, action in
switch action {
case .onAppear:
@ -110,13 +115,15 @@ public struct Settings {
case .addressBookButtonTapped:
return .none
case .copyEmail:
pasteboard.setString(SupportDataGenerator.Constants.email.redacted)
return .none
case .integrations:
return .none
case .sendFeedback:
return .none
case .whatsNew:
return .none
case .protectedAccessRequest(let destination):
return .run { send in
if await localAuthentication.authenticate() {
@ -128,51 +135,9 @@ public struct Settings {
state.destination = destination
return .none
case .sendSupportMail:
if MFMailComposeViewController.canSendMail() {
state.supportData = SupportDataGenerator.generate()
} else {
state.alert = AlertState.sendSupportMail()
}
return .none
case .sendSupportMailFinished:
state.supportData = nil
return .none
case .alert(.presented(let action)):
return Effect.send(action)
case .alert(.dismiss):
state.alert = nil
return .none
case .advancedSettings:
return .none
case .alert:
return .none
}
}
.ifLet(\.$alert, action: \.alert)
}
}
// MARK: Alerts
extension AlertState where Action == Settings.Action {
public static func sendSupportMail() -> AlertState {
AlertState {
TextState(L10n.Settings.Alert.CantSendEmail.title)
} actions: {
ButtonState(action: .copyEmail) {
TextState(L10n.Settings.Alert.CantSendEmail.copyEmail(SupportDataGenerator.Constants.email))
}
ButtonState(action: .sendSupportMailFinished) {
TextState(L10n.General.close)
}
} message: {
TextState(L10n.Settings.Alert.CantSendEmail.message)
}
}
}

View File

@ -8,6 +8,8 @@ import UIComponents
import PrivateDataConsent
import ServerSetup
import AddressBook
import WhatsNew
import SendFeedback
public struct SettingsView: View {
@Perception.Bindable var store: StoreOf<Settings>
@ -21,7 +23,7 @@ public struct SettingsView: View {
VStack {
List {
Group {
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.user.image,
title: L10n.Settings.addressBook
) {
@ -29,15 +31,15 @@ public struct SettingsView: View {
}
if store.isEnoughFreeSpaceMode {
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.integrations.image,
title: L10n.Settings.integrations,
accessoryView:
HStack(spacing: 0) {
Asset.Assets.Partners.coinbaseSeeklogo.image
.resizable()
.frame(width: 20, height: 20)
.resizable()
.frame(width: 20, height: 20)
if store.featureFlags.flexa {
Asset.Assets.Partners.flexaSeekLogo.image
.resizable()
@ -50,26 +52,34 @@ public struct SettingsView: View {
}
}
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.settings.image,
title: L10n.Settings.advanced
) {
store.send(.updateDestination(.advanced))
}
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.magicWand.image,
title: L10n.Settings.whatsNew
) {
store.send(.updateDestination(.whatsNew))
}
ActionRow(
icon: Asset.Assets.infoOutline.image,
title: L10n.Settings.about
) {
store.send(.updateDestination(.about))
}
SettingsRow(
ActionRow(
icon: Asset.Assets.Icons.messageSmile.image,
title: L10n.Settings.feedback,
divider: false
) {
store.send(.sendSupportMail)
store.send(.updateDestination(.sendFeedback))
//store.send(.sendSupportMail)
}
}
.listRowInsets(EdgeInsets())
@ -102,22 +112,22 @@ public struct SettingsView: View {
AddressBookView(store: store.addressBookStore())
}
)
.navigationLinkEmpty(
isActive: store.bindingFor(.whatsNew),
destination: {
WhatsNewView(store: store.whatsNewStore())
}
)
.navigationLinkEmpty(
isActive: store.bindingFor(.sendFeedback),
destination: {
SendFeedbackView(store: store.sendFeedbackStore())
}
)
.onAppear {
store.send(.onAppear)
}
if let supportData = store.supportData {
UIMailDialogView(
supportData: supportData,
completion: {
store.send(.sendSupportMailFinished)
}
)
// UIMailDialogView only wraps MFMailComposeViewController presentation
// so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0)
}
Spacer()
Asset.Assets.zashiTitle.image
@ -132,10 +142,6 @@ public struct SettingsView: View {
.applyScreenBackground()
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.alert(store: store.scope(
state: \.$alert,
action: \.alert
))
.zashiBack()
.screenTitle(L10n.Settings.title)
.walletStatusPanel()
@ -198,18 +204,32 @@ extension StoreOf<Settings> {
action: \.about
)
}
func addressBookStore() -> StoreOf<AddressBook> {
self.scope(
state: \.addressBookState,
action: \.addressBook
)
}
func integrationsStore() -> StoreOf<Integrations> {
self.scope(
state: \.integrationsState,
action: \.integrations
)
}
func addressBookStore() -> StoreOf<AddressBook> {
func sendFeedbackStore() -> StoreOf<SendFeedback> {
self.scope(
state: \.addressBookState,
action: \.addressBook
state: \.sendFeedbackState,
action: \.sendFeedback
)
}
func whatsNewStore() -> StoreOf<WhatsNew> {
self.scope(
state: \.whatsNewState,
action: \.whatsNew
)
}
}

View File

@ -7,11 +7,14 @@
import ComposableArchitecture
import WhatsNewProvider
import AppVersion
@Reducer
public struct WhatsNew {
@ObservableState
public struct State: Equatable {
public var appVersion = ""
public var appBuild = ""
public var latest: WhatNewRelease
public var releases: WhatNewReleases
@ -28,6 +31,7 @@ public struct WhatsNew {
case onAppear
}
@Dependency(\.appVersion) var appVersion
@Dependency(\.whatsNewProvider) var whatsNewProvider
public init() { }
@ -36,6 +40,8 @@ public struct WhatsNew {
Reduce { state, action in
switch action {
case .onAppear:
state.appVersion = appVersion.appVersion()
state.appBuild = appVersion.appBuild()
state.latest = whatsNewProvider.latest()
state.releases = whatsNewProvider.all()
return .none

View File

@ -20,65 +20,79 @@ public struct WhatsNewView: View {
public var body: some View {
WithPerceptionTracking {
ScrollView {
HStack {
Text(L10n.WhatsNew.version(store.latest.version))
.font(.custom(FontFamily.Inter.bold.name, size: 14))
VStack(spacing: 0) {
ScrollView {
HStack(spacing: 0) {
Text(L10n.WhatsNew.version(store.latest.version))
.zFont(.semiBold, size: 20, style: Design.Text.primary)
Spacer()
Text(store.latest.date)
.zFont(.semiBold, size: 14, style: Design.Text.primary)
}
.padding(.top, 40)
.padding(.bottom, 16)
.frame(maxWidth: .infinity, alignment: .leading)
Spacer()
Text(store.latest.date)
.font(.custom(FontFamily.Inter.bold.name, size: 14))
}
.padding(.vertical, 25)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 35)
WithPerceptionTracking {
ForEach(0..<store.latest.sections.count, id: \.self) { sectionIndex in
VStack(alignment: .leading, spacing: 6) {
WithPerceptionTracking {
Text(store.latest.sections[sectionIndex].title)
.font(.custom(FontFamily.Inter.bold.name, size: 14))
.frame(maxWidth: .infinity, alignment: .leading)
WithPerceptionTracking {
ForEach(0..<store.latest.sections.count, id: \.self) { sectionIndex in
VStack(alignment: .leading, spacing: 6) {
WithPerceptionTracking {
ForEach(0..<store.latest.sections[sectionIndex].bulletpoints.count, id: \.self) { index in
WithPerceptionTracking {
if let previewText = try? AttributedString(
markdown: store.latest.sections[sectionIndex].bulletpoints[index],
including: \.zashiApp) {
HStack {
VStack {
Circle()
.frame(width: 4, height: 4)
.padding(.top, 7)
Text(store.latest.sections[sectionIndex].title)
.zFont(.semiBold, size: 16, style: Design.Text.primary)
.frame(maxWidth: .infinity, alignment: .leading)
WithPerceptionTracking {
ForEach(0..<store.latest.sections[sectionIndex].bulletpoints.count, id: \.self) { index in
WithPerceptionTracking {
if let previewText = try? AttributedString(
markdown: store.latest.sections[sectionIndex].bulletpoints[index],
including: \.zashiApp) {
HStack {
VStack {
Circle()
.frame(width: 4, height: 4)
.padding(.top, 7)
.padding(.leading, 8)
Spacer()
}
Spacer()
ZashiText(withAttributedString: previewText)
.zFont(size: 14, style: Design.Text.primary)
.frame(maxWidth: .infinity, alignment: .leading)
.accentColor(Asset.Colors.primary.color)
.lineSpacing(1.5)
}
ZashiText(withAttributedString: previewText)
.font(.custom(FontFamily.Inter.regular.name, size: 14))
.frame(maxWidth: .infinity, alignment: .leading)
.accentColor(Asset.Colors.primary.color)
}
}
}
}
}
}
.padding(.bottom, 16)
}
.padding(.bottom, 20)
}
.padding(.horizontal, 35)
}
.padding(.vertical, 1)
.zashiBack()
.onAppear { store.send(.onAppear) }
.screenTitle(L10n.Settings.whatsNew.uppercased())
Spacer()
Asset.Assets.zashiTitle.image
.zImage(width: 73, height: 20, color: Asset.Colors.primary.color)
.padding(.bottom, 16)
Text(L10n.Settings.version(store.appVersion, store.appBuild))
.zFont(size: 16, style: Design.Text.tertiary)
.padding(.bottom, 24)
}
.padding(.vertical, 1)
.zashiBack()
.onAppear { store.send(.onAppear) }
.screenTitle(L10n.About.whatsNew)
}
.navigationBarTitleDisplayMode(.inline)
.screenHorizontalPadding()
.applyScreenBackground()
}
}

View File

@ -15,17 +15,15 @@ public enum L10n {
return L10n.tr("Localizable", "qrCodeFor", String(describing: p1), fallback: "QR Code for %@")
}
public enum About {
/// Built by Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in user-feedback mechanism to enable more features, more quickly.
public static let additionalInfo = L10n.tr("Localizable", "about.additionalInfo", fallback: "Built by Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in user-feedback mechanism to enable more features, more quickly.")
/// Send and receive ZEC on Zashi!
/// Zashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps your transaction history and wallet balance private. Built by Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in user-feedback mechanism to enable more features, more quickly.
public static let info = L10n.tr("Localizable", "about.info", fallback: "Send and receive ZEC on Zashi!\nZashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps your transaction history and wallet balance private. Built by Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in user-feedback mechanism to enable more features, more quickly.")
/// Zashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps your transaction history and wallet balance private.
public static let info = L10n.tr("Localizable", "about.info", fallback: "Send and receive ZEC on Zashi!\nZashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps your transaction history and wallet balance private.")
/// Privacy Policy
public static let privacyPolicy = L10n.tr("Localizable", "about.privacyPolicy", fallback: "Privacy Policy")
/// Zashi Version %@ (%@)
public static func version(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "about.version", String(describing: p1), String(describing: p2), fallback: "Zashi Version %@ (%@)")
}
/// What's new
public static let whatsNew = L10n.tr("Localizable", "about.whatsNew", fallback: "What's new")
/// Introducing Zashi
public static let title = L10n.tr("Localizable", "about.title", fallback: "Introducing Zashi")
}
public enum AddressBook {
/// Add New Contact
@ -170,16 +168,18 @@ public enum L10n {
public static let title = L10n.tr("Localizable", "deeplinkWarning.title", fallback: "Looks like you used a third-party app to scan for payment.")
}
public enum DeleteWallet {
/// Delete
public static let actionButtonTitle = L10n.tr("Localizable", "deleteWallet.actionButtonTitle", fallback: "Delete")
/// Reset Zashi
public static let actionButtonTitle = L10n.tr("Localizable", "deleteWallet.actionButtonTitle", fallback: "Reset Zashi")
/// I understand
public static let iUnderstand = L10n.tr("Localizable", "deleteWallet.iUnderstand", fallback: "I understand")
/// Please don't delete this app unless you're sure you understand the effects.
public static let message1 = L10n.tr("Localizable", "deleteWallet.message1", fallback: "Please don't delete this app unless you're sure you understand the effects.")
/// Deleting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in Zashi or another Zcash wallet.
public static let message2 = L10n.tr("Localizable", "deleteWallet.message2", fallback: "Deleting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in Zashi or another Zcash wallet.")
/// Delete Zashi
public static let title = L10n.tr("Localizable", "deleteWallet.title", fallback: "Delete Zashi")
/// Please don't reset this app unless you're sure you understand the effects.
public static let message1 = L10n.tr("Localizable", "deleteWallet.message1", fallback: "Please don't reset this app unless you're sure you understand the effects.")
/// Resetting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in another Zcash wallet.
public static let message2 = L10n.tr("Localizable", "deleteWallet.message2", fallback: "Resetting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in another Zcash wallet.")
/// Reset
public static let screenTitle = L10n.tr("Localizable", "deleteWallet.screenTitle", fallback: "Reset")
/// Reset Zashi
public static let title = L10n.tr("Localizable", "deleteWallet.title", fallback: "Reset Zashi")
}
public enum ExportLogs {
public enum Alert {
@ -354,16 +354,18 @@ public enum L10n {
}
}
public enum PrivateDataConsent {
/// I agree
public static let confirmation = L10n.tr("Localizable", "privateDataConsent.confirmation", fallback: "I agree")
/// By clicking "I Agree" below, you give your consent to export Zashis private data which includes the entire history of the wallet, all private information, memos, amounts and recipient addresses, even for your shielded activity.*
///
/// This private data also gives the ability to see certain future actions you take with Zashi.
///
/// Sharing this private data is irrevocable once you have shared this private data with someone, there is no way to revoke their access.
public static let message = L10n.tr("Localizable", "privateDataConsent.message", fallback: "By clicking \"I Agree\" below, you give your consent to export Zashis private data which includes the entire history of the wallet, all private information, memos, amounts and recipient addresses, even for your shielded activity.*\n\nThis private data also gives the ability to see certain future actions you take with Zashi.\n\nSharing this private data is irrevocable — once you have shared this private data with someone, there is no way to revoke their access.")
/// I agree to Zashi's Export Private Data Policies and Privacy Policy
public static let confirmation = L10n.tr("Localizable", "privateDataConsent.confirmation", fallback: "I agree to Zashi's Export Private Data Policies and Privacy Policy")
/// By clicking I Agree below, you give your consent to export Zashis private data which includes the entire history of the wallet, sll private information, memos, amounts, and recipient addresses, even for your shielded activity.*
public static let message1 = L10n.tr("Localizable", "privateDataConsent.message1", fallback: "By clicking “I Agree” below, you give your consent to export Zashis private data which includes the entire history of the wallet, sll private information, memos, amounts, and recipient addresses, even for your shielded activity.*")
/// The private data also gives the ability to see certain future actions you take with Zashi.
public static let message2 = L10n.tr("Localizable", "privateDataConsent.message2", fallback: "The private data also gives the ability to see certain future actions you take with Zashi.")
/// Sharing this private data is irrevocable - once you have shared this private data with someone, there is no way to revoke their access.
public static let message3 = L10n.tr("Localizable", "privateDataConsent.message3", fallback: "Sharing this private data is irrevocable - once you have shared this private data with someone, there is no way to revoke their access.")
/// *Note that this private data does not give them the ability to spend your funds, only the ability to see what you do with your funds.
public static let note = L10n.tr("Localizable", "privateDataConsent.note", fallback: "*Note that this private data does not give them the ability to spend your funds, only the ability to see what you do with your funds.")
public static let message4 = L10n.tr("Localizable", "privateDataConsent.message4", fallback: "*Note that this private data does not give them the ability to spend your funds, only the ability to see what you do with your funds.")
/// Data Export
public static let screenTitle = L10n.tr("Localizable", "privateDataConsent.screenTitle", fallback: "Data Export")
/// Consent for Exporting Private Data
public static let title = L10n.tr("Localizable", "privateDataConsent.title", fallback: "Consent for Exporting Private Data")
}
@ -388,6 +390,8 @@ public enum L10n {
public static let title = L10n.tr("Localizable", "proposalPartial.title", fallback: "Send Failed")
/// Transaction Ids
public static let transactionIds = L10n.tr("Localizable", "proposalPartial.transactionIds", fallback: "Transaction Ids")
/// Transaction statuses:
public static let transactionStatuses = L10n.tr("Localizable", "proposalPartial.transactionStatuses", fallback: "Transaction statuses:")
}
public enum Receive {
/// Copy
@ -422,18 +426,24 @@ public enum L10n {
}
}
public enum RecoveryPhraseDisplay {
/// Wallet birthday height: %@
public static func birthdayHeight(_ p1: Any) -> String {
return L10n.tr("Localizable", "recoveryPhraseDisplay.birthdayHeight", String(describing: p1), fallback: "Wallet birthday height: %@")
}
/// The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!
public static let description = L10n.tr("Localizable", "recoveryPhraseDisplay.description", fallback: "The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!")
/// Wallet Birthday Height determines the birth (chain) height of your wallet and facilitates faster wallet restore process. Save this number together with your seed phrase in a safe place.
public static let birthdayDesc = L10n.tr("Localizable", "recoveryPhraseDisplay.birthdayDesc", fallback: "Wallet Birthday Height determines the birth (chain) height of your wallet and facilitates faster wallet restore process. Save this number together with your seed phrase in a safe place.")
/// Wallet Birthday Height
public static let birthdayTitle = L10n.tr("Localizable", "recoveryPhraseDisplay.birthdayTitle", fallback: "Wallet Birthday Height")
/// The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device.
public static let description = L10n.tr("Localizable", "recoveryPhraseDisplay.description", fallback: "The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device.")
/// Hide security details
public static let hide = L10n.tr("Localizable", "recoveryPhraseDisplay.hide", fallback: "Hide security details")
/// The keys are missing. No backup phrase is stored in the keychain.
public static let noWords = L10n.tr("Localizable", "recoveryPhraseDisplay.noWords", fallback: "The keys are missing. No backup phrase is stored in the keychain.")
/// Your Secret
public static let titlePart1 = L10n.tr("Localizable", "recoveryPhraseDisplay.titlePart1", fallback: "Your Secret")
/// Reveal security details
public static let reveal = L10n.tr("Localizable", "recoveryPhraseDisplay.reveal", fallback: "Reveal security details")
/// Recovery Phrase
public static let titlePart2 = L10n.tr("Localizable", "recoveryPhraseDisplay.titlePart2", fallback: "Recovery Phrase")
public static let screenTitle = L10n.tr("Localizable", "recoveryPhraseDisplay.screenTitle", fallback: "Recovery Phrase")
/// Secure Your Wallet
public static let title = L10n.tr("Localizable", "recoveryPhraseDisplay.title", fallback: "Secure Your Wallet")
/// Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!
public static let warning = L10n.tr("Localizable", "recoveryPhraseDisplay.warning", fallback: "Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!")
public enum Alert {
public enum Failed {
/// Attempt to load the stored wallet from the keychain failed. Error: %@
@ -641,6 +651,8 @@ public enum L10n {
public static let acknowledge = L10n.tr("Localizable", "securityWarning.acknowledge", fallback: "I acknowledge")
/// Confirm
public static let confirm = L10n.tr("Localizable", "securityWarning.confirm", fallback: "Confirm")
/// Privacy Notice
public static let screenTitle = L10n.tr("Localizable", "securityWarning.screenTitle", fallback: "Privacy Notice")
/// Security warning:
public static let title = L10n.tr("Localizable", "securityWarning.title", fallback: "Security warning:")
/// Zashi %@ (%@) is a Zcash-only, shielded wallet built by Zcashers for Zcashers. Zashi has been engineered for your privacy and safety. By installing and using Zashi, you consent to share crash reports with Electric Coin Co. (the wallet developer), which will help us improve the Zashi user experience.*
@ -734,6 +746,28 @@ public enum L10n {
public static let total = L10n.tr("Localizable", "send.requestPayment.total", fallback: "Total")
}
}
public enum SendFeedback {
/// Please let us know about any problems you have had, or features you want to see in the future.
public static let desc = L10n.tr("Localizable", "sendFeedback.desc", fallback: "Please let us know about any problems you have had, or features you want to see in the future.")
/// I would like to ask about...
public static let hcwhPlaceholder = L10n.tr("Localizable", "sendFeedback.hcwhPlaceholder", fallback: "I would like to ask about...")
/// How can we help you?
public static let howCanWeHelp = L10n.tr("Localizable", "sendFeedback.howCanWeHelp", fallback: "How can we help you?")
/// How is your Zashi experience?
public static let ratingQuestion = L10n.tr("Localizable", "sendFeedback.ratingQuestion", fallback: "How is your Zashi experience?")
/// Support
public static let screenTitle = L10n.tr("Localizable", "sendFeedback.screenTitle", fallback: "Support")
/// Send Us Feedback
public static let title = L10n.tr("Localizable", "sendFeedback.title", fallback: "Send Us Feedback")
public enum Share {
/// Zashi
public static let desc = L10n.tr("Localizable", "sendFeedback.share.desc", fallback: "Zashi")
/// Your device doesnt have an Apple email set up, so we prepared this message for you to send using your preferred email client. Please send this message to:
public static let notAppleMailInfo = L10n.tr("Localizable", "sendFeedback.share.notAppleMailInfo", fallback: "Your device doesnt have an Apple email set up, so we prepared this message for you to send using your preferred email client. Please send this message to:")
/// Support message
public static let title = L10n.tr("Localizable", "sendFeedback.share.title", fallback: "Support message")
}
}
public enum ServerSetup {
/// Active
public static let active = L10n.tr("Localizable", "serverSetup.active", fallback: "Active")
@ -771,8 +805,8 @@ public enum L10n {
}
}
public enum Settings {
/// About Us
public static let about = L10n.tr("Localizable", "settings.about", fallback: "About Us")
/// About
public static let about = L10n.tr("Localizable", "settings.about", fallback: "About")
/// Address Book
public static let addressBook = L10n.tr("Localizable", "settings.addressBook", fallback: "Address Book")
/// Advanced Settings
@ -785,8 +819,8 @@ public enum L10n {
public static let coinbaseDesc = L10n.tr("Localizable", "settings.coinbaseDesc", fallback: "A hassle-free way to buy ZEC and get it directly into your Zashi wallet.")
/// Currency Conversion
public static let currencyConversion = L10n.tr("Localizable", "settings.currencyConversion", fallback: "Currency Conversion")
/// Delete Zashi
public static let deleteZashi = L10n.tr("Localizable", "settings.deleteZashi", fallback: "Delete Zashi")
/// Reset Zashi
public static let deleteZashi = L10n.tr("Localizable", "settings.deleteZashi", fallback: "Reset Zashi")
/// You will be asked to confirm on the next screen
public static let deleteZashiWarning = L10n.tr("Localizable", "settings.deleteZashiWarning", fallback: "You will be asked to confirm on the next screen")
/// Export logs only
@ -811,6 +845,8 @@ public enum L10n {
public static func version(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "settings.version", String(describing: p1), String(describing: p2), fallback: "Version %@ (%@)")
}
/// What's new
public static let whatsNew = L10n.tr("Localizable", "settings.whatsNew", fallback: "What's new")
public enum Alert {
public enum CantSendEmail {
/// Copy %@

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "eyeOff.png",
"filename" : "eye-off.png",
"idiom" : "universal"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "eyeOn.png",
"filename" : "eye.png",
"idiom" : "universal"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "magicWand.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -5,6 +5,7 @@
// MARK: - Security Warning
"securityWarning.title" = "Security warning:";
"securityWarning.screenTitle" = "Privacy Notice";
"securityWarning.warningA" = "Zashi %@ (%@) is a Zcash-only, shielded wallet — built by Zcashers for Zcashers. Zashi has been engineered for your privacy and safety. By installing and using Zashi, you consent to share crash reports with Electric Coin Co. (the wallet developer), which will help us improve the Zashi user experience.*";
"securityWarning.warningB" = "Please acknowledge and confirm below to proceed.";
"securityWarning.warningC" = "*Note";
@ -13,12 +14,16 @@
"securityWarning.confirm" = "Confirm";
// MARK: - Secret Recovery Phrase Display
"recoveryPhraseDisplay.titlePart1" = "Your Secret";
"recoveryPhraseDisplay.titlePart2" = "Recovery Phrase";
"recoveryPhraseDisplay.description" = "The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!";
"recoveryPhraseDisplay.screenTitle" = "Recovery Phrase";
"recoveryPhraseDisplay.title" = "Secure Your Wallet";
"recoveryPhraseDisplay.description" = "The following 24 words are the keys to your funds and are the only way to recover your funds if you get locked out or get a new device.";
"recoveryPhraseDisplay.button.wroteItDown" = "I've saved it";
"recoveryPhraseDisplay.noWords" = "The keys are missing. No backup phrase is stored in the keychain.";
"recoveryPhraseDisplay.birthdayHeight" = "Wallet birthday height: %@";
"recoveryPhraseDisplay.birthdayTitle" = "Wallet Birthday Height";
"recoveryPhraseDisplay.birthdayDesc" = "Wallet Birthday Height determines the birth (chain) height of your wallet and facilitates faster wallet restore process. Save this number together with your seed phrase in a safe place.";
"recoveryPhraseDisplay.warning" = "Protect your ZEC by storing this phrase in a place you trust and never share it with anyone!";
"recoveryPhraseDisplay.reveal" = "Reveal security details";
"recoveryPhraseDisplay.hide" = "Hide security details";
"recoveryPhraseDisplay.alert.failed.title" = "Failed to load stored wallet";
"recoveryPhraseDisplay.alert.failed.message" = "Attempt to load the stored wallet from the keychain failed. Error: %@";
@ -33,6 +38,7 @@
"proposalPartial.mailPart1" = "Hi Zashi Team,";
"proposalPartial.mailPart2" = "While sending a transaction to a TEX address, I encountered an error state. I'm reaching out to get guidance on how to recover my funds.";
"proposalPartial.mailPart3" = "Thank you.";
"proposalPartial.transactionStatuses" = "Transaction statuses:";
// MARK: - Import Wallet Screen
"importWallet.description" = "Enter secret\nrecovery phrase";
@ -175,11 +181,12 @@
"zecKeyboard.invalid" = "This transaction amount is invalid.";
// MARK: - Delete Wallet
"deleteWallet.title" = "Delete Zashi";
"deleteWallet.message1" = "Please don't delete this app unless you're sure you understand the effects.";
"deleteWallet.message2" = "Deleting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in Zashi or another Zcash wallet.";
"deleteWallet.title" = "Reset Zashi";
"deleteWallet.message1" = "Please don't reset this app unless you're sure you understand the effects.";
"deleteWallet.message2" = "Resetting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in another Zcash wallet.";
"deleteWallet.iUnderstand" = "I understand";
"deleteWallet.actionButtonTitle" = "Delete";
"deleteWallet.actionButtonTitle" = "Reset Zashi";
"deleteWallet.screenTitle" = "Reset";
// MARK: - Not Enogh Free Space
"notEnoughFreeSpace.title" = "Not enough free space";
@ -190,9 +197,9 @@
// MARK: - About
"about.info" = "Send and receive ZEC on Zashi!
Zashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps your transaction history and wallet balance private. Built by Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in user-feedback mechanism to enable more features, more quickly.";
"about.version" = "Zashi Version %@ (%@)";
"about.whatsNew" = "What's new";
Zashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps your transaction history and wallet balance private.";
"about.additionalInfo" = "Built by Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in user-feedback mechanism to enable more features, more quickly.";
"about.title" = "Introducing Zashi";
"about.privacyPolicy" = "Privacy Policy";
// MARK: - What's new
@ -202,7 +209,7 @@ Zashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps you
"settings.title" = "Settings";
"settings.addressBook" = "Address Book";
"settings.advanced" = "Advanced Settings";
"settings.about" = "About Us";
"settings.about" = "About";
"settings.feedback" = "Send Us Feedback";
"settings.integrations" = "Integrations";
"settings.recoveryPhrase" = "Recovery Phrase";
@ -210,25 +217,38 @@ Zashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps you
"settings.exportLogsOnly" = "Export logs only";
"settings.chooseServer" = "Choose a Server";
"settings.currencyConversion" = "Currency Conversion";
"settings.whatsNew" = "What's new";
"settings.buyZecCB" = "Buy ZEC with Coinbase";
"settings.coinbaseDesc" = "A hassle-free way to buy ZEC and get it directly into your Zashi wallet.";
"settings.flexa" = "Pay with Flexa";
"settings.flexaDesc" = "Pay with Flexa payment clips and explore a new way of spending Zcash.";
"settings.restoreWarning" = "During the Restore process, it is not possible to use payment integrations.";
"settings.deleteZashi" = "Delete Zashi";
"settings.deleteZashi" = "Reset Zashi";
"settings.deleteZashiWarning" = "You will be asked to confirm on the next screen";
"settings.version" = "Version %@ (%@)";
"settings.alert.cantSendEmail.title" = "Oh, no!";
"settings.alert.cantSendEmail.message" = "It looks like you don't have a default email app configured on your device. Copy the address below, and use your favorite email client to send us a message.";
"settings.alert.cantSendEmail.copyEmail" = "Copy %@";
// MARK: - Send Feedback
"sendFeedback.title" = "Send Us Feedback";
"sendFeedback.screenTitle" = "Support";
"sendFeedback.desc" = "Please let us know about any problems you have had, or features you want to see in the future.";
"sendFeedback.ratingQuestion" = "How is your Zashi experience?";
"sendFeedback.howCanWeHelp" = "How can we help you?";
"sendFeedback.hcwhPlaceholder" = "I would like to ask about...";
"sendFeedback.share.title" = "Support message";
"sendFeedback.share.desc" = "Zashi";
"sendFeedback.share.notAppleMailInfo" = "Your device doesnt have an Apple email set up, so we prepared this message for you to send using your preferred email client. Please send this message to:";
// MARK: - Private Data Consent
"privateDataConsent.title" = "Consent for Exporting Private Data";
"privateDataConsent.message" = "By clicking \"I Agree\" below, you give your consent to export Zashis private data which includes the entire history of the wallet, all private information, memos, amounts and recipient addresses, even for your shielded activity.*\n
This private data also gives the ability to see certain future actions you take with Zashi.\n
Sharing this private data is irrevocable — once you have shared this private data with someone, there is no way to revoke their access.";
"privateDataConsent.note" = "*Note that this private data does not give them the ability to spend your funds, only the ability to see what you do with your funds.";
"privateDataConsent.confirmation" = "I agree";
"privateDataConsent.message1" = "By clicking “I Agree” below, you give your consent to export Zashis private data which includes the entire history of the wallet, sll private information, memos, amounts, and recipient addresses, even for your shielded activity.*";
"privateDataConsent.message2" = "The private data also gives the ability to see certain future actions you take with Zashi.";
"privateDataConsent.message3" = "Sharing this private data is irrevocable - once you have shared this private data with someone, there is no way to revoke their access.";
"privateDataConsent.message4" = "*Note that this private data does not give them the ability to spend your funds, only the ability to see what you do with your funds.";
"privateDataConsent.confirmation" = "I agree to Zashi's Export Private Data Policies and Privacy Policy";
"privateDataConsent.screenTitle" = "Data Export";
// MARK: - Sync message
"sync.message.uptodate" = "Up-To-Date";

View File

@ -74,6 +74,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 magicWand = ImageAsset(name: "magicWand")
public static let messageSmile = ImageAsset(name: "messageSmile")
public static let partial = ImageAsset(name: "partial")
public static let pencil = ImageAsset(name: "pencil")

View File

@ -29,11 +29,11 @@ public struct RecoveryPhrase: Equatable, Redactable {
}
public func toGroups() -> [Group] {
let chunks = words.count / 2
let chunks = words.count / 3
var res: [Group] = []
for i in 0..<2 {
for i in 0..<3 {
var subwords: [RedactableString] = []
for j in (i * chunks)..<((i + 1) * chunks) {
subwords.append(words[j])

View File

@ -1,5 +1,5 @@
//
// SettingsRow.swift
// ActionRow.swift
// Zashi
//
// Created by Lukáš Korba on 22.08.2024.
@ -9,7 +9,7 @@ import SwiftUI
import Generated
public struct SettingsRow<AccessoryContent>: View where AccessoryContent: View{
public struct ActionRow<AccessoryContent>: View where AccessoryContent: View{
@Environment(\.isEnabled) private var isEnabled
let icon: Image
@ -18,15 +18,17 @@ public struct SettingsRow<AccessoryContent>: View where AccessoryContent: View{
let customIcon: Bool
@ViewBuilder let accessoryView: AccessoryContent?
let divider: Bool
let horizontalPadding: CGFloat
let action: () -> Void
init(
public init(
icon: Image,
title: String,
desc: String? = nil,
customIcon: Bool = false,
accessoryView: AccessoryContent? = EmptyView(),
divider: Bool = true,
horizontalPadding: CGFloat = 20,
action: @escaping () -> Void
) {
self.icon = icon
@ -35,6 +37,7 @@ public struct SettingsRow<AccessoryContent>: View where AccessoryContent: View{
self.customIcon = customIcon
self.accessoryView = accessoryView
self.divider = divider
self.horizontalPadding = horizontalPadding
self.action = action
}
@ -89,7 +92,7 @@ public struct SettingsRow<AccessoryContent>: View where AccessoryContent: View{
.zImage(size: 20, style: Design.Text.quaternary)
}
}
.padding(.horizontal, 20)
.padding(.horizontal, horizontalPadding)
if divider {
Design.Surfaces.divider.color

View File

@ -15,25 +15,21 @@ public struct CheckboxToggleStyle: ToggleStyle {
HStack {
ZStack {
if configuration.isOn {
Image(systemName: "checkmark.square.fill")
.renderingMode(.template)
.resizable()
.frame(width: 20, height: 20, alignment: .center)
.background {
Asset.Colors.background.color
.scaleEffect(x: 0.94, y: 0.94)
RoundedRectangle(cornerRadius: 4)
.fill(Design.Checkboxes.onBg.color)
.frame(width: 16, height: 16)
.overlay {
Asset.Assets.check.image
.zImage(size: 12, style: Design.Checkboxes.onFg)
}
.foregroundColor(Asset.Colors.primary.color)
} else {
Image(systemName: "square")
.renderingMode(.template)
.resizable()
.frame(width: 20, height: 20, alignment: .center)
RoundedRectangle(cornerRadius: 4)
.fill(Design.Checkboxes.offBg.color)
.frame(width: 16, height: 16)
.background {
Asset.Colors.background.color
.scaleEffect(x: 0.94, y: 0.94)
RoundedRectangle(cornerRadius: 4)
.stroke(Design.Checkboxes.offStroke.color)
}
.foregroundColor(Asset.Colors.primary.color)
}
}
.onTapGesture {

View File

@ -0,0 +1,21 @@
//
// BirthdayPreferenceKey.swift
// Zashi
//
// Created by Lukáš Korba on 10-30-2024.
//
import SwiftUI
public struct BirthdayPreferenceKey: PreferenceKey {
public typealias Value = Anchor<CGRect>?
public static var defaultValue: Value = nil
public static func reduce(
value: inout Value,
nextValue: () -> Value
) {
value = nextValue() ?? value
}
}

View File

@ -27,11 +27,17 @@ public struct ZashiToggle: View {
Button {
isOn.toggle()
} label: {
Toggle(isOn: $isOn, label: {
HStack(alignment: .top, spacing: 0) {
Toggle(isOn: $isOn, label: {})
.toggleStyle(CheckboxToggleStyle())
.padding(.trailing, 8)
Text(label)
.font(.custom(FontFamily.Inter.medium.name, size: 14))
})
.toggleStyle(CheckboxToggleStyle())
.zFont(.medium, size: 14, style: Design.Text.primary)
.multilineTextAlignment(.leading)
Spacer()
}
}
.foregroundColor(textColor)
}

View File

@ -13,22 +13,27 @@ public struct Tooltip: View {
public var onTapGesture: () -> Void
public var title: String
public var desc: String
public var bottomMode: Bool
public init(
title: String,
desc: String,
bottomMode: Bool = false,
onTapGesture: @escaping () -> Void
) {
self.title = title
self.desc = desc
self.bottomMode = bottomMode
self.onTapGesture = onTapGesture
}
public var body: some View {
VStack(alignment: .center, spacing: 0) {
Asset.Assets.tooltip.image
.zImage(width: 16, height: 6, style: Design.HintTooltips.surfacePrimary)
.offset(x: 0, y: 2)
if !bottomMode {
Asset.Assets.tooltip.image
.zImage(width: 16, height: 6, style: Design.HintTooltips.surfacePrimary)
.offset(x: 0, y: 2)
}
HStack(alignment: .top) {
VStack(alignment: .leading) {
@ -41,6 +46,7 @@ public struct Tooltip: View {
.font(.custom(FontFamily.Inter.medium.name, size: 14))
.foregroundColor(Design.Text.lightSupport.color)
.lineLimit(nil)
.lineSpacing(1.5)
}
Asset.Assets.buttonCloseX.image
@ -54,6 +60,13 @@ public struct Tooltip: View {
// TODO: Colors from Design once available
.shadow(color: Color(red: 0.137, green: 0.122, blue: 0.125).opacity(0.03), radius: 4, x: 0, y: 4)
.shadow(color: Color(red: 0.137, green: 0.122, blue: 0.125).opacity(0.08), radius: 8, x: 0, y: 12)
if bottomMode {
Asset.Assets.tooltip.image
.zImage(width: 16, height: 6, style: Design.HintTooltips.surfacePrimary)
.rotationEffect(Angle(degrees: 180))
.offset(x: 0, y: -2)
}
}
.onTapGesture { onTapGesture() }
}

View File

@ -48,6 +48,46 @@ public final class ShareableImage: NSObject, UIActivityItemSource {
}
}
public final class ShareableMessage: NSObject, UIActivityItemSource {
let title: String
let message: String
let desc: String
public init(title: String, message: String, desc: String) {
self.title = title
self.message = message
self.desc = desc
super.init()
}
public func activityViewControllerPlaceholderItem(
_ activityViewController: UIActivityViewController
) -> Any {
message
}
public func activityViewController(
_ activityViewController: UIActivityViewController,
itemForActivityType activityType: UIActivity.ActivityType?
) -> Any? {
message
}
public func activityViewControllerLinkMetadata(
_ activityViewController: UIActivityViewController
) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
if let image = UIImage(named: "ZashiLogo") {
metadata.iconProvider = NSItemProvider(object: image)
}
metadata.title = title
metadata.originalURL = URL(fileURLWithPath: desc)
return metadata
}
}
public class UIShareDialog: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)

View File

@ -1948,7 +1948,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
@ -1979,7 +1979,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;
@ -2009,7 +2009,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;