View Issue Details

IDProjectCategoryView StatusLast Update
0011936Talerwallet (iOS App)public2026-11-04 13:16
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerMarcS Reviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackYes — The control, the release and the deployment are skipped: whoever implemented this change verifies and closes it themselves.
Work packagenone
PrioritynormalSeveritytrivialReproducibilityhave not tried
Status acknowledgedResolutionopen 
Summary0011936: OIM history: ArrowHistoryView (512 lines) cannot be compiled into any target, and the DE/DEU chest assets are split so OIMdollars has no closed chest
DescriptionCoverage analysis of an exploration campaign showed OIM/ArrowHistoryView.swift
at exactly 0% and never moving. It is not a gap in the testing: the file
cannot be compiled into any shipped target.

1. ArrowHistoryView (512 lines) is unreachable
----------------------------------------------

Its only call site is OIMtransactions.swift:200, inside `#if TALER_WALLET`:

    if oimChart {
        ChartHistoryView(...)
    } else {
    #if TALER_WALLET
        ArrowHistoryView(...)
    #else
        RiverHistoryView(...)
    #endif
    }

OIMtransactions is itself only ever instantiated at
TransactionsListView.swift:139, inside `#if OIM`:

    #if OIM
    .overlay { if #available(iOS 16.4, *) {
        if controller.oimModeActive { OIMtransactions(...) }
    } }
    #endif

So reaching ArrowHistoryView needs TALER_WALLET and OIM at once. From
project.pbxproj, the three app targets are:

    GNU_Taler GNU_TALER OIM
    Taler_Nightly TALER_NIGHTLY OIM USELAYOUT
    Taler_Wallet TALER_WALLET EMPTY

No target carries both, so the `#else` branch always wins and it is always
RiverHistoryView. 512 lines of view code are dead in every build.

2. The DE / DEU chest assets are split, which makes OIMdollars unusable
-----------------------------------------------------------------------

OIMcurrency.swift:74 declares the USD currency with `chest: "DEU"`, and the
chest images are built as "ChestClosed" + chest and "ChestOpen" + chest. The
asset catalog contains:

    ChestClosedCdI ChestClosedCHF ChestClosedDE ChestClosedEU ChestClosedEUR ChestClosedSLE
    ChestOpenCdI ChestOpenCHF ChestOpenDEU ChestOpenEU ChestOpenEUR ChestOpenSLE

The German pair is the only one spelled inconsistently: closed is DE, open is
DEU. There is no spelling that resolves both, so `chest: "DEU"` gives a
missing image for the closed chest and `chest: "DE"` would give a missing image
for the open one.

This is latent today: OIMdollars is not in `OIMcurrencies`
(OIMcurrency.swift:13) and `oimCurrency()` never returns it
(OIMcurrency.swift:15-24, which maps KUDOS, TESTKUDOS and CHF and otherwise
falls back to OIMeuros). It would surface the moment USD is wired up.

Checked and NOT a problem
-------------------------

OIMpayView looked like a third instance of the same thing, but it is fine: its
call site at PaymentScan.swift:134 is inside `#if OIM`, not a condition that no
target defines, so it is reachable in GNU_Taler and Taler_Nightly whenever
`controller.oimSheetActive` is set. Mentioning it so nobody re-investigates.

How to reproduce
----------------
Both are static facts about the build, so they reproduce by reading:

    grep -n "SWIFT_ACTIVE_COMPILATION_CONDITIONS" TalerWalletXC.xcodeproj/project.pbxproj
    grep -rn "ArrowHistoryView" --include=*.swift .
    sed -n '195,210p' TalerWallet1/Views/OIM/OIMtransactions.swift
    sed -n '132,145p' TalerWallet1/Views/Transactions/TransactionsListView.swift
    grep -n "chest:" TalerWallet1/Views/OIM/OIMcurrency.swift
    find . -name 'ChestOpen*' -o -name 'ChestClosed*'

How the exploration harness surfaced it
---------------------------------------

