Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
51b8faf
new dev cert
Sajjon Apr 20, 2026
750e317
try to stabelize iOS sim selectiion
Sajjon Apr 21, 2026
8bf2dd7
docs: thoroughly document ChooseWallet + CreateNewWallet flow
Sajjon Apr 26, 2026
39c5e53
docs(Models): document every type, member, and nontrivial body
Sajjon Apr 26, 2026
43d83f3
docs(Controller): document scene-controller glue + bar-button plumbing
Sajjon Apr 26, 2026
fda508b
docs(UseCases): top up Default*UseCase docs (init, @Injected, methods)
Sajjon Apr 26, 2026
a306cb1
docs(Extensions/Combine,Foundation): document reactive plumbing + hel…
Sajjon Apr 26, 2026
0e6df5e
docs(Extensions/UIKit): document UIKit helpers + styling system
Sajjon Apr 26, 2026
4d34617
docs(Views): document protocols, scene-views, components, table system
Sajjon Apr 26, 2026
dadada1
docs(Application): top up partial DI/ViewModel/OutputFormatting docs
Sajjon Apr 26, 2026
29f7668
docs(Persistence): document KV-store layering, Keychain reinstall trick
Sajjon Apr 26, 2026
8b50fe7
docs(Utils): document appearance/bootstrap/QR/reflection helpers
Sajjon Apr 26, 2026
7411e53
docs(Navigation): document coordinator/navigator/deep-link plumbing
Sajjon Apr 26, 2026
99b441d
docs(InputValidators): document validator pipeline + per-field valida…
Sajjon Apr 26, 2026
d7bc371
docs(Onboarding): document Welcome + Terms scenes + OnboardingCoordin…
Sajjon Apr 26, 2026
32d092b
docs(Onboarding): document CrashReporting + ECC-warning scenes
Sajjon Apr 26, 2026
127f29b
docs(ChooseWallet): document chooser, privacy gate, create-flow coord…
Sajjon Apr 26, 2026
9139463
docs(BackupWallet): document backup hub + keystore/keypair reveal flows
Sajjon Apr 26, 2026
fe425b7
docs(RestoreWallet): document restore coordinator + segmented restore…
Sajjon Apr 26, 2026
1d5bf8e
docs(SetPincode): document pincode chooser + confirm flow
Sajjon Apr 26, 2026
52fb830
docs(Main+Unlock): document main coordinator, app-lock cover, unlock …
Sajjon Apr 26, 2026
c668282
docs(Main+Send): document hub + four-step Send flow
Sajjon Apr 26, 2026
ff6378d
docs(Receive+Settings): document Receive screen + Settings hub + sub-…
Sajjon Apr 26, 2026
5b7c685
fix(security/safety): phase 1 — quick wins from code review
Sajjon Apr 26, 2026
8bfce17
fix(security/safety): phase 2 — reinstall wipe + unlock single-fire
Sajjon Apr 26, 2026
dc82a06
fix(safety): phase 3 — graceful pop on missing wallet (no fatal traps)
Sajjon Apr 26, 2026
20b3e71
fix(security): pasteboard expiration on sensitive copies
Sajjon Apr 26, 2026
15129a8
perf(wallet): cache via CurrentValueSubject; one Keychain read per se…
Sajjon Apr 26, 2026
ad2d581
fix(send): gate Review button on real network nonce
Sajjon Apr 26, 2026
69ee99d
fix(send): pre-verify password against keystore before enabling Sign
Sajjon Apr 26, 2026
30c1f78
fix(wallet-create): persist immediately, gate on backup-confirmed flag
Sajjon Apr 26, 2026
4bda366
fix(safety): convert [unowned] → [weak] in core navigation infra
Sajjon Apr 26, 2026
f1674ca
fix(safety): convert [unowned] → [weak] in DefaultTransactionsUseCase
Sajjon Apr 26, 2026
863f056
fix(safety): convert [unowned] → [weak] in 0_Onboarding scenes
Sajjon Apr 26, 2026
b85810b
fix(safety): convert [unowned] → [weak] in 1_Main scenes
Sajjon Apr 26, 2026
4125500
fix(review): address Copilot PR comments
Sajjon Apr 26, 2026
5c650eb
cleanup: remove superfluous .eraseToAnyPublisher() calls
Sajjon Apr 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
docs(Main+Send): document hub + four-step Send flow
- Main hub (View/VM): documents the three balance-fetch triggers, the
  cached-balance-prepend trick (no zero flash on launch), and the
  pre-warming of the gas-price cache for Send.
