PR #5732 review

Loading states: what works and what to fix

The PR adds loaders across the client app. It was tested against the commit it branches from, in the local app with demo data, on desktop and phone, with requests slowed down and forced to fail. Most of it works well. Two issues show up in normal use and are worth fixing before merge. The rest only appear when a request fails.

8 October 2026. Base: development at 3d46971. PR head: 4f16e2e.

At a glance

  1. 1Loader space stays after the page loadsFix before mergeBlank gaps on seven screens. Worst on Add account, where Next drops below the fold on a phone.
  2. 2Signup address step shows an error on a normal loadFix before mergeA red "Unable to load" banner above a form that loaded fine.
  3. 3Notifications panel shows a loader every time it opensYour callThe list it already has is hidden until the refresh returns.
  4. 4Recipient details disappear if their payments fail to loadWorth fixingThe whole card goes, including Edit and Make a transfer.
  5. 5Same-currency confirmation can vanish or spin foreverWorth fixingDepends on the recipients request, and on errors having a message.
  6. 6The loader is drawn over buttons that already workWorth fixingAllocate Recipient is covered, and blocked, while payments load.
  7. 7The refer and earn loader never showsSmallOne-line fix.
  8. 8The same-currency transfer page can spin foreverNot newHappens today too. A one-line service fix lets the PR's new error handling work.

What works

  • Loaders now show on about 25 pages that used to sit blank while loading, inside the content area and below the headings.
  • Failed requests no longer leave a spinner. With each page's own requests forced to fail, six pages used to spin forever: transfer history, trade history, authorised users, new quote, same-currency transfer and the duplicate check. Now every one clears and says what happened.
  • Three console errors that happen today are gone, on the same-currency transfer page, rate alerts and market orders.
  • No new console errors. It compiles and type-checks cleanly, merges cleanly into today's development, and CI passes. A real quote still goes from the form to Book.
Recipients overview while loading: the loader sits inside the card, below the search and Add recipient.
Recipients while loading. The loader sits in the card, under the search.
Transfer history with its request failing (forced to fail in the browser)
Before
Before: the loader keeps spinning after the request failed.
With the PR
With the PR: the loader clears and 'Unable to load transfers' shows.
1
Fix before mergeSeen in normal use

Loader space stays after the page loads

The new loading regions keep their minimum height after the data arrives: 380px, 200px for the small ones, and 240px in the notifications panel. Wherever the real content is shorter, the leftover shows as blank space. The orange boxes mark it.

Add account, desktop The empty fields area holds 380px between the account types and Next.
Before
Before: Next sits right under the two account type tiles.
With the PR
With the PR: 380px of empty space between the account type tiles and Next.
Add account, phone Next moves below the fold.
Before
Before, on a phone: Next is visible under the account types.
With the PR
With the PR, on a phone: empty space pushes Next below the fold.
Rate alerts, none set up
Before
Before: the Active rate alerts card ends under the pager.
With the PR
With the PR: 124px of empty space under the pager.
Recipients, one recipient
Before
Before: the card ends under the only recipient.
With the PR
With the PR: 125px of empty space under the recipient.
Dashboard, no recent activity
Before
Before: the dashboard ends after the recipients row.
With the PR
With the PR: 200px of empty space at the bottom.
Transfer confirmation, allocate step
Before
Before: Make bank transfer follows the unallocated funds box.
With the PR
With the PR: 100px of empty space before Make bank transfer.
Notifications panel, one notification
Before
Before: the panel fits its one notification.
With the PR
With the PR: 111px of empty space under the notification.

Why

.ct-loading-content sets min-height: 380px, and .ct-loading-content-small 200px (loading-state.component.scss:36). The class stays on the wrapper after loading. The notifications body now also has min-height: 240px (notification-list.component.scss:22).

Suggested fix

Hold the space only while the loader is showing. One change in loading-state.component.scss covers every place that uses the class:

.ct-loading-content {
  display: block;
  position: relative;

  &:has(> ct-loading-state .loading-state-container) {
    min-height: 380px;
  }

  &.ct-loading-content-small:has(> ct-loading-state .loading-state-container) {
    min-height: 200px;
  }
}

Keep the existing .ct-flex rule, and treat .notification-body's 240px the same way.

Tried in the local app by injecting this CSS into the PR build. Page heights after loading match today's exactly (Add account 890px, rate alerts 811px, transfer confirmation 1,882px), and the regions still hold 380px and 200px while loading.

2
Fix before mergeSeen in normal use

Signup address step shows an error on a normal load

On the personal address step of signup, the page shows "Unable to load this content. Please refresh the page to try again." above a form that loaded fine. Refreshing doesn't help, because it usually happens on a fresh load.

Personal address step A user part-way through signup, with the client lookup arriving after the step fields, as it does on a fresh load. The account was made to look mid-signup in the browser; the step fields are real.
Before
Before: the Home residency form, no banner.
With the PR
With the PR: the same form under a red 'Unable to load this content' banner.