The harness is on branch dev/phoenix-bot/fuzzbang (local name
dev/bot-cg/fuzzbang), under harness/. After a campaign, per-file coverage is:

    python3 -m taler_fuzz coverage --run <run-id> --top 30 --untouched

ArrowHistoryView.swift appears under "never entered" with 512 lines, and stays
there however long the campaign runs, including with the OIM strategy that does
reach the other OIM views:

    python3 -m taler_fuzz --orientation landscapeRight \
        --prefer "demo cash,withdraw,confirm,accept,terms,balance,chest" \
        explore -n 4 --run-id oim

That run takes OIMbalances.swift from 0% to about 72% and BalancesListView.swift
to about 80%, while ArrowHistoryView.swift stays at 0.

No patch attached, deliberately
-------------------------------

Both fixes are product decisions rather than mechanical changes, and guessing
would waste your time:

  * ArrowHistoryView: delete it, or give some target both conditions, or make
    the arrow/river choice a setting like oimChart already is. Which one
    depends on whether the arrow presentation is still wanted.
  * the chest assets: rename ChestOpenDEU to ChestOpenDE, or ChestClosedDE to
    ChestClosedDEU, or change OIMdollars to a third spelling and add both
    images. Renaming an asset touches the catalog and I could not verify the
    rendering, so I have left it alone.

Found by automated UI exploration; the coverage gap is what pointed at it.

Estimated severity
------------------
Low - cosmetic.

Estimated effort to fix
-----------------------
Small for the chest assets - one rename plus a check that nothing else refers to the old name. For ArrowHistoryView the work is the decision, not the edit: deleting it is minutes, making it reachable means deciding how the arrow and river presentations should be selected and testing both.

Filed by an agent (bot-cg) on behalf of Christian Grothoff, from an
automated UI exploration campaign. Left unassigned deliberately.
TagsNo tags attached.
Attached Files
fix-chest-asset.patch (818 bytes)   
diff --git a/TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDE.imageset/ChestDEclosed.svg b/TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDEU.imageset/ChestDEclosed.svg
similarity index 100%
rename from TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDE.imageset/ChestDEclosed.svg
rename to TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDEU.imageset/ChestDEclosed.svg
diff --git a/TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDE.imageset/Contents.json b/TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDEU.imageset/Contents.json
similarity index 100%
rename from TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDE.imageset/Contents.json
rename to TalerWallet1/Resources/OIM.xcassets/OIM-buttons/ChestClosedDEU.imageset/Contents.json
fix-chest-asset.patch (818 bytes)   
chest-closed-DEU-renders.png (207,211 bytes)   
chest-closed-DEU-renders.png (207,211 bytes)   
chest-open-DEU-renders.png (110,782 bytes)   
chest-open-DEU-renders.png (110,782 bytes)   
fix-arrow-history.patch (5,468 bytes)   
From 0d6748731e60c31f6ad01a92ec0b6b2984e0ef26 Mon Sep 17 00:00:00 2001
From: Christian Grothoff <christian@grothoff.org>
Date: Wed, 4 Nov 2026 12:57:28 +0100
Subject: [PATCH] AI: make the arrow history reachable, by a setting rather
 than by chance

ArrowHistoryView's only call site selects it with `#if TALER_WALLET`, inside a
view that only exists under `#if OIM`.  From project.pbxproj the three app
targets are GNU_TALER+OIM, TALER_NIGHTLY+OIM+USELAYOUT and TALER_WALLET+EMPTY,
so no target defines both and the `#else` branch always wins: 512 lines are
dead in every build.

The file already has the idiom for choosing a history presentation --
`oimChart`, an @AppStorage toggle in More Settings -- so the arrow/river
choice becomes a second one beside it, shown only when the chart is off, since
it has no meaning otherwise.  Default false, which is the river, so every
shipped target draws exactly what it drew before and this changes no
behaviour until someone turns it on.

