Skip to content

Release: develop -> main - #1211

Merged
TaprootFreak merged 5 commits into
mainfrom
develop
Jul 30, 2026
Merged

Release: develop -> main#1211
TaprootFreak merged 5 commits into
mainfrom
develop

Conversation

@github-actions

Copy link
Copy Markdown

Automatic Release PR

This PR was automatically created after changes were pushed to develop.

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge when ready for production

Danswar and others added 2 commits July 29, 2026 19:23
…result (#1204)

* fix(buy): stop the completion mail step from silently swallowing its result

MailEdit is rendered by BuyCompletion when the account has no mail yet. Two
defects turned that step into a dead end:

BuyCompletion passed `onSubmit={(email) => (!email || email.length === 0) &&
close()}`, which for a non-empty address evaluates to `false && close()` and
therefore does nothing. After a successful save the confirmation screen stayed
exactly as it was — same address, same enabled Confirm button, no feedback — so
the user clicked Confirm again. The account now carried a mail, which the API
answers with 403 TFA_REQUIRED, and MailEdit rethrew that into an unhandled
promise rejection with no UI change at all, inviting yet another click. In
production this produced 1206 TFA_REQUIRED responses on PUT /v2/user/mail over
30 days against 19 successful sets.

MailEdit now handles the 403 the way kyc.screen.tsx already does — redirect to
/2fa with the Basic level — surfaces every other error through ErrorHint instead
of discarding it, and tracks its own in-flight state so Confirm is disabled
during the request regardless of the isUserUpdating flag from the react package.
BuyCompletion closes on a successful submit, matching what its own Close button
does in the branch for accounts that already have a mail.

* fix(buy): explain the mail step's failure cases instead of redirecting out of the flow

Follow-up to the previous commit, from review:

The 2FA redirect it introduced was not an improvement. setRedirect stores only the
pathname, while the completion view is component-local state in the buy screens, so
returning from /2fa remounts an empty buy form: the typed address is gone, the buy
completion screen is unreachable, and nothing explains why. That trades a visible
dead end for a silent flow reset. This step can only ever set a FIRST address —
changing an existing one also needs a code sent to the new address, and this
component has no field to enter one — so it now says exactly that and leaves the
user on the completion screen, where Close still works.

Two more cases the previous commit routed into the generic error hint:

- A 409 "account already exists - account merge request sent" reaches this step on
  the first-set path too. ErrorHint tells the user to try again, which only mails
  another merge request; the correct action is to follow the link already sent. It
  now shows the same hint the account and KYC screens use for this case.

- A stale error survived the Change button, so a new address could be presented
  with the previous address's error still on screen.

The new string is added to the German, French and Italian translations.

* fix(settings): stop the mail parameter from re-submitting on every user refresh

The effect that applies the mail app-parameter compares it against the cached
user address with a case-sensitive !==, but the API lowercases the address on
write. A parameter such as ?mail=John.Doe@Example.com therefore never satisfies
the guard: it fires the update again on every change of the user object, which
is one of the ways an account that already carries an address ends up hammering
PUT /v2/user/mail and collecting 403 TFA_REQUIRED. Comparing case-insensitively
lets the guard close once the address has been applied.

* fix(buy): give the "account already has an address" case its own screen and a way out

Review found the previous commit inconsistent with its own reasoning. It moved the
409 merge case out of ErrorHint because ErrorHint's fixed opening line tells the
user to try again, which is the wrong action there — then routed the 2FA case into
ErrorHint, where retrying is just as futile: the account has an address, so every
further PUT answers 403 and this component has no field for the emailed code. The
user read "Please try again" directly above "you can change it in your account
settings", with Confirm still enabled.

It also had no exit. BuyCompletion renders its Close button only in the branch for
accounts that already carry an address, and both host screens suppress the layout
back button while the completion is showing, so this state could only be left
through the burger menu.

The case now gets the same treatment as the merge hint: its own short screen with
an OK button that ends the step.

* fix(settings): bound the mail parameter to one submit per address

The case-insensitive guard from the previous commit only mirrored half of the
server's normaliser. The API applies trim and lowercase, so a padded parameter
such as ?mail=%20a@b.com is stored as a@b.com while the guard keeps comparing
against the padded value — permanently open, and re-submitting on every change
of the user object exactly as before. The parameter is persisted to local
storage, so that survives reloads.

The guard now normalises the same way the server does, and additionally
remembers the address it already submitted. That also covers the two cases
normalisation alone cannot close: a value the API rejects outright, and a change
that is accepted with 202 and stays pending mail verification, where the stored
address legitimately does not move. The normalised address is what gets sent,
which is what the server would store anyway.

* fix(buy): give the completion screen a real exit when closing cannot do anything

close() calls closeServices and then blanks the component. On /buy/info that
combination strands the user: the screen passes navigateOnClose={false}, and
closeServices only acts when the app runs as a widget, inside an iframe, or with
a redirect URI configured — outside those it executes nothing at all. The body
then renders empty with the layout back button suppressed, leaving only the
burger menu.

That state predates this branch, reachable through the existing Close button and
the empty-address Finish path, but this PR routes three more outcomes into it. It
now falls back to the account screen when closing has nowhere to go.

* docs(buy): condense the comments on the mail step

Keep why each branch exists — retrying cannot succeed, closing can be a no-op,
the parameter guard has to normalise like the API — and drop the narration.

* fix(buy): decide the completion exit on a real close, not on host flags

isEmbedded and canClose describe the host, not whether closeServices did
anything. A widget host that leaves its optional close callback unbound, and a
stored redirect URI that is discarded as unsafe, both satisfied the flags while
nothing fired -- so the completion blanked itself and, with the back button
suppressed, stranded the user.

closeServices now reports whether a channel actually fired and the completion
falls back to the account screen when none did. Three callers that returned its
result from a Promise<void> function are adjusted accordingly.

* fix(settings): keep a transient mail-parameter failure retryable

The guard was set before the request and never taken back, so a single offline
blip or a rate-limit response lost the mail parameter for the rest of the
session. It now survives anything transient and keeps blocking only what
retrying cannot fix, and the rejection is handled instead of escaping unhandled.

* chore(deps): move @dfx.swiss/react off the prerelease to 1.7.0

The lockfile pinned 1.7.0-beta.0, so npm ci installed the prerelease even
though the caret range already allowed the stable release. 1.7.0 is the one
that carries the updateMail cache refresh this branch builds on.

Brings @dfx.swiss/core 0.6.0-beta.0 -> 0.6.0 along as a transitive
requirement of react 1.7.0; nothing else in the lockfile moves.

---------

Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
…te (#1209)

* docs(safe): say why a completed order may fall back to its creation date

The fallback inside the completed branch covers a single case - an order
completed before the valuta column existed - and the comment did not name
it, which is the one thing a reader would want to know there.

* docs(safe): describe the valuta fallback as an invariant guard

The previous wording claimed a historical data gap that never existed:
the migration adding the column backfilled every Completed row, and
production holds none without a valuta. What the fallback actually
guards is the invariant itself.

* docs(safe): call the valuta fallback what it is

Saying it guards the invariant overstated it: the fallback does not repair
a completed order without a valuta, it hides the anomaly behind a less
precise date. That is defensible in a rendered row and not where interest
is computed - the backend raises the same case there - and the comment now
says so instead of implying protection it does not provide.
…1210)

* fix(safe): format activity timestamps in Swiss notation

The activity list took its date format from the app UI language, so an
English interface rendered 1/28/26, 1:31 PM. The date format is a Swiss
convention and does not follow the interface language, so the locale is
fixed to de-CH regardless of the selected language.

Explicit field options instead of dateStyle: 'short', which would shorten
the year to two digits.

* test(safe): regenerate Safe baselines for the Swiss date format

The activity list is visible in every Safe screenshot, so the format
change affects all of them. Regenerated locally against the API on the
same seed data the previous baselines were taken on, so the diff shows
the date format and nothing else.

The PDF modal shot is unchanged: it covers the activity list.

* test(safe): regenerate the account switcher baselines too

safe-accounts.spec.ts renders the same /safe route and therefore the same
activity list, so its four screenshots carried the US format as well. They
were missed in the previous commit because the spec never names the
component — only the route.

Regenerated on the same seed data, so the diff again shows the date format
and nothing else.
* test: give async waits the time a loaded machine needs

transaction-document-error.test.tsx failed 1 in 5 full-suite runs on plain
develop, with nothing changed: findByRole ran into DOM Testing Library's
1000ms default while the transaction list had not rendered yet.

The test is not at fault. It passes 20 of 20 isolated runs, TransactionList
has no timer in its load path, and both mocks resolve immediately. Measuring
every findBy* in that file across ten full runs on an 18-core machine gave a
median wait of 278ms, a longest wait of 1523ms, and 7 of 50 waits above the
1000ms limit. With that limit as the only changed variable, all ten runs
passed. Jest defaults to one worker fewer than the machine has cores, and
under that parallelism a worker goes without CPU for well over a second.

asyncUtilTimeout now allows 5000ms, three times the longest wait measured.
jest.setTimeout follows to 15000ms so it stays above it; otherwise Jest ends
the test first and Testing Library's DOM dump naming the element it could not
find is replaced by a bare "Exceeded timeout of 5000 ms". react-scripts does
not accept testTimeout in the jest section of package.json, so it is set in
setupTests.

No assertion is weakened: each still waits for exactly the condition it
waited for before, and a genuinely broken expectation still fails. The
setting is suite-wide because every findBy* and waitFor runs against the same
limit; transaction-document-error.test.tsx is only where it surfaced first.

* build: keep the test setup out of the library build

tsconfig.build.json excludes test code by name pattern alone — **/*.test.ts,
**/*spec.ts, **/__tests__/** — and src/setupTests.ts matches none of them. It
therefore stayed in the program that build:lib emits to dist/ and that the
review bot type-checks, although it is test-only code.

That went unnoticed while the file held nothing but an import. jest.setTimeout
is the first value access to jest in it, and tsconfig.json sets
typeRoots: ["typings"], which keeps node_modules/@types out of scope, so
@types/jest never loads and the reference fails with TS2708. The whole test
suite hits the same wall, which is why it is excluded from this config to
begin with; this file was simply missed.

Verified with the command the bot runs: tsc -p tsconfig.build.json --noEmit
reports no errors, where it reported one before.
* perf(financial): skip balancesByType on the overview screen

The overview screen polls dashboard/financial/log every minute but never
reads balancesByType, which makes up ~82% of the response payload. Pass
byType=false (now supported server-side) so the overview call skips that
subtree; the history screen still requests it for its area chart.

* fix(financial): keep the by-type charts type-safe instead of defaulting to zero

The optional DTO field leaked into the chart component, turning a loud crash
into a silent zero line. A derived, narrower entry type now carries the
requirement, so the charts access the field directly again as before. The
History screen resolves it once and states plainly when the data is missing.
@TaprootFreak
TaprootFreak merged commit 89020c2 into main Jul 30, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants