Skip to content

Release: develop -> main - #194

Merged
TaprootFreak merged 3 commits into
mainfrom
develop
Jul 29, 2026
Merged

Release: develop -> main#194
TaprootFreak merged 3 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

TaprootFreak and others added 3 commits July 27, 2026 22:01
* feat(bank): add a receive-IBAN check to the bank client and hook

The support form asks a customer which account their missing transfer went to. Today
that field is a required dropdown filled from GET /bank, which lists only the collective
accounts - a customer who deposits through a personal IBAN can never find their own
receiving IBAN there and has to pick an account they never transferred to. The field is
becoming free text, and the API gained PUT /bank/receive-iban so the frontend can tell
the customer straight away whether what they typed is an IBAN DFX receives money on.

This exposes that call: BankUrl.receiveIban, the ReceiveIbanStatus enum with its four
wire values, the ReceiveIbanCheck response type, BankApi.checkReceiveIban on the client,
and checkReceiveIban on the useBank hook.

NotMatched deliberately does not claim the IBAN is unknown to DFX - personal IBANs of
other accounts are never checked, and an account merge leaves personal IBANs behind on
the former account, so a customer's own older IBAN can land there too. The comment on the
enum says so, because the wording a frontend shows depends on it.

Unlike the neighbouring list(), the new client method does not pass token: false. The
endpoint distinguishes an authenticated check from LoginRequired, so the bearer token has
to travel with the request whenever one exists.

No existing API changes: getBanks, BankUrl.get and the Bank type are untouched.

* test(core): pin the receive-IBAN call shape and document the status enum

Review found two gaps in the previous commit.

The client method had no test, although CONTRIBUTING asks for tests when functionality is
added to core, and the pattern was there all along - auth-api.test.ts asserts the exact
call shape of a client method, token flag included. The relevant invariant here is the
absence of token: false, since dropping the bearer token would make LoginRequired
unresolvable and silently disable the personal-IBAN half of the check. bank-api.test.ts
now pins the URL, method and body, asserts that no token key is present, and keeps list()
asserted with token: false so the deliberate asymmetry between the two is written down.

The enum comments were line comments. tsc strips those from the declaration output, and a
consumer only ever sees dist/ - so the warning that NotMatched makes no claim about DFX
ownership would never have reached the developer writing the customer-facing wording, who
is exactly who it was written for. All four members are JSDoc now and reach
dist/definitions/bank.d.ts, verified on the built artifact rather than the source.

DfxIban is phrased as ownership rather than active receiving, because most rows in the
bank registry are decommissioned accounts and a present-tense claim would be untrue.

* docs(core): correct the receive-IBAN test name and document both DfxIban branches

Review found the test title stating the inverse of the invariant it guards. The
API answers LoginRequired precisely when no token arrives, so naming the
token-preserving test after that status said the opposite of what it pins, and mocking
LoginRequired made the wrong reading look deliberate. Title and mock now agree with the
assertion and its comment; the assertion itself is unchanged.

The DfxIban documentation named only the collective-account branch. The other one - the
requesting account's own personal deposit IBAN - is the case the endpoint exists for,
and with the enum documented as the source of truth for the customer-facing wording,
leaving the positive case unstated was the worst half to omit.

* refactor(core): use a camelCase route for the receive-IBAN check

The API repository documents camelCase for URL routes, and the closest sibling for
this very concept - GET /v1/buy/personalIban - follows it, as do the two read-with-a-body
precedents this endpoint was modelled on. The hyphenated form was an unjustified
deviation.

Doing it now is free: the endpoint is unreleased and has no consumer in production.
Once this package ships the call, the same rename would break everyone using it.

Only the route value changes. The BankUrl key, the enum values, the method and type
names and the documentation all stay as they are; BankApi and the hook reference the
constant symbolically and follow automatically. The test keeps a literal rather than the
constant, so an unintended change to the route still fails a test.

* docs(core): put the usage notes where a consumer sees them and fix three status claims

Two independent reviews found that the enum documentation had been copied from the API
repository without the corrections made there afterwards. Three statements were therefore
still wrong here.

LoginRequired described itself as "the caller is not logged in". That excludes the case it
also covers: an authenticated token that carries no account-scoped identity. It now names
the cause instead of the login state.

DfxIban claimed that many bank rows are decommissioned. That quantifies over the contents
of the production table, cannot be checked from this repository, and was never the point -
what a consumer needs is that a match does not imply the account still accepts funds.

NotMatched explained itself by naming a function in the API repository. A reader of this
package cannot open it, and the statement can go stale there without anything failing here.
The consequence a consumer actually needs survives without the mechanism: an IBAN the same
customer used earlier can land in this state, so it must never be phrased as "this IBAN
does not belong to DFX".

The concurrency and throttling requirements lived only in the pull request description,
which nobody sees who installs the package. They are now JSDoc on both public methods and
verified present in the built declarations. For the React side that meant annotating the
interface member rather than the function inside the hook: useBank emits as returning
BankInterface, so a comment on the implementation would have produced no declaration at all
and the note would have been invisible for the third time in this repository.

* docs(core,react): keep the receive-IBAN docs to what a consumer can verify

This documentation ships as .d.ts to consumers who have only the installed
package in front of them. Anything it asserts about the service behind the call
is unverifiable there and goes stale without a single test turning red.

Removed accordingly: that the endpoint is throttled and what HTTP 429 means,
that many DFX accounts are decommissioned, that an IBAN the same customer used
earlier can end up unmatched, that another customer's personal IBAN is never
checked, and which steps the check skips when no identity is present.

What replaces them is either verifiable in this repository or phrased as
guidance rather than fact: any non-2xx response is raised as an ApiException by
the client in this package, and a rejected call means the IBAN could not be
checked rather than that it was rejected. The wording rule for NotMatched
stays - it is an instruction to the consumer, not a claim about the service.

The test comment loses its assertion about how the service distinguishes two
statuses and states what the assertion actually pins: unlike list(), this call
does not opt out of the token.

* fix(core,react): make the failure wording true for both packages and the test bite

The previous wording said any non-2xx response is raised. That holds for the
core client, but not for the hook: on HTTP 401 the shared api hook retries once
with the current token when the stored token changed while the call was in
flight, and a successful retry resolves normally. Both places now say what is
true in either package - a failure never arrives as a status, and a rejection is
an ApiException - and keep the instruction to read that as "could not check".

The endpoint test asserted the sent IBAN against the same constant it passed in,
so an implementation that always sent one hardcoded IBAN passed. It now runs
over two distinct example IBANs, which fails that implementation on the second
case.

The second test's title claimed what the service can answer. It now states what
the assertion pins: unlike list(), this call does not set token: false.
 - @dfx.swiss/core@0.6.0-beta.0
 - @dfx.swiss/react@1.7.0-beta.0
* fix(react): refresh the cached user after updateMail

updateMail was a bare passthrough to the API hook: unlike updateUser, verifyMail
and every other sibling in this context, it neither refreshed the cached user
nor toggled isUserUpdating. The endpoint answers with an empty body, so the
refreshed user has to be fetched explicitly.

Consumers therefore kept a user whose mail was still null after a successful
update, re-rendered the same "enter your email" step and re-submitted the same
address — which the API answers with 403 TFA_REQUIRED once the account carries a
mail. The missing isUserUpdating also left callers unable to disable their submit
button, so the confirm action stayed clickable while a request was in flight.

Both were lost in the switch from changeMail to updateMail in #473; changeMail
delegated to updateUser and got the refresh and the flag for free.

* fix(react): keep a failed user refresh from failing the mail update

The refresh added in the previous commit rejected updateMail when the follow-up
getUser() failed, even though the mail had already been changed server-side.
Consumers would then present a successful change as an error, the user would
re-submit the same address, and the account now carrying a mail would answer 403
TFA_REQUIRED — the exact loop this change set exists to remove, just on a
narrower path. The refresh is now best-effort; the update result itself still
propagates.

* fix(react): keep the user identity stable when the mail update did not apply

Review found that the refresh could drive a consumer into an endless update loop.
PUT /v2/user/mail only persists the address when the account has none; once one
exists it answers 202 and holds the new address until the emailed code is
verified, so the refetched user still carries the OLD address. A consumer whose
effect watches the user object and compares it against a target address — which
is exactly what the services widget does for its mail parameter — then sees a new
object identity with the condition still unmet, calls updateMail again, and
repeats. Each pass sends another verification mail and invalidates the previous
code, so the flow can never be completed.

The refresh now keeps the previous object when the address did not move, so an
update that changed nothing cannot re-trigger those effects.

* fix(react): do not clear the cached user when the refresh yields nothing

The identity guard adopted the refetched value whenever the address differed,
which included the case where the refetch resolved undefined: an OK response
whose body does not parse resolves that way, and the cached user was then wiped
even though the mail update itself had succeeded. That contradicted the
best-effort contract of this refresh, under which a failed refetch leaves the
user untouched. The previous value is now kept unless a real user came back.

* docs(react): condense the comment on the updateMail refresh

Keep the two non-obvious constraints, best-effort and stable identity when the
address did not move, without restating the chain below.

* fix(react): keep a post-logout refresh from restoring the user

The refresh callback treated a cleared state as "no previous value to compare
against" and wrote the fetched user back. A getUser() response arriving after
the session ended therefore put the signed-out user into the context again,
without a valid token behind it.

updateMail is the only mutation in this file that lacks the `if (!user) return;`
guard its siblings carry, because it was a bare passthrough until this branch
gave it a state write.

---------

Co-authored-by: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com>
@TaprootFreak
TaprootFreak merged commit 3dcf2ae into main Jul 29, 2026
2 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