- SendCoordinator: linear four-step flow with the QR/deeplink intent
  merge into a single transactionIntent stream, plus topmost-scene gating
  to avoid stale prefills.
- Step 1 PrepareTransaction (View/VM): documents the four parallel
  concerns (balance fetch, per-field validation, sufficient-funds check,
  prefill). Switches the type_body_length disable to a same-line :this
  directive so it doesn't conflict with the docstring.
- Step 1.1 ScanQRCode (View/VM): zilliqa:// prefix stripping + Combine
  bridge from the third-party reader.
- Step 2 ReviewTransactionBeforeSigning (View/VM): read-only summary +
  formatted amount/fee/total + accept-checkbox gating.
- Step 3 SignTransaction (View/VM): wallet-keystore signing pipeline,
  errorTracker for wrong-password, flatMapLatest cancellation.
- Step 4 PollTransactionStatus (View/VM): polling with linear backoff,
  haptic + sound on receipt, four exit paths (skip/dismiss/view/timeout).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  • Loading branch information
Sajjon and claude committed Apr 26, 2026
commit c66828222ec68fbdb39dd428311c49db3ac2a61f
3 changes: 3 additions & 0 deletions Sources/Scenes/1_Main/2_Main/0_Main/Main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
import Foundation
import UIKit

/// `SceneController` glue for the wallet hub screen.
final class Main: Scene<MainView> {}

/// Right-bar settings cog icon — taps fire `goToSettings` on the view-model.
extension Main: RightBarButtonContentMaking {
static let makeRightContent = BarButtonContent(image: UIImage(resource: .settings))
}