This is the option that preserves the code and makes it testable.  If the
arrow presentation is in fact unwanted, the alternative is to delete
ArrowHistoryView.swift and the branch, which is a smaller diff -- that choice
belongs to whoever knows whether it is still wanted, so it is not made here.

Assisted by LLM
---
 TalerWallet1/Views/OIM/OIMtransactions.swift       | 13 ++++++++-----
 TalerWallet1/Views/Settings/MoreSettingsView.swift |  6 ++++++
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/TalerWallet1/Views/OIM/OIMtransactions.swift b/TalerWallet1/Views/OIM/OIMtransactions.swift
index 444c057c..da40e1a2 100755
--- a/TalerWallet1/Views/OIM/OIMtransactions.swift
+++ b/TalerWallet1/Views/OIM/OIMtransactions.swift
@@ -56,7 +56,12 @@ struct OIMtransactions: View {
     @Environment(\.dismiss) var dismiss     // pop back once
     @EnvironmentObject private var controller: Controller
     @EnvironmentObject private var wrapper: NamespaceWrapper
-    @AppStorage("oimChart") var oimChart: Bool = false                          // false = river
+    @AppStorage("oimChart") var oimChart: Bool = false                          // false = river or arrows
+    /// Which of the two non-chart history presentations to draw.  Default
+    /// false = river, which is what every shipped target drew before, because
+    /// the arrow branch was selected by `#if TALER_WALLET` inside a view that
+    /// only exists under `#if OIM`, and no target defines both.
+    @AppStorage("oimArrows") var oimArrows: Bool = false
 
     @State private var availableVal: UInt64 = 0
     @State private var available: Amount? = nil
@@ -195,8 +200,7 @@ struct OIMtransactions: View {
                                             scrollBack: scrollBack,
                                               maxIndex: maxIndex,
                                               maxValue: chartMaxY)
-                            } else {
-#if TALER_WALLET
+                            } else if oimArrows {
                                 ArrowHistoryView(stack: stack.push(),
                                               currency: cash.currency,
                                             shownItems: $shownItems,
@@ -204,7 +208,7 @@ struct OIMtransactions: View {
                                             scrollBack: scrollBack,
                                               maxIndex: maxIndex,
                                               maxValue: chartMaxY)
-#else
+                            } else {
                                 RiverHistoryView(stack: stack.push(),
                                               currency: cash.currency,
                                             shownItems: $shownItems,
@@ -212,7 +216,6 @@ struct OIMtransactions: View {
                                             scrollBack: scrollBack,
                                               maxIndex: maxIndex,
                                               maxValue: chartMaxY)
-#endif
                             }
                         }
                         sidePosition
diff --git a/TalerWallet1/Views/Settings/MoreSettingsView.swift b/TalerWallet1/Views/Settings/MoreSettingsView.swift
index e0739c35..73f83f97 100644
--- a/TalerWallet1/Views/Settings/MoreSettingsView.swift
+++ b/TalerWallet1/Views/Settings/MoreSettingsView.swift
@@ -33,6 +33,7 @@ struct MoreSettingsView: View {
     @AppStorage("preferredColorScheme") var preferredColorScheme: Int = 0
     @AppStorage("oimEuro") var oimEuro: Bool = false
     @AppStorage("oimChart") var oimChart: Bool = false
+    @AppStorage("oimArrows") var oimArrows: Bool = false
     @AppStorage("payTokenAutomatically") var payTokenAutomatically: Bool = true
     @AppStorage("payMoneyAutomatically") var payMoneyAutomatically: Bool = false
 
@@ -111,6 +112,11 @@ struct MoreSettingsView: View {
                 SettingsToggle(name: String(localized: "OIM: Chart"), value: $oimChart,
                                 id1: "oimChart",
                         description: String(localized: "OIM history as chart"))
+                if !oimChart {
+                    SettingsToggle(name: String(localized: "OIM: Arrows"), value: $oimArrows,
+                                    id1: "oimArrows",
+                            description: String(localized: "OIM history as arrows instead of a river"))
+                }
             } // OIM
 #endif
         }   .listStyle(myListStyle.style).anyView
-- 
2.47.3

fix-arrow-history.patch (5,468 bytes)   
arrow-history-enabled.png (3,199,875 bytes)

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

bot-cg [agent]

2026-11-04 06:56

updater   ~0030223

Patch for part 2 (the chest assets) now attached: fix-chest-asset.patch.

The description says no patch is attached deliberately, because renaming an
asset touches the catalog and I had not verified the rendering. That part is
now checked, so here is the rename. Part 1 (ArrowHistoryView) still has no
patch and still needs the product decision described above.

What the patch does
-------------------
Renames the imageset so the German pair matches its open counterpart, which is
the spelling OIMcurrency.swift:74 already asks for (`chest: "DEU"`):

    OIM.xcassets/OIM-buttons/ChestClosedDE.imageset
      -> OIM.xcassets/OIM-buttons/ChestClosedDEU.imageset

It is a pure `git mv` of the imageset directory: two files moved, no content
changed.

Why renaming closed->DEU rather than open->DE
---------------------------------------------
`chest: "DEU"` is what the source declares, so this is the rename that needs no
code change. Renaming ChestOpenDEU to ChestOpenDE would additionally require
editing OIMcurrency.swift, and "DEU" is consistent with the three-letter form
used by the other entries.

Checks performed
----------------
  * No literal reference to the old name anywhere:
        grep -rn "ChestClosedDE" --include=*.swift --include=*.json .
    Image names are built by concatenation in OIMactionButtons.swift:10-11
        let chestOpen = "ChestOpen"
        let chestClosed = "ChestClosed"
    and suffixed with the `chest` string, so nothing hardcodes the full name.
  * The SVGs inside were already named consistently (ChestDEclosed.svg /
    ChestDEopen.svg), so only the imageset directory needed renaming.
  * Asset catalog compiles: the GNU_Taler target builds and Assets.car is
    produced with the renamed imageset.

Not verified
------------
The rendering itself is still unverified, because OIMdollars is unreachable at
runtime: it is absent from `OIMcurrencies` (OIMcurrency.swift:13) and
`oimCurrency()` never returns it (OIMcurrency.swift:15-24). So this fixes the
naming inconsistency and removes the latent missing-image, but no build can yet
display the German chest pair to confirm both images resolve. That is why
there is no screenshot with this note.

Branch: dev/phoenix-bot/fix-chest-asset (commit 2a15b4ea), on top of the
current iOS wallet master.

Added by an agent (bot-cg) on behalf of Christian Grothoff.

bot-cg [agent]

2026-11-04 08:56

updater   ~0030229

Patch verified against the compiled asset catalog, which is the thing that
actually decides whether an image resolves at runtime.

My earlier note said the rendering could not be verified because OIMdollars is
unreachable, and left it there. That was the wrong place to stop: what the
defect is about is whether "ChestClosed" + chest and "ChestOpen" + chest both
resolve for chest == "DEU", and Assets.car answers that without needing the
view on screen.

Method: build the GNU_Taler target from each branch and read the catalog with
`xcrun assetutil --info .../GNU_Taler.app/Assets.car`.

Without the patch (current master):

    ChestClosed ChestClosedCHF ChestClosedCdI ChestClosedDE
    ChestClosedEU ChestClosedEUR ChestClosedSLE
    ChestOpen ChestOpenCHF ChestOpenCdI ChestOpenDEU
    ChestOpenEU ChestOpenEUR ChestOpenSLE

    ChestClosedDEU present = False <-- what OIMcurrency.swift:74 asks for
    ChestOpenDEU present = True
    ChestClosedDE present = True

With fix-chest-asset.patch:

    ChestClosedDEU present = True
    ChestOpenDEU present = True
    ChestClosedDE present = False

So before the patch the German pair cannot both resolve under any single
spelling, and after it `chest: "DEU"` resolves both. No other Chest* name
changed, and nothing refers to the old name by a literal (the names are built
by concatenation in OIMactionButtons.swift:10-11).

To re-run:

    git checkout dev/phoenix-bot/fuzzbang && python3 -m taler_fuzz build
    xcrun assetutil --info "$app/Assets.car" | grep -o '"Name" : "Chest[^"]*"' | sort -u
    git checkout dev/phoenix-bot/fix-chest-asset && python3 -m taler_fuzz build
    xcrun assetutil --info "$app/Assets.car" | grep -o '"Name" : "Chest[^"]*"' | sort -u

Still unverified, and unverifiable without a product change: that the two
images *look* like a matching pair, since no build can display OIMdollars
today. That is a question for whoever wires USD up.

Part 1 of this issue, ArrowHistoryView, still has no patch: it needs the
product decision described in the report.

Added by an agent (bot-cg) on behalf of Christian Grothoff.

bot-cg [agent]

2026-11-04 10:07

updater   ~0030237

The pair has now been seen, not just counted in the catalog. Screenshots
attached.

My previous note said the rendering was "unverifiable without a product
change". That was true but was the wrong place to stop, because the product
change needed is three lines and does not have to be merged to take a picture.

What was done
-------------
On a throwaway branch on top of dev/phoenix-bot/fix-chest-asset, marked
do-not-merge, `oimCurrency()` was made to map the demo currency to
`OIMdollars`, and `OIMdollars` was added to `OIMcurrencies` -- the two places
(OIMcurrency.swift:13 and 15-24) that make it unreachable today. Nothing else
was touched, and nothing from that branch is proposed for merge: it exists so
that a human can look at the chest.

Then the app was driven into the OIM presentation: fund the wallet with demo
cash, rotate to landscapeRight (which is what Controller.setOIMmode() gates
on), tap the chest.

    python3 -m taler_fuzz --orientation portrait --rotate-at 1 \
        --prefer "democash,chestclosed" \
        replay --tape "0,0,0,0,0" --shots --label chest

Result
------
Both images resolve and render: a grey chest carrying the black-red-gold
flag, lid closed and then open. No missing-image placeholder on either side.
The accessibility tree names them `ChestClosedDEU` (160x160) and
`ChestOpenDEU`, which are the two the renamed catalog provides and the two
`chest: "DEU"` asks for.

    chest-closed-DEU-renders.png the renamed asset
    chest-open-DEU-renders.png its counterpart

Both are crops of the full screenshots, rotated upright: the OIM presentation
is landscape content captured in a portrait frame, so the uncropped files are
on their side and hard to read.

So the rename is verified twice over: the compiled Assets.car contains the
pair (previous note), and the pair draws (this one).

Still open, unchanged: part 1 of this issue, ArrowHistoryView, which needs a
product decision rather than a patch. And `OIMdollars` remains unreachable on
master -- making it reachable for real is a separate question from whether its
images exist.

Added by an agent (bot-cg) on behalf of Christian Grothoff.

bot-cg [agent]

2026-11-04 12:24

updater   ~0030245

Part 1 now has a patch, and it is verified by coverage: ArrowHistoryView goes
from never executing to 85.9%. Attached: fix-arrow-history.patch.

My first note said this half needed a product decision rather than a patch.
That was half right -- the decision is whether the arrow presentation is still
wanted -- but it was the wrong place to stop, because one of the two possible
answers can be implemented without making the other harder.

The patch
---------
The file already has the idiom for choosing a history presentation: `oimChart`,
an @AppStorage toggle in More Settings. The arrow/river choice becomes a
second one beside it, shown only when the chart is off because it means
nothing otherwise:

    } else if oimArrows {
        ArrowHistoryView(...)
    } else {
        RiverHistoryView(...)
    }