Why

The page loads the step fields and the client in parallel, and the client takes two requests. When the step fields arrive first, getStepFields() reads this.client.different_home_address while client is still undefined (personal-address-page.component.ts:83). Today that error goes unhandled and the page carries on. The PR's new .catch (line 91) turns it into loadingFailed.

Suggested fix

if (this.client?.different_home_address) {

Or request the step fields once the client has arrived. The same .then(...).catch(() => this.loadingFailed = true) shape is used across the PR, so any error inside a success handler now reads as a load failure. Worth a quick check on the other signup steps.

3
Your callSeen in normal use

Notifications panel shows a loader every time it opens

Opening the bell used to show the last list straight away and refresh it quietly. With the PR, the list is replaced by "Loading Notifications" until the refresh returns, on every open. If that's intended, nothing to do.

0.7 seconds after opening the panel (requests slowed down so the moment is visible)
Before
Before: the panel shows the list straight away.
With the PR
With the PR: the panel shows Loading Notifications instead of the list.

Why

The panel's isLoading now comes from notificationService.isFetchingNotifications, and opening the panel always calls refreshNotifications().

Suggested fix

If the loader should only cover the first load, use the condition the recent activity card already uses:

[isLoading]="notificationService.isFetchingNotifications
  && !notificationService.isNotificationsLoaded"
4
Worth fixingOnly when a request fails

Recipient details disappear if their payments fail to load

If only the request for a recipient's payments fails, the whole recipient card goes, including Edit recipient and Make a transfer. Only an error and the invite card remain.

Recipient page with only the payments request failing
Before
Before: the recipient card with Edit recipient and Make a transfer still shows.
With the PR
With the PR: the recipient card is gone; an error banner shows instead.

Why

The card now waits for both requests: @if (recipient && payments), where it was @if (recipient). The payments section already has its own @if (payments).

Suggested fix

Keep @if (recipient) for the card, and show the payments error inside the payments section.

5
Worth fixingOnly when a request fails

Same-currency confirmation can vanish or spin forever

Two cases on the same-currency transfer confirmation.

  • If the recipients lookup fails, the whole confirmation is replaced by an error: title, Save as PDF, broker, totals and payments. The content now requires recipients too: @if (trade() && payments && recipients). Before, everything except the payments list still showed.
  • If a request fails without a JSON body, the loader never stops. That covers a network error, or an HTML 502 page during a deploy. The loader waits for error(), which is set from UtilsService.getError(err).message, and that is undefined in those cases.

No screenshot: the local demo data has no same-currency transfers. Both cases come from reading the code.

Where

same-currency-confirmation.component.html:1-6, and the error handlers in same-currency-confirmation.component.ts:58 and :82.

Suggested fix

Keep @if (trade() && payments) and require recipients only around the payments list. Drive the loader from explicit loading and failed flags, with fallback copy when the error has no message.

6
Worth fixingSeen while loading

The loader is drawn over buttons that already work

On the transfer confirmation, the Unallocated funds box and its Allocate Recipient button appear straight away. The loader is then drawn on top of them while payments load, and clicks are blocked until it clears. It comes back every time payments reload: after allocating, approving or rejecting, and after a mass payment.

Transfer confirmation while payments load (request slowed down)
Before
Before: the Unallocated funds box and Allocate Recipient button, usable.
With the PR
With the PR: the loader is drawn over the Unallocated funds box.

Why

ct-loading-state is position: absolute over the whole region, at z-index 8. With transparent, the content shows through but can't be clicked. loadPayments() turns it on at the start of every call (recipients-allocate-page.component.ts:178).

Suggested fix

Hide the content while its loader shows, or skip the overlay when the content is already there, as on a reload.

7
Small

The refer and earn loader never shows

referralData starts as an empty object, so the loader's !referralData check is never true and the page renders before its data arrives. Confirmed on a cold load: no loader appears.

Where

refer-and-earn-page.component.ts:27, and the loader at refer-and-earn-page.component.html:7.

Suggested fix

referralData: any;

The template already waits with @if (referralData), so this should also stop an existing console error on a cold load (accept_invitation of undefined).

8
Not new

The same-currency transfer page can spin forever

Two cases where the loader on the same-currency transfer page never clears. Both happen today as well, so they aren't caused by the PR, but the first one stops the PR's new error handling from working.

  • If delivery dates fail to load. refreshSameCurrencyTransferData() reports the error on the supported-currencies stream instead of its own (currencies.service.ts:111), so the new error handler in getDeliveryDates() never runs.
  • If the account has no balance that supports same-currency transfers. selectBalance(this.balances[0]) throws on undefined, so the delivery dates are never requested.

Suggested fix

// currencies.service.ts, refreshSameCurrencyTransferData()
this.sameCurrencyTransferDataObject.error(error);

And guard the empty balance list before selectBalance().

How this was tested