ENH - refine banking experience

This commit is contained in:
Type
2026-08-07 13:03:48 +02:00
parent 082e16f465
commit 0bde681655
10 changed files with 104 additions and 157 deletions
+17 -26
View File
@@ -1724,32 +1724,6 @@ button {
color: #fff;
}
.banking-profile {
position: relative;
display: grid;
width: 36px;
height: 36px;
flex: 0 0 auto;
place-items: center;
border-radius: 50%;
background: transparent;
color: #dce9ff;
}
.banking-profile .banking-spin {
position: absolute;
right: -2px;
bottom: -2px;
padding: 3px;
border-radius: 50%;
background: var(--bank-blue);
box-sizing: content-box;
}
.banking-spin { animation: banking-spin 0.75s linear infinite; }
@keyframes banking-spin { to { transform: rotate(360deg); } }
.banking-scroll {
position: absolute;
inset: 0;
@@ -1761,6 +1735,23 @@ button {
.banking-scroll::-webkit-scrollbar { display: none; }
.banking-pull-refresh {
position: absolute;
z-index: 4;
top: 78px;
right: 0;
left: 0;
display: flex;
justify-content: center;
opacity: 0;
pointer-events: none;
transition:
opacity 0.16s ease,
transform 0.16s ease;
}
.banking-pull-refresh.is-visible { opacity: 1; }
.banking-loading,
.banking-empty {
position: absolute;
+1 -1
View File
@@ -55,7 +55,7 @@ describe('banking store', () => {
const banking = useBankingStore()
banking.overview = overview
await banking.perform('withdraw', 50000)
await banking.perform('transfer', 50000, 17)
expect(banking.overview).toEqual(overview)
expect(banking.error).toBe('insufficient_funds')
-12
View File
@@ -216,8 +216,6 @@ const defaultLocales: LocaleTree = {
recentPeriod: 'in recent activity',
actions: 'Banking actions',
send: 'Send',
deposit: 'Deposit',
withdraw: 'Withdraw',
accounts: 'Accounts',
bankAccount: 'Bank account',
cash: 'Cash',
@@ -249,16 +247,6 @@ const defaultLocales: LocaleTree = {
body: 'Transfer money from your bank account to an online player.',
submit: 'Send transfer',
},
deposit: {
title: 'Deposit cash',
body: 'Move cash from your wallet into your bank account.',
submit: 'Deposit cash',
},
withdraw: {
title: 'Withdraw cash',
body: 'Move money from your bank account into your wallet.',
submit: 'Withdraw cash',
},
},
errors: {
invalid_request: 'Enter a valid whole amount and player ID.',
+1 -1
View File
@@ -22,4 +22,4 @@ export type BankingOverview = {
transactions: BankingTransaction[]
}
export type BankingAction = 'deposit' | 'withdraw' | 'transfer'
export type BankingAction = 'transfer'
+76 -38
View File
@@ -24,7 +24,6 @@ import {
CircleDollarSign,
House,
Landmark,
RefreshCw,
Send,
WalletCards,
X,
@@ -48,6 +47,14 @@ const action = ref<BankingAction | null>(null)
const amount = ref('')
const target = ref('')
const formError = ref('')
const bankingScroll = ref<HTMLElement | null>(null)
const isRefreshing = ref(false)
const pullDistance = ref(0)
const pullThreshold = 56
let pullStartY = 0
let isPulling = false
let wheelRefreshTimeout: ReturnType<typeof setTimeout> | undefined
const transactionIcons: Record<BankingTransactionKind, typeof Send> = {
deposit: ArrowDownLeft,
@@ -136,6 +143,55 @@ function closeAction(): void {
action.value = null
}
async function refresh(): Promise<void> {
if (isRefreshing.value) return
isRefreshing.value = true
pullDistance.value = pullThreshold
await banking.load()
isRefreshing.value = false
pullDistance.value = 0
}
function atTop(): boolean {
return (bankingScroll.value?.scrollTop ?? 0) <= 0
}
function startPull(event: TouchEvent): void {
if (!atTop() || isRefreshing.value) return
pullStartY = event.touches[0]?.clientY ?? 0
isPulling = true
}
function movePull(event: TouchEvent): void {
if (!isPulling || isRefreshing.value) return
const distance = (event.touches[0]?.clientY ?? pullStartY) - pullStartY
if (distance <= 0) {
pullDistance.value = 0
return
}
pullDistance.value = Math.min(pullThreshold + 20, distance * 0.45)
}
function finishPull(): void {
if (!isPulling && pullDistance.value === 0) return
isPulling = false
if (pullDistance.value >= pullThreshold) {
void refresh()
return
}
pullDistance.value = 0
}
function pullWithWheel(event: WheelEvent): void {
if (!atTop() || isRefreshing.value || event.deltaY >= 0) return
pullDistance.value = Math.min(
pullThreshold + 20,
pullDistance.value + Math.abs(event.deltaY) * 0.18,
)
if (wheelRefreshTimeout) clearTimeout(wheelRefreshTimeout)
wheelRefreshTimeout = setTimeout(finishPull, 130)
}
function errorMessage(code: string): string {
return phone.t(`Apps.banking.errors.${code}`) ===
`Apps.banking.errors.${code}`
@@ -179,20 +235,7 @@ onMounted(() => void banking.load())
class="banking-navbar"
:subtitle="phone.t('Apps.banking.welcome')"
:title="banking.overview?.playerName ?? phone.t('Common.loading')"
>
<template #right>
<button
type="button"
class="banking-profile"
:aria-label="phone.t('Apps.banking.refresh')"
:disabled="banking.isLoading"
@click="banking.load()"
>
<Landmark :size="20" />
<RefreshCw v-if="banking.isLoading" class="banking-spin" :size="10" />
</button>
</template>
</k-navbar>
/>
<div v-if="!banking.overview && banking.isLoading" class="banking-loading">
<k-preloader />
@@ -208,7 +251,23 @@ onMounted(() => void banking.load())
</k-button>
</div>
<div v-else class="banking-scroll">
<div
v-else
ref="bankingScroll"
class="banking-scroll"
@touchend="finishPull"
@touchmove.passive="movePull"
@touchstart.passive="startPull"
@wheel="pullWithWheel"
>
<div
class="banking-pull-refresh"
:class="{ 'is-visible': pullDistance > 0 }"
:style="{ transform: `translateY(${pullDistance - pullThreshold}px)` }"
aria-live="polite"
>
<k-preloader />
</div>
<template v-if="activeTab === 'home'">
<k-glass class="banking-balance">
<div class="banking-balance__label">
@@ -233,24 +292,6 @@ onMounted(() => void banking.load())
<b>{{ phone.t('Apps.banking.send') }}</b>
<ChevronRight :size="16" aria-hidden="true" />
</k-glass>
<k-glass
component="button"
type="button"
class="banking-action banking-action--secondary"
@click="openAction('deposit')"
>
<span class="banking-action__icon"><ArrowDownLeft :size="20" /></span>
<b>{{ phone.t('Apps.banking.deposit') }}</b>
</k-glass>
<k-glass
component="button"
type="button"
class="banking-action banking-action--secondary"
@click="openAction('withdraw')"
>
<span class="banking-action__icon"><ArrowUpRight :size="20" /></span>
<b>{{ phone.t('Apps.banking.withdraw') }}</b>
</k-glass>
</section>
<k-card class="banking-card banking-accounts">
@@ -404,15 +445,12 @@ onMounted(() => void banking.load())
<X :size="17" />
</button>
<span class="banking-modal__icon">
<Send v-if="action === 'transfer'" :size="23" />
<ArrowDownLeft v-else-if="action === 'deposit'" :size="23" />
<ArrowUpRight v-else :size="23" />
<Send :size="23" />
</span>
<h2>{{ phone.t(`Apps.banking.forms.${action}.title`) }}</h2>
<p>{{ phone.t(`Apps.banking.forms.${action}.body`) }}</p>
<k-list inset strong class="banking-form-list">
<k-list-input
v-if="action === 'transfer'"
:label="phone.t('Apps.banking.playerId')"
inputmode="numeric"
min="1"