- Flexa tested and fixes implemented on our side
- SDK numerator/denominator fixes WIP

[#1284] Flexa

- Total vs. Available balance handling
- Fixed freeze of Zashi by wrapping Flexa code into Task
- Prints cleanup

[#1284] Flexa

- SDK reverted to 2.2.3

[#1284] Flexa

- rebased

[#1284] Flexa

- Initialization moved to the Integrations screen

[#1284] Flexa

- simple feature flaggins system implemented
- flexa is behind the feature flags, hidden in the production & testnet builds, available for internal builds

[#1284] Flexa

- flexa seek logo added to the integrations screen

[#1284] Flexa

- fixes

[#1284] Flexa

- versions bumped up

[#1284] Flexa

- versions cleanup

[#1284] Flexa

- live vs. test Flexa keys support

[#1284] Flexa

- Versions

[#1284] Flexa

- WIP

[#1284] Flexa

- disconnect a user after reset zashi

[#1284] Flexa

- ZEC logo in Flexa
- shielded balance instead of total balance

[#1284] Flexa

- code cleanup
This commit is contained in:
Lukas Korba 2024-10-08 13:22:58 +02:00
parent 5b8b5f0d62
commit 32a49809cd
23 changed files with 91 additions and 64 deletions

View File

@ -89,7 +89,7 @@ let package = Package(
.package(url: "https://github.com/zcash-hackworks/MnemonicSwift", from: "2.2.4"),
.package(url: "https://github.com/Electric-Coin-Company/zcash-swift-wallet-sdk", from: "2.2.6"),
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.27.0"),
.package(url: "https://github.com/flexa/flexa-ios.git", exact: "1.0.4"),
.package(url: "https://github.com/flexa/flexa-ios.git", from: "1.0.5"),
.package(url: "https://github.com/pacu/zcash-swift-payment-uri", from: "0.1.0-beta.9"),
.package(url: "https://github.com/airbnb/lottie-spm.git", from: "4.5.0")
],

View File

@ -27,4 +27,5 @@ public struct FlexaHandlerClient {
public var transactionSent: @Sendable (String, String) -> Void
public var updateBalance: @Sendable (Zatoshi, Zatoshi?) -> Void
public var flexaAlert: @Sendable (String, String) -> Void
public var signOut: @Sendable () -> Void
}

View File

@ -20,7 +20,7 @@ enum Constants {
static let zecHash = "bip122:00040fe8ec8471911baa1db1266ea15d"
static let zecId = "\(Constants.zecHash)/slip44:133"
static func accountId() -> String {
static func assetAccountHash() -> String {
let uuid = UUID()
let uuidString = uuid.uuidString
let data = Data(uuidString.utf8)
@ -61,8 +61,10 @@ extension FlexaHandlerClient: DependencyKey {
onTransactionRequest.send(nil)
Flexa.sections([.spend])
.appAccounts(FlexaHandlerClient.accounts(latestSpendableBalance.value, zecAvailableAmount: latestSpendableAvailableBalance.value))
.selectedAsset(Constants.accountId(), Constants.zecId)
.assetAccounts(
FlexaHandlerClient.accounts(latestSpendableBalance.value, zecAvailableAmount: latestSpendableAvailableBalance.value)
)
.selectedAsset(Constants.assetAccountHash(), Constants.zecId)
.onTransactionRequest({
onTransactionRequest.send($0)
})
@ -117,7 +119,9 @@ extension FlexaHandlerClient: DependencyKey {
updateBalance: {
latestSpendableBalance.value = $0.decimalValue.decimalValue
latestSpendableAvailableBalance.value = $1?.decimalValue.decimalValue
Flexa.updateAppAccounts(FlexaHandlerClient.accounts(latestSpendableBalance.value, zecAvailableAmount: latestSpendableAvailableBalance.value))
Flexa.updateAssetAccounts(
FlexaHandlerClient.accounts(latestSpendableBalance.value, zecAvailableAmount: latestSpendableAvailableBalance.value)
)
},
flexaAlert: { title, message in
DispatchQueue.main.async {
@ -129,16 +133,19 @@ extension FlexaHandlerClient: DependencyKey {
alert.addAction(UIAlertAction(title: L10n.General.ok, style: .cancel))
UIViewController.showOnTop(alert)
}
},
signOut: {
Flexa.buildIdentity().build().close()
}
)
}
}
private extension FlexaHandlerClient {
static func accounts(_ zecAmount: Decimal = 0, zecAvailableAmount: Decimal? = nil) -> [FXAppAccount] {
static func accounts(_ zecAmount: Decimal = 0, zecAvailableAmount: Decimal? = nil) -> [FXAssetAccount] {
[
FXAppAccount(
accountId: Constants.accountId(),
FXAssetAccount(
assetAccountHash: Constants.assetAccountHash(),
displayName: "",
custodyModel: .local,
availableAssets: [
@ -146,7 +153,8 @@ private extension FlexaHandlerClient {
assetId: Constants.zecId,
symbol: "ZEC",
balance: zecAmount,
balanceAvailable: zecAvailableAmount
balanceAvailable: zecAvailableAmount,
icon: UIImage(named: "zcashZecLogo")
)
]
)
@ -160,7 +168,7 @@ private extension FlexaHandlerClient {
Flexa.initialize(
FXClient(
publishableKey: flexaPublishableKey,
appAccounts: FlexaHandlerClient.accounts(),
assetAccounts: FlexaHandlerClient.accounts(),
theme: .default
)
)

View File

@ -17,7 +17,8 @@ extension FlexaHandlerClient: TestDependencyKey {
clearTransactionRequest: unimplemented("\(Self.self).clearTransactionRequest"),
transactionSent: unimplemented("\(Self.self).transactionSent"),
updateBalance: unimplemented("\(Self.self).updateBalance"),
flexaAlert: unimplemented("\(Self.self).flexaAlert")
flexaAlert: unimplemented("\(Self.self).flexaAlert"),
signOut: unimplemented("\(Self.self).signOut")
)
}
@ -29,6 +30,7 @@ extension FlexaHandlerClient {
clearTransactionRequest: { },
transactionSent: { _, _ in },
updateBalance: { _, _ in },
flexaAlert: { _, _ in }
flexaAlert: { _, _ in },
signOut: { }
)
}

View File

@ -11,15 +11,20 @@ public struct PartnerKeys {
private enum Constants {
static let cbProjectId = "cbProjectId"
static let flexaPublishableKey = "flexaPublishableKey"
static let flexaPublishableTestKey = "flexaPublishableTestKey"
}
public static var cbProjectId: String? {
PartnerKeys.value(for: Constants.cbProjectId)
}
public static var flexaPublishableKey: String? {
PartnerKeys.value(for: Constants.flexaPublishableKey)
}
public static var flexaPublishableTestKey: String? {
PartnerKeys.value(for: Constants.flexaPublishableTestKey)
}
}
private extension PartnerKeys {

View File

@ -32,5 +32,5 @@ public struct ReadTransactionsStorageClient {
public let markIdAsRead: (RedactableString) throws -> Void
public var readIds: () throws -> [RedactableString: Bool]
public var availabilityTimestamp: () throws -> TimeInterval
public var nukeWallet: () throws -> Void
public var resetZashi: () throws -> Void
}

View File

@ -105,7 +105,7 @@ extension ReadTransactionsStorageClient: DependencyKey {
throw error
}
},
nukeWallet: {
resetZashi: {
let context = persistentContainer.viewContext
let deleteRequestIds = NSBatchDeleteRequest(

View File

@ -13,7 +13,7 @@ extension ReadTransactionsStorageClient: TestDependencyKey {
markIdAsRead: unimplemented("\(Self.self).markIdAsRead", placeholder: {}()),
readIds: unimplemented("\(Self.self).readIds", placeholder: [:]),
availabilityTimestamp: unimplemented("\(Self.self).availabilityTimestamp", placeholder: 0),
nukeWallet: unimplemented("\(Self.self).nukeWallet", placeholder: {}())
resetZashi: unimplemented("\(Self.self).resetZashi", placeholder: {}())
)
}
@ -22,6 +22,6 @@ extension ReadTransactionsStorageClient {
markIdAsRead: { _ in },
readIds: { [:] },
availabilityTimestamp: { 0 },
nukeWallet: { }
resetZashi: { }
)
}

View File

@ -136,7 +136,7 @@ public struct WalletStorage {
}
}
public func nukeWallet() {
public func resetZashi() {
deleteData(forKey: Constants.zcashStoredWallet)
}

View File

@ -73,7 +73,7 @@ public struct WalletStorageClient {
/// Use carefully: deletes the stored wallet.
/// There's no fate but what we make for ourselves - Sarah Connor.
public var nukeWallet: () -> Void
public var resetZashi: () -> Void
// TODO: str4d
// not sure what format the key is, for now I made it a String

View File

@ -36,8 +36,8 @@ extension WalletStorageClient: DependencyKey {
markUserPassedPhraseBackupTest: { flag in
try walletStorage.markUserPassedPhraseBackupTest(flag)
},
nukeWallet: {
walletStorage.nukeWallet()
resetZashi: {
walletStorage.resetZashi()
},
importAddressBookEncryptionKeys: { keys in
try walletStorage.importAddressBookEncryptionKeys(keys)

View File

@ -15,7 +15,7 @@ extension WalletStorageClient: TestDependencyKey {
areKeysPresent: unimplemented("\(Self.self).areKeysPresent", placeholder: false),
updateBirthday: unimplemented("\(Self.self).updateBirthday", placeholder: {}()),
markUserPassedPhraseBackupTest: unimplemented("\(Self.self).markUserPassedPhraseBackupTest", placeholder: {}()),
nukeWallet: unimplemented("\(Self.self).nukeWallet", placeholder: {}()),
resetZashi: unimplemented("\(Self.self).resetZashi", placeholder: {}()),
importAddressBookEncryptionKeys: unimplemented("\(Self.self).importAddressBookEncryptionKeys", placeholder: {}()),
exportAddressBookEncryptionKeys: unimplemented("\(Self.self).exportAddressBookEncryptionKeys", placeholder: .empty)
)
@ -28,7 +28,7 @@ extension WalletStorageClient {
areKeysPresent: { false },
updateBirthday: { _ in },
markUserPassedPhraseBackupTest: { _ in },
nukeWallet: { },
resetZashi: { },
importAddressBookEncryptionKeys: { _ in },
exportAddressBookEncryptionKeys: { .empty }
)

View File

@ -182,9 +182,9 @@ extension Root {
case .flexaTransactionFailed(let message):
flexaHandler.flexaAlert(L10n.Partners.Flexa.transactionFailedTitle, message)
return .none
case .tabs, .initialization, .onboarding, .updateStateAfterConfigUpdate, .alert, .phraseDisplay, .synchronizerStateChanged,
.welcome, .binding, .nukeWalletFailed, .nukeWalletSucceeded, .debug, .walletConfigLoaded, .exportLogs, .confirmationDialog,
.welcome, .binding, .resetZashiFailed, .resetZashiSucceeded, .debug, .walletConfigLoaded, .exportLogs, .confirmationDialog,
.notEnoughFreeSpace, .serverSetup, .serverSetupBindingUpdated, .batteryStateChanged, .cancelAllRunningEffects, .addressBookBinding, .addressBook, .addressBookContactBinding, .addressBookAccessGranted:
return .none
}

View File

@ -31,8 +31,8 @@ extension Root {
case initialSetups
case initializationFailed(ZcashError)
case initializationSuccessfullyDone(UnifiedAddress?)
case nukeWallet
case nukeWalletRequest
case resetZashi
case resetZashiRequest
case respondToWalletInitializationState(InitializationState)
case restoreExistingWallet
case seedValidationResult(Bool)
@ -110,10 +110,8 @@ extension Root {
if let accountBalance = latestState.data.accountBalance?.data {
let shieldedBalance = accountBalance.saplingBalance.spendableValue + accountBalance.orchardBalance.spendableValue
let shieldedWithPendingBalance = accountBalance.saplingBalance.total() + accountBalance.orchardBalance.total()
let transparentBalance = accountBalance.unshielded
let totalBalance = shieldedWithPendingBalance + transparentBalance
flexaHandler.updateBalance(totalBalance, shieldedBalance)
flexaHandler.updateBalance(shieldedWithPendingBalance, shieldedBalance)
}
// handle possible service unavailability
@ -368,24 +366,24 @@ extension Root {
return Effect.send(.initialization(.initializationFailed(error.toZcashError())))
}
case .initialization(.nukeWalletRequest):
case .initialization(.resetZashiRequest):
state.alert = AlertState.wipeRequest()
return .none
case .initialization(.nukeWallet), .tabs(.settings(.advancedSettings(.deleteWallet(.deleteTapped)))):
case .initialization(.resetZashi), .tabs(.settings(.advancedSettings(.deleteWallet(.deleteTapped)))):
guard let wipePublisher = sdkSynchronizer.wipe() else {
return Effect.send(.nukeWalletFailed)
return Effect.send(.resetZashiFailed)
}
return .publisher {
wipePublisher
.replaceEmpty(with: Void())
.map { _ in return Root.Action.nukeWalletSucceeded }
.replaceError(with: Root.Action.nukeWalletFailed)
.map { _ in return Root.Action.resetZashiSucceeded }
.replaceError(with: Root.Action.resetZashiFailed)
.receive(on: mainQueue)
}
.cancellable(id: SynchronizerCancelId, cancelInFlight: true)
case .nukeWalletSucceeded:
case .resetZashiSucceeded:
if state.appInitializationState != .keysMissing {
state = .initial
}
@ -393,8 +391,9 @@ extension Root {
state.isRestoringWallet = false
userDefaults.remove(Constants.udIsRestoringWallet)
state.walletStatus = .none
walletStorage.nukeWallet()
try? readTransactionsStorage.nukeWallet()
walletStorage.resetZashi()
flexaHandler.signOut()
try? readTransactionsStorage.resetZashi()
if state.appInitializationState == .keysMissing && state.onboardingState.destination == .importExistingWallet {
state.appInitializationState = .uninitialized
@ -418,7 +417,7 @@ extension Root {
)
}
case .nukeWalletFailed:
case .resetZashiFailed:
let backDestination: Effect<Root.Action>
if let previousDestination = state.destinationState.previousDestination {
backDestination = Effect.send(.destination(.updateDestination(previousDestination)))

View File

@ -131,8 +131,8 @@ public struct Root {
case tabs(Tabs.Action)
case initialization(InitializationAction)
case notEnoughFreeSpace(NotEnoughFreeSpace.Action)
case nukeWalletFailed
case nukeWalletSucceeded
case resetZashiFailed
case resetZashiSucceeded
case onboarding(OnboardingFlow.Action)
case phraseDisplay(RecoveryPhraseDisplay.Action)
case splashFinished
@ -402,7 +402,7 @@ extension AlertState where Action == Root.Action {
AlertState {
TextState(L10n.Root.Initialization.Alert.Wipe.title)
} actions: {
ButtonState(role: .destructive, action: .initialization(.nukeWallet)) {
ButtonState(role: .destructive, action: .initialization(.resetZashi)) {
TextState(L10n.General.yes)
}
ButtonState(role: .cancel, action: .alert(.dismiss)) {
@ -428,7 +428,7 @@ extension AlertState where Action == Root.Action {
ButtonState(role: .cancel, action: .alert(.dismiss)) {
TextState(L10n.Root.SeedPhrase.DifferentSeed.tryAgain)
}
ButtonState(role: .destructive, action: .initialization(.nukeWallet)) {
ButtonState(role: .destructive, action: .initialization(.resetZashi)) {
TextState(L10n.General.Alert.continue)
}
} message: {
@ -443,7 +443,7 @@ extension AlertState where Action == Root.Action {
ButtonState(role: .cancel, action: .initialization(.restoreExistingWallet)) {
TextState(L10n.Root.ExistingWallet.restore)
}
ButtonState(role: .destructive, action: .initialization(.nukeWallet)) {
ButtonState(role: .destructive, action: .initialization(.resetZashi)) {
TextState(L10n.General.Alert.continue)
}
} message: {

View File

@ -279,7 +279,7 @@ private extension RootView {
}
Button(L10n.Root.Debug.Option.nukeWallet) {
store.send(.initialization(.nukeWalletRequest))
store.send(.initialization(.resetZashiRequest))
}
}
}

View File

@ -2039,7 +2039,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "secant-distrib.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 2;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
@ -2052,7 +2052,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.3;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-mainnet";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -2080,7 +2080,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "secant-distrib.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;
@ -2092,7 +2092,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.3;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-mainnet";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -2281,7 +2281,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "secant-distrib.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;
@ -2293,7 +2293,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.3;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-mainnet";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -2334,7 +2334,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.4.3;
MARKETING_VERSION = 0.4.4;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-testnet";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -2364,7 +2364,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.4.3;
MARKETING_VERSION = 0.4.4;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-testnet";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -2394,7 +2394,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.4.3;
MARKETING_VERSION = 0.4.4;
OTHER_SWIFT_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = "co.electriccoin.secant-testnet";
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@ -86,8 +86,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/flexa/flexa-ios.git",
"state" : {
"revision" : "61c916b2267178fad0e4356014e0b9d695db5261",
"version" : "1.0.4"
"revision" : "aae0c067058946bbeb18be71593e38017c1c5af4",
"version" : "1.0.5"
}
},
{

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -22,7 +22,7 @@ final class WalletNukeTests: XCTestCase {
Root()
}
await store.send(.initialization(.nukeWalletRequest)) { state in
await store.send(.initialization(.resetZashiRequest)) { state in
state.alert = AlertState.wipeRequest()
}
@ -40,9 +40,9 @@ final class WalletNukeTests: XCTestCase {
store.dependencies.sdkSynchronizer.wipe = { nil }
store.dependencies.readTransactionsStorage = .noOp
await store.send(.initialization(.nukeWallet))
await store.send(.initialization(.resetZashi))
await store.receive(.nukeWalletFailed) { state in
await store.receive(.resetZashiFailed) { state in
state.alert = AlertState.wipeFailed()
}
@ -65,17 +65,17 @@ final class WalletNukeTests: XCTestCase {
store.dependencies.readTransactionsStorage = .noOp
store.dependencies.readTransactionsStorage.readIds = { readIds }
store.dependencies.readTransactionsStorage.nukeWallet = { readIds.removeAll() }
store.dependencies.readTransactionsStorage.resetZashi = { readIds.removeAll() }
store.dependencies.walletStorage = .noOp
store.dependencies.walletStorage.areKeysPresent = { areKeysPresent }
store.dependencies.walletStorage.nukeWallet = { areKeysPresent = false }
store.dependencies.walletStorage.resetZashi = { areKeysPresent = false }
store.dependencies.mainQueue = .immediate
store.dependencies.databaseFiles = .noOp
XCTAssertEqual(readIds, ["id1".redacted: true])
XCTAssertTrue(areKeysPresent)
await store.send(.nukeWalletSucceeded) { state in
await store.send(.resetZashiSucceeded) { state in
var stateAfterWipe = Root.State.initial
stateAfterWipe.splashAppeared = true

View File

@ -50,7 +50,7 @@ class SettingsTests: XCTestCase {
markUserPassedPhraseBackupTest: { _ in
throw WalletStorage.KeychainError.encoding
},
nukeWallet: { }
resetZashi: { }
)
let store = TestStore(

View File

@ -119,7 +119,7 @@ class WalletStorageTests: XCTestCase {
XCTFail("`testDeleteWallet` storing `walletData` failed.")
}
storage.nukeWallet()
storage.resetZashi()
let data = data(forKey: WalletStorage.Constants.zcashStoredWallet)