/// Translucent navigation bar so the parallax aurora background bleeds under it.
extension Main: NavigationBarLayoutOwner {
var navigationBarLayout: NavigationBarLayout {
.translucent
Expand Down
14 changes: 14 additions & 0 deletions Sources/Scenes/1_Main/2_Main/0_Main/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@
import Combine
import UIKit

/// Wallet hub screen — shows balance with pull-to-refresh + send/receive CTAs.
/// `PullToRefreshCapable` brings in the refresh control + its publishers.
final class MainView: ScrollableStackViewOwner, PullToRefreshCapable {
/// Container hosting the parallax aurora background.
private lazy var motionEffectAuroraImageView = UIView()
/// "Balance" label above the value.
private lazy var balanceTitleLabel = UILabel()
/// The big balance number — uses the impression font.
private lazy var balanceValueLabel = UILabel()
/// Zilliqa logo aligned bottom-left of the balance number.
private lazy var zilliqaBalanceImageView = UIImageView()
private lazy var zilliqaImageVerticalPositioner = UIStackView(arrangedSubviews: [
.spacer,
Expand Down Expand Up @@ -59,6 +65,7 @@ final class MainView: ScrollableStackViewOwner, PullToRefreshCapable {
extension MainView: ViewModelled {
typealias ViewModel = MainViewModel

/// Surfaces pull-to-refresh + the two CTA taps.
var inputFromView: InputFromView {
InputFromView(
pullToRefreshTrigger: pullToRefreshTriggerPublisher,
Expand All @@ -67,6 +74,8 @@ extension MainView: ViewModelled {
)
}

/// Binds fetching state → refresh-spinner, balance string → big number,
/// and last-updated copy → the refresh control's title.
func populate(with viewModel: MainViewModel.Output) -> [AnyCancellable] {
[
viewModel.isFetchingBalance --> isRefreshingBinder,
Expand All @@ -77,6 +86,9 @@ extension MainView: ViewModelled {
}

private extension MainView {
/// Styling pass — labels, logo image, two image-above-label buttons,
/// and the parallax aurora background inserted *behind* the scrollView
/// so it stays put while content scrolls.
func setupSubviews() {
balanceTitleLabel.withStyle(.init(
text: String(localized: .Main.balanceTitle),
Expand Down Expand Up @@ -117,6 +129,8 @@ private extension MainView {
}
}

/// Shared helper that wires the three-layer aurora parallax into `effectView`.
/// Used by both `MainView` and `LockAppScene` so the visual effect is identical.
func addAuroraImagesWithMotionEffect(to effectView: UIView) {
effectView.backgroundColor = .clear
effectView.translatesAutoresizingMaskIntoConstraints = false
Expand Down
33 changes: 33 additions & 0 deletions Sources/Scenes/1_Main/2_Main/0_Main/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,44 @@ import Zesame

// MARK: - MainUserAction

/// Outcomes of the wallet hub.
enum MainUserAction {
/// User tapped the Send CTA.
case send
/// User tapped the Receive CTA.
case receive
/// User tapped the right-bar settings cog.
case goToSettings
}

// MARK: - MainViewModel

/// View model for the wallet hub. Manages three independent triggers that
/// fire a balance refetch (initial load, pull-to-refresh, post-send refresh)
/// and surfaces formatted balance + freshness label to the view.
final class MainViewModel: BaseViewModel<
MainUserAction,
MainViewModel.InputFromView,
MainViewModel.Output
> {
/// Network + cache façade for balance/gas-price calls.
@Injected(\.transactionsUseCase) private var transactionUseCase: TransactionsUseCase
/// Wallet source — resolves to the persisted wallet.
@Injected(\.walletStorageUseCase) private var walletStorageUseCase: WalletStorageUseCase

/// External pulse that asks for a refetch — e.g. the post-send hook from
/// `MainCoordinator.triggerFetchingOfBalance()`.
private let updateBalanceTrigger: AnyPublisher<Void, Never>

// MARK: - Initialization

/// Captures the external balance-refresh trigger.
init(updateBalanceTrigger: AnyPublisher<Void, Never>) {
self.updateBalanceTrigger = updateBalanceTrigger
}

/// Composes the three refetch triggers, runs the balance use case,
/// caches the result, and surfaces formatted balance + freshness.
override func transform(input: Input) -> Output {
func userIntends(to intention: NavigationStep) {
navigator.next(intention)
Expand All @@ -61,8 +75,15 @@ final class MainViewModel: BaseViewModel<

let activityIndicator = ActivityIndicator()

// Three triggers fan into a single fetch stream:
// - external (post-send) → updateBalanceTrigger
// - user-initiated → pullToRefreshTrigger
// - initial load → wallet emission
let fetchTrigger = Publishers.Merge3(updateBalanceTrigger, input.fromView.pullToRefreshTrigger, wallet.mapToVoid()).eraseToAnyPublisher()

// Run the balance call. flatMapLatest cancels in-flight requests when
// a new trigger fires (e.g. user pulls again before the first fetch returns).
// handleEvents caches the balance for the next launch's pre-fetch UI.
let latestBalanceAndNonce: AnyPublisher<BalanceResponse, Never> = fetchTrigger.withLatestFrom(wallet)
.flatMapLatest { [unowned self] in
self.transactionUseCase
Expand All @@ -73,11 +94,14 @@ final class MainViewModel: BaseViewModel<
}
.eraseToAnyPublisher()

// Sample balanceUpdatedAt at fetch time for the "Updated N min ago" label.
let balanceWasUpdatedAt = fetchTrigger.map { [unowned self] in
self.transactionUseCase.balanceUpdatedAt
}

// Format output
// Show the cached balance immediately on first launch so the user
// doesn't see a "0 ZIL" flash before the network call resolves.
let _cachedBalance: Amount = transactionUseCase.cachedBalance ?? 0
let latestBalanceOrZero = latestBalanceAndNonce.map(\.balance).prepend(_cachedBalance)

Expand All @@ -91,6 +115,7 @@ final class MainViewModel: BaseViewModel<
input.fromView.receiveTrigger
.sink { userIntends(to: .receive) },

// Pre-warm the gas-price cache so the Send screen has a value ready.
transactionUseCase.getMinimumGasPrice().sink(receiveCompletion: { _ in }, receiveValue: { _ in }),
].forEach { $0.store(in: &cancellables) }

Expand All @@ -109,15 +134,23 @@ final class MainViewModel: BaseViewModel<
}

extension MainViewModel {
/// User-event publishers the view-model consumes.
struct InputFromView {
/// Fires when the user pulls to refresh.
let pullToRefreshTrigger: AnyPublisher<Void, Never>
/// Fires when the user taps Send.
let sendTrigger: AnyPublisher<Void, Never>
/// Fires when the user taps Receive.
let receiveTrigger: AnyPublisher<Void, Never>
}

/// Reactive bindings the view installs.
struct Output {
/// Drives the pull-to-refresh spinner.
let isFetchingBalance: AnyPublisher<Bool, Never>
/// Pre-formatted balance string (ZIL, with thousands separator).
let balance: AnyPublisher<String, Never>
/// Localized "Updated N min ago" string for the refresh control.
let refreshControlLastUpdatedTitle: AnyPublisher<String, Never>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@

import UIKit

/// `SceneController` glue for the prepare-transaction screen (step 1 of Send).
final class PrepareTransaction: Scene<PrepareTransactionView> {}

/// Localized title; back arrow hidden because cancel is the only "back" path.
extension PrepareTransaction: BackButtonHiding {
static let title = String(localized: .PrepareTransaction.title)
}

/// Right "Cancel" button — aborts the entire Send flow.
extension PrepareTransaction: RightBarButtonMaking {
static let makeRight: BarButton = .cancel
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import Factory
import UIKit
import Zesame

/// Step 1 of Send — recipient/amount/gas entry with QR-scan + max-amount conveniences,
/// plus pull-to-refresh on balance. The view-model handles validation, max-amount
/// calculation (balance - gas), and QR-scan pre-fill routing.
final class PrepareTransactionView: ScrollableStackViewOwner, PullToRefreshCapable {
private lazy var balanceTitleLabel = UILabel()
private lazy var balanceValueLabel = UILabel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,37 @@ import Zesame

// MARK: - PrepareTransactionUserAction

/// Outcomes of step 1 of Send.
enum PrepareTransactionUserAction {
/// User tapped the right-bar Cancel.
case cancel
/// User submitted a fully-validated payment for review (advances to step 2).
case reviewPayment(Payment)
/// User tapped the QR-scan icon — coordinator presents the scanner modal.
case scanQRCode
}

// MARK: - PrepareTransactionViewModel

// swiftlint:disable:next type_body_length
final class PrepareTransactionViewModel: BaseViewModel<
/// View model for step 1 of Send. Handles four parallel concerns:
/// - balance fetch (with pull-to-refresh + cached-balance fallback);
/// - real-time per-field validation (recipient, amount, gas limit, gas price);
/// - cross-field "sufficient funds" check;
/// - pre-fill from inbound `TransactionIntent` (deep-link or QR scan).
final class PrepareTransactionViewModel: BaseViewModel< // swiftlint:disable:this type_body_length
PrepareTransactionUserAction,
PrepareTransactionViewModel.InputFromView,
PrepareTransactionViewModel.Output
> {
/// Network façade for balance + gas-price fetches.
@Injected(\.transactionsUseCase) private var transactionUseCase: TransactionsUseCase
/// Wallet source for the recipient/amount/balance pipeline.
@Injected(\.walletStorageUseCase) private var walletStorageUseCase: WalletStorageUseCase

/// Pre-fill source — merged stream of QR-scanned and deep-linked intents from the coordinator.
private let scannedOrDeeplinkedTransaction: AnyPublisher<TransactionIntent, Never>

/// Captures the pre-fill source.
init(scannedOrDeeplinkedTransaction: AnyPublisher<TransactionIntent, Never>) {
self.scannedOrDeeplinkedTransaction = scannedOrDeeplinkedTransaction
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@

import UIKit

/// `SceneController` glue for the QR-code scanner side-trip from PrepareTransaction.
final class ScanQRCode: Scene<ScanQRCodeView> {}

/// Localized navigation title.
extension ScanQRCode {
static let title = String(localized: .ScanQRCode.title)
}

/// Left "Cancel" bar-button — closes the scanner without picking a transaction.
extension ScanQRCode: LeftBarButtonMaking {
static let makeLeft: BarButton = .cancel
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ import Combine
import QRCodeReader
import UIKit

/// QR-code scanner view backed by the third-party `QRCodeReader` library.
/// Captures from the back camera and emits scanned strings through the view-model.
final class ScanQRCodeView: UIView {
/// Bridges the third-party reader's `didFindCode` callback into a Combine publisher.
private let scannedQrCodeSubject = PassthroughSubject<String?, Never>()
/// Library-provided viewfinder view.
private lazy var readerView = QRCodeReaderView()
/// Underlying QR-code reader (back camera, QR metadata only).
private lazy var reader = QRCodeReader(metadataObjectTypes: [.qr], captureDevicePosition: .back)

init() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,35 @@ import Foundation

// MARK: - User action and navigation steps

/// Outcomes of the QR-scan modal.
enum ScanQRCodeUserAction {
case /* user did */ cancel, scanQRContainingTransaction(TransactionIntent)
/// User tapped Cancel.
case cancel
/// QR code scanned and successfully decoded into a `TransactionIntent`.
case scanQRContainingTransaction(TransactionIntent)
}

// MARK: - ScanQRCodeViewModel

/// View model for the QR scanner. Decodes scanned strings into a
/// `TransactionIntent` and routes them upstream.
///
/// Accepts both raw JSON-payload QRs and ones prefixed with `zilliqa://`
/// (the QR scheme other Zilliqa wallets emit).
final class ScanQRCodeViewModel: BaseViewModel<
ScanQRCodeUserAction,
ScanQRCodeViewModel.InputFromView,
ScanQRCodeViewModel.Output
> {
/// Result type for the scan→decode pipeline.
typealias ScannedQRResult = Result<TransactionIntent, Swift.Error>

/// Currently unused side-channel for "start scanning" pulses (kept for
/// future use if the reader needs an explicit start trigger).
private let startScanningSubject = CurrentValueSubject<Void, Never>(())

/// Decodes scanned strings, strips an optional `zilliqa://` prefix, and
/// surfaces the resulting `TransactionIntent` (or cancel on bar-button tap).
override func transform(input: Input) -> Output {
func userDid(_ userAction: NavigationStep) {
navigator.next(userAction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@

import Foundation

/// `SceneController` glue for the review-transaction screen (step 2 of Send).
/// Shows the prepared payment for the user to confirm before signing.
final class ReviewTransactionBeforeSigning: Scene<ReviewTransactionBeforeSigningView> {}

/// Localized navigation title.
extension ReviewTransactionBeforeSigning {
static let title = String(localized: .ReviewTransaction.title)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import Combine
import UIKit

/// Step 2 of Send — read-only summary of the prepared payment (recipient,
/// amount, gas, total) plus an "I have reviewed" checkbox + accept CTA.
final class ReviewTransactionBeforeSigningView: ScrollableStackViewOwner {
private lazy var recipientAddressesLabel = UILabel()
private lazy var recipientLegacyAddressView = TitledValueView()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,30 @@ import Combine
import Foundation
import Zesame

/// Outcome of step 2 of Send.
enum ReviewTransactionBeforeSigningUserAction {
/// User checked "I have reviewed" and tapped accept; payment forwarded to signing.
case acceptPaymentProceedWithSigning(Payment)
}

/// View model for step 2 of Send. Displays the prepared payment in human-readable
/// form (formatted amounts, both legacy hex + bech32 recipient addresses) and
/// gates the accept CTA on the "I have reviewed" checkbox.
final class ReviewTransactionBeforeSigningViewModel: BaseViewModel<
ReviewTransactionBeforeSigningUserAction,
ReviewTransactionBeforeSigningViewModel.InputFromView,
ReviewTransactionBeforeSigningViewModel.Output
> {
/// The payment to display + forward.
private let paymentToReview: Payment

/// Captures the payment to display.
init(paymentToReview: Payment) {
self.paymentToReview = paymentToReview
}

/// Wires the accept-tap (carries `paymentToReview` upstream) and formats
/// the four displayed values (recipient hex/bech32, amount, fee, total).
override func transform(input: Input) -> Output {
func userDid(_ userAction: NavigationStep) {
navigator.next(userAction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@

import Foundation

/// `SceneController` glue for the sign-transaction screen (step 3 of Send).
/// Re-prompts for the wallet password and runs the actual signing + broadcast.
final class SignTransaction: Scene<SignTransactionView> {}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import Combine
import UIKit

/// Step 3 of Send — re-prompt for the keystore password and sign+broadcast on tap.
final class SignTransactionView: ScrollableStackViewOwner {
private lazy var confirmTransactionLabel = UILabel()
private lazy var encryptionPasswordField = FloatingLabelTextField()
Expand Down
Loading