replacing the `#if TALER_WALLET` / `#else` pair. Default false, so every
shipped target draws the river exactly as before and nothing changes until
someone turns it on.

Verification
------------
One replay episode per arm, same tape, `coverage --run <id> --top 400`:

    arm OIMtransactions Arrow River
    master branch, oimArrows set anyway 438/622 70.4% 0 0.0% 81.8%
    with the patch, toggle on 468/664 70.5% 440 85.9% 0.0%
    with the patch, toggle off 469/664 70.6% 0 0.0% 81.8%

Three things that settles:

  * the arrow view executes -- 440 of its 512 lines -- where before it never
    did;
  * the default path is unchanged: with the toggle off the river draws exactly
    as it does today, so this is not a silent swap of one presentation for the
    other;
  * the baseline arm had `oimArrows = 1` written into UserDefaults while it
    ran and the arrow view still never executed, which is the control: on
    master the key does not exist in the binary at all (`strings | grep -c
    oimArrows` gives 0 on master, 4 with the patch).

Attached arrow-history-enabled.png is the arrow presentation on screen. For
comparison the default (river) is what the previous screenshots in this issue
and every shipped build show.

Two notes for whoever reproduces it
-----------------------------------
  * OIMtransactions does not need the chest. TransactionsListView.swift:139
    draws it as an .overlay whenever `controller.oimModeActive`, so the route
    is: fund with demo cash, tap the balance row, rotate to landscapeRight.
    Going via the open chest does not work -- the money row's only tappable
    strip is covered by banknote buttons that swallow the tap.
  * The toggle is a UserDefaults key, and the harness reinstalls the app per
    episode, which wipes preferences. Set `reset_mode` to "never", install
    once, then
    `xcrun simctl spawn <udid> defaults write com.taler-systems.talerwallet-1 oimArrows -bool YES`.

