[#826] proper handling of apps lifecycle (#827)

- SDK version updated
- handling of stopped state added
- swiftgen plugin removed until it's supported by xcode 15 again
This commit is contained in:
Lukas Korba 2023-09-29 10:23:17 +02:00 committed by GitHub
parent 9586bcbb60
commit bbd8da45d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 131 additions and 48 deletions

View File

@ -60,9 +60,8 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.59.0"),
.package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.14.1"),
.package(url: "https://github.com/pointfreeco/swift-url-routing", from: "0.5.0"),
.package(url: "https://github.com/SwiftGen/SwiftGenPlugin", from: "6.6.0"),
.package(url: "https://github.com/zcash-hackworks/MnemonicSwift", from: "2.2.4"),
.package(url: "https://github.com/zcash/ZcashLightClientKit", from: "2.0.0-rc.4"),
.package(url: "https://github.com/zcash/ZcashLightClientKit", from: "2.0.0"),
.package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.11.0")
],
targets: [
@ -193,10 +192,7 @@ let package = Package(
),
.target(
name: "Generated",
resources: [.process("Resources")],
plugins: [
.plugin(name: "SwiftGenPlugin", package: "SwiftGenPlugin")
]
resources: [.process("Resources")]
),
.target(
name: "Home",

View File

@ -33,7 +33,7 @@ public struct SDKSynchronizerClient {
public let getShieldedBalance: () -> WalletBalance?
public let getTransparentBalance: () -> WalletBalance?
public let getAllTransactions: () async throws -> [WalletEvent]
public var getAllTransactions: () async throws -> [WalletEvent]
public let getUnifiedAddress: (_ account: Int) async throws -> UnifiedAddress?
public let getTransparentAddress: (_ account: Int) async throws -> TransparentAddress?

View File

@ -27,6 +27,8 @@ extension RootReducer {
case nukeWallet
case nukeWalletRequest
case respondToWalletInitializationState(InitializationState)
case synchronizerStartFailed(ZcashError)
case retryStart
case walletConfigChanged(WalletConfig)
}
@ -34,10 +36,32 @@ extension RootReducer {
public func initializationReduce() -> Reduce<RootReducer.State, RootReducer.Action> {
Reduce { state, action in
switch action {
case .initialization(.appDelegate(.didEnterBackground)):
sdkSynchronizer.stop()
return .none
case .initialization(.appDelegate(.willEnterForeground)):
return EffectTask(value: .initialization(.retryStart))
.delay(for: 1.0, scheduler: mainQueue)
.eraseToEffect()
case .initialization(.synchronizerStartFailed(let zcashError)):
state.alert = AlertState.retryStartFailed(zcashError)
return .none
case .initialization(.retryStart):
return .run { send in
do {
try await sdkSynchronizer.start(true)
} catch {
await send(.initialization(.synchronizerStartFailed(error.toZcashError())))
}
}
case .initialization(.appDelegate(.didFinishLaunching)):
// TODO: [#704], trigger the review request logic when approved by the team,
// https://github.com/zcash/secant-ios-wallet/issues/704
return EffectTask(value: .initialization(.checkWalletConfig))
return EffectTask(value: .initialization(.initialSetups))
.delay(for: 0.02, scheduler: mainQueue)
.eraseToEffect()

View File

@ -288,6 +288,18 @@ extension AlertState where Action == RootReducer.Action {
TextState(L10n.Root.Initialization.Alert.Wipe.message)
}
}
public static func retryStartFailed(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.RetryStartFailed.title)
} actions: {
ButtonState(action: .initialization(.retryStart)) {
TextState(L10n.Home.SyncFailed.retry)
}
} message: {
TextState(L10n.Root.Initialization.Alert.RetryStartFailed.message)
}
}
}
extension ConfirmationDialogState where Action == RootReducer.Action {

View File

@ -71,11 +71,16 @@ public struct WalletEventsFlowReducer: ReducerProtocol {
switch action {
case .onAppear:
state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations
return sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map { WalletEventsFlowReducer.Action.synchronizerStateChanged($0.syncStatus) }
.eraseToEffect()
.cancellable(id: CancelId.timer, cancelInFlight: true)
return .merge(
sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map { WalletEventsFlowReducer.Action.synchronizerStateChanged($0.syncStatus) }
.eraseToEffect()
.cancellable(id: CancelId.timer, cancelInFlight: true),
.run { send in
await send(.updateWalletEvents(try await sdkSynchronizer.getAllTransactions()))
}
)
case .onDisappear:
return .cancel(id: CancelId.timer)

View File

@ -410,6 +410,12 @@ public enum L10n {
/// Wallet initialisation failed.
public static let title = L10n.tr("Localizable", "root.initialization.alert.failed.title", fallback: "Wallet initialisation failed.")
}
public enum RetryStartFailed {
/// The app was in background so re-start of the synchronizer is needed but this operation failed.
public static let message = L10n.tr("Localizable", "root.initialization.alert.retryStartFailed.message", fallback: "The app was in background so re-start of the synchronizer is needed but this operation failed.")
/// Synchronizer failed to start
public static let title = L10n.tr("Localizable", "root.initialization.alert.retryStartFailed.title", fallback: "Synchronizer failed to start")
}
public enum SdkInitFailed {
/// Failed to initialize the SDK
public static let title = L10n.tr("Localizable", "root.initialization.alert.sdkInitFailed.title", fallback: "Failed to initialize the SDK")
@ -573,6 +579,8 @@ public enum L10n {
public static func error(_ p1: Any) -> String {
return L10n.tr("Localizable", "sync.message.error", String(describing: p1), fallback: "Error: %@")
}
/// Stopped
public static let stopped = L10n.tr("Localizable", "sync.message.stopped", fallback: "Stopped")
/// %@%% Synced
public static func sync(_ p1: Any) -> String {
return L10n.tr("Localizable", "sync.message.sync", String(describing: p1), fallback: "%@%% Synced")

View File

@ -142,6 +142,7 @@
"sync.message.uptodate" = "Up-To-Date";
"sync.message.unprepared" = "Unprepared";
"sync.message.error" = "Error: %@";
"sync.message.stopped" = "Stopped";
"sync.message.sync" = "%@%% Synced";
// MARK: - Transactions
@ -231,6 +232,8 @@
"root.initialization.alert.wipe.title" = "Wipe of the wallet";
"root.initialization.alert.wipe.message" = "Are you sure?";
"root.initialization.alert.wipeFailed.title" = "Nuke of the wallet failed";
"root.initialization.alert.retryStartFailed.title" = "Synchronizer failed to start";
"root.initialization.alert.retryStartFailed.message" = "The app was in background so re-start of the synchronizer is needed but this operation failed.";
"root.destination.alert.failedToProcessDeeplink.title" = "Failed to process deeplink.";
"root.destination.alert.failedToProcessDeeplink.message" = "Deeplink: \(%@))\nError: \(%@) (code: %@)";

View File

@ -9,4 +9,6 @@ import Foundation
public enum AppDelegateAction: Equatable {
case didFinishLaunching
case didEnterBackground
case willEnterForeground
}

View File

@ -29,6 +29,9 @@ public struct SyncStatusSnapshot: Equatable {
case .error(let error):
return SyncStatusSnapshot(state, L10n.Sync.Message.error(error.toZcashError().message))
case .stopped:
return SyncStatusSnapshot(state, L10n.Sync.Message.stopped)
case .syncing(let progress):
return SyncStatusSnapshot(state, L10n.Sync.Message.sync(String(format: "%0.1f", progress * 100)))
}

View File

@ -612,6 +612,7 @@
buildConfigurationList = 0D26AF91299E8196005260EE /* Build configuration list for PBXNativeTarget "secant-mainnet" */;
buildPhases = (
0D26AE97299E8196005260EE /* Generate GoogleService-Info.plist */,
34968BEE2ABDC6FA0027F495 /* Swiftgen */,
0D26AE99299E8196005260EE /* SwiftLint */,
0D26AE9A299E8196005260EE /* Sources */,
0D26AF70299E8196005260EE /* Frameworks */,
@ -639,6 +640,7 @@
buildConfigurationList = 0D4E7A2A26B364180058B01E /* Build configuration list for PBXNativeTarget "secant-testnet" */;
buildPhases = (
0D3B01ED298DB0FE007EBCDA /* Generate GoogleService-Info.plist */,
34968BED2ABDC6310027F495 /* Swiftgen */,
6696BA8726F0B1D200D5C875 /* SwiftLint */,
0D4E7A0126B364170058B01E /* Sources */,
0D4E7A0226B364170058B01E /* Frameworks */,
@ -849,6 +851,44 @@
shellPath = /bin/zsh;
shellScript = "# this creates an empty file for the firebase SDK\n\necho \"Creating an empty file for the firebase SDK\"\n\nCRASH_REPORTER_FILE=\"./secant/Resources/GoogleService-Info.plist\"\nif [[ -f $CRASH_REPORTER_FILE ]]; then\n echo \"$CRASH_REPORTER_FILE Exists. Not doing anything.\"\nelse \n echo \"$CRASH_REPORTER_FILE does not exist. Will insert a DUMMY FILE\"\n\n echo \"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KICAgIDxrZXk+SVNfRFVNTVlfRklMRTwva2V5PgogICAgPHRydWU+PC90cnVlPgo8L2RpY3Q+CjwvcGxpc3Q+Cg==\" | base64 --decode > $CRASH_REPORTER_FILE\nfi\n";
};
34968BED2ABDC6310027F495 /* Swiftgen */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftgen;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "swiftgen_version=\"SwiftGen v6.6.2 (Stencil v0.15.1, StencilSwiftKit v2.10.1, SwiftGenKit v6.6.2)\"\n\nif which swiftgen >/dev/null; then\n if [[ $(swiftgen --version) != $swiftgen_version ]]; then\n echo \"warning: Compatible SwiftGen version not installed, download version $swiftgen_version. Currently installed version is $(swiftgen --version)\"\n fi\n \n echo \"Running swiftgen\" \n swiftgen config run --config modules/swiftgen.yml\nelse\n echo \"warning: swiftgen not installed\"\nfi\n";
};
34968BEE2ABDC6FA0027F495 /* Swiftgen */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftgen;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "swiftgen_version=\"SwiftGen v6.6.2 (Stencil v0.15.1, StencilSwiftKit v2.10.1, SwiftGenKit v6.6.2)\"\n\nif which swiftgen >/dev/null; then\n if [[ $(swiftgen --version) != $swiftgen_version ]]; then\n echo \"warning: Compatible SwiftGen version not installed, download version $swiftgen_version. Currently installed version is $(swiftgen --version)\"\n fi\n \n echo \"Running swiftgen\" \n swiftgen config run --config modules/swiftgen.yml\nelse\n echo \"warning: swiftgen not installed\"\nfi\n";
};
6696BA8726F0B1D200D5C875 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;

View File

@ -248,8 +248,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "0e0d0aab665ff1a0659ce75ac003081f2b1c8997",
"version" : "1.19.0"
"revision" : "fb70a0f5e984f23be48b11b4f1909f3bee016178",
"version" : "1.19.1"
}
},
{
@ -306,15 +306,6 @@
"version" : "0.5.0"
}
},
{
"identity" : "swiftgenplugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SwiftGen/SwiftGenPlugin",
"state" : {
"revision" : "879b85a470cacd70c19e22eb7e11a3aed66f4068",
"version" : "6.6.2"
}
},
{
"identity" : "swiftui-navigation",
"kind" : "remoteSourceControl",
@ -338,8 +329,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "8eebc40ac69598c3c02344cf547e1005d5963b31",
"version" : "0.4.0-rc.4"
"revision" : "9bc5877ef6302e877922f79ebead52e50bce94fd",
"version" : "0.4.0"
}
},
{
@ -347,8 +338,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash/ZcashLightClientKit",
"state" : {
"revision" : "d8b069fd15bb4d9d9e4ca975af035c881c55b8c6",
"version" : "2.0.0-rc.4"
"revision" : "61aba39dc902c1676c6c5829ee16227c3fc3ea36",
"version" : "2.0.0"
}
}
],

View File

@ -48,6 +48,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
@main
struct SecantApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@Environment (\.scenePhase) private var scenePhase
init() {
FontFamily.registerAllCustomFonts()
@ -63,6 +64,13 @@ struct SecantApp: App {
.font(
.custom(FontFamily.Inter.regular.name, size: 17)
)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
appDelegate.rootViewStore.send(.initialization(.appDelegate(.willEnterForeground)))
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
appDelegate.rootViewStore.send(.initialization(.appDelegate(.didEnterBackground)))
}
.preferredColorScheme(.light)
}
}
}

View File

@ -211,10 +211,6 @@ class RecoveryPhraseValidationFlowFeatureFlagTests: XCTestCase {
await testQueue.advance(by: 0.02)
await store.receive(.initialization(.checkWalletConfig))
await store.receive(.walletConfigLoaded(WalletConfig.default))
await store.receive(.initialization(.initialSetups))
await testQueue.advance(by: 0.02)

View File

@ -117,10 +117,6 @@ class AppInitializationTests: XCTestCase {
await testQueue.advance(by: 0.02)
await store.receive(.initialization(.checkWalletConfig))
await store.receive(.walletConfigLoaded(WalletConfig.default))
await store.receive(.initialization(.initialSetups))
await testQueue.advance(by: 0.02)
@ -166,10 +162,6 @@ class AppInitializationTests: XCTestCase {
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
await store.receive(.initialization(.checkWalletConfig))
await store.receive(.walletConfigLoaded(WalletConfig.default))
await store.receive(.initialization(.initialSetups))
await store.receive(.initialization(.configureCrashReporter))
@ -200,10 +192,6 @@ class AppInitializationTests: XCTestCase {
// Root of the test, the app finished the launch process and triggers the checks and initializations.
await store.send(.initialization(.appDelegate(.didFinishLaunching)))
await store.receive(.initialization(.checkWalletConfig))
await store.receive(.walletConfigLoaded(WalletConfig.default))
await store.receive(.initialization(.initialSetups))
await store.receive(.initialization(.configureCrashReporter))

View File

@ -190,6 +190,8 @@ class RootTests: XCTestCase {
reducer: RootReducer(tokenName: "ZEC", zcashNetwork: ZcashNetworkBuilder.network(for: .testnet))
)
store.dependencies.sdkSynchronizer = .noOp
let address = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po".redacted
store.send(.home(.walletEvents(.replyTo(address))))

View File

@ -14,7 +14,7 @@ import WalletEventsFlow
@testable import secant_testnet
class WalletEventsTests: XCTestCase {
func testSynchronizerSubscription() throws {
@MainActor func testSynchronizerSubscription() async throws {
let store = TestStore(
initialState: WalletEventsFlowReducer.State(
destination: .latest,
@ -25,16 +25,21 @@ class WalletEventsTests: XCTestCase {
)
store.dependencies.sdkSynchronizer = .mocked()
store.dependencies.sdkSynchronizer.getAllTransactions = { [] }
store.dependencies.mainQueue = .immediate
store.send(.onAppear) { state in
await store.send(.onAppear) { state in
state.requiredTransactionConfirmations = 10
}
store.receive(.synchronizerStateChanged(.unprepared))
await store.receive(.synchronizerStateChanged(.unprepared))
await store.receive(.updateWalletEvents([]))
// ending the subscription
store.send(.onDisappear)
await store.send(.onDisappear)
await store.finish()
}
@MainActor func testSynchronizerStateChanged2Synced() async throws {