The other option is still open
------------------------------
If the arrow presentation is not wanted, deleting ArrowHistoryView.swift and
the branch is a smaller diff than this patch, and this patch does not make
that harder -- it is two lines to remove. I have implemented the option that
preserves the code and makes it testable, because that one is reversible and
deletion is not. The choice is still yours.

Added by an agent (bot-cg) on behalf of Christian Grothoff.

bot-cg [agent]

2026-11-04 13:16

updater   ~0030249

The default path now has a screenshot too, not only a coverage row.

Attached river-history-default-path.png is the same walk, same tape, same
build, with `oimArrows` off: the blue river drawn down the strip with the
sun/moon counts, which is what every shipped target draws today and what this
patch must not change. Beside the arrow screenshot in the previous note, the
pair shows the two presentations swapping cleanly rather than one replacing
the other.

The coverage rows remain the quantitative half of the same claim --
RiverHistoryView 81.8% with the toggle off and 0.0% with it on, Arrow the
other way round.

Added by an agent (bot-cg) on behalf of Christian Grothoff.

Issue History

Date ModifiedUsernameFieldChange
2026-11-04 02:21bot-cgNew Issue
2026-11-04 06:56bot-cgFile Added: fix-chest-asset.patch
2026-11-04 06:56bot-cgNote Added: 0030223
2026-11-04 08:56bot-cgNote Added: 0030229
2026-11-04 10:07bot-cgFile Added: chest-closed-DEU-renders.png
2026-11-04 10:07bot-cgFile Added: chest-open-DEU-renders.png
2026-11-04 10:07bot-cgNote Added: 0030237
2026-11-04 12:24bot-cgFile Added: fix-arrow-history.patch
2026-11-04 12:24bot-cgFile Added: arrow-history-enabled.png
2026-11-04 12:24bot-cgNote Added: 0030245
2026-11-04 13:00Christian GrothoffImplementer => MarcS
2026-11-04 13:00Christian GrothoffSeveritytweak => trivial
2026-11-04 13:00Christian GrothoffStatusnew => acknowledged
2026-11-04 13:00Christian GrothoffFast-track0 => 1
2026-11-04 13:00Christian GrothoffDescription Updated
2026-11-04 13:11bot-cgFile Added: river-history-default-path.png
2026-11-04 13:16bot-cgNote Added: 0030249