From 335b9a98b5af89fcf36c01ae5a2425f4db3c9232 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Mon, 17 Aug 2026 00:24:58 +0200 Subject: [PATCH] FIX - polish banking interactions and transaction details --- frontend/src/App.vue | 33 +++- frontend/src/assets/main.css | 112 ++++++++---- frontend/src/stores/banking.test.ts | 65 ++++++- frontend/src/stores/banking.ts | 65 ++++--- frontend/src/stores/phone.ts | 8 + frontend/src/utils/bankingAmount.test.ts | 23 +++ frontend/src/utils/bankingAmount.ts | 24 +++ .../views/apps/BankingApp.contract.test.ts | 72 ++++++++ frontend/src/views/apps/BankingApp.vue | 167 +++++++++++++----- sky_phone/config/locales/en.lua | 6 + sky_phone/source/client/main.lua | 4 +- sky_phone/source/server/banking.lua | 14 +- 12 files changed, 482 insertions(+), 111 deletions(-) create mode 100644 frontend/src/utils/bankingAmount.test.ts create mode 100644 frontend/src/utils/bankingAmount.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 81ab047..a76cec0 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -90,6 +90,7 @@ type AppMessage = { | PicstagramVerificationData | PicstagramNotificationData | FeatherNotificationData + | BankingChangedData | BillingNotificationData | EasyShareEvent | PhoneCall @@ -238,6 +239,13 @@ type BillingNotificationData = { title?: string } +type BankingChangedData = { + amount?: number + currency?: string + kind?: 'transfer_in' + sender?: string +} + type CrewLinkNotificationData = { actor?: string device?: PhoneNotificationDevicePayload @@ -973,7 +981,30 @@ function onMessage(event: MessageEvent): void { } else if (event.data?.type === 'calls:changed') { void calls.loadRecents() } else if (event.data?.type === 'banking:changed') { - void banking.load() + const data = event.data.data as BankingChangedData | undefined + void banking.load(false, true) + if ( + data?.kind === 'transfer_in' && + typeof data.amount === 'number' && + data.amount > 0 + ) { + const formattedAmount = `${ + data.currency ?? banking.overview?.currency ?? '$' + }${new Intl.NumberFormat(phone.lang, { + maximumFractionDigits: 0, + minimumFractionDigits: 0, + }).format(data.amount)}` + notifications.show({ + appId: 'banking', + route: '/apps/banking', + subtitle: data.sender, + text: phone.t('Apps.banking.notifications.received', { + amount: formattedAmount, + sender: data.sender ?? '', + }), + title: phone.t('Apps.banking.notifications.receivedTitle'), + }) + } } else if (event.data?.type === 'billing:changed') { void billing.loadOverview() } else if (event.data?.type === 'billing:new' && event.data.data) { diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 25a248f..d94ee08 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -3720,6 +3720,9 @@ button { --bank-blue: #2d76ff; --bank-cyan: #57d5ff; --bank-green: #4ee6a4; + --banking-card-radius: 22px; + --banking-page-gutter: 16px; + --banking-section-gap: 16px; position: relative; height: 100%; overflow: hidden; @@ -3772,8 +3775,16 @@ button { inset: 0; overflow-x: hidden; overflow-y: auto; - padding: 108px 17px 112px; + overscroll-behavior-y: contain; + padding: 108px var(--banking-page-gutter) 112px; scrollbar-width: none; + touch-action: pan-y; + -webkit-overflow-scrolling: touch; +} + +.banking-scroll.is-locked { + overflow-y: hidden; + touch-action: none; } .banking-scroll::-webkit-scrollbar { @@ -3828,8 +3839,8 @@ button { .banking-balance { position: relative; overflow: hidden; - padding: 22px 20px 20px; - border-radius: 27px; + padding: 20px; + border-radius: var(--banking-card-radius); } .banking-balance__label { @@ -3871,8 +3882,8 @@ button { .banking-actions { display: grid; grid-template-columns: repeat(2, 1fr); - gap: 9px; - margin: 13px 0 18px; + gap: 10px; + margin: 12px 0 var(--banking-section-gap); } .banking-action { @@ -3884,7 +3895,7 @@ button { min-height: 64px; overflow: hidden; padding: 11px 12px; - border-radius: 19px; + border-radius: var(--banking-card-radius); color: rgb(237 243 255 / 82%); font-size: 10px; } @@ -3933,7 +3944,8 @@ button { } .banking-card { - margin: 0 0 18px !important; + margin: 0 0 var(--banking-section-gap) !important; + border-radius: var(--banking-card-radius) !important; } .banking-transaction-card { @@ -3944,7 +3956,7 @@ button { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 11px; + margin-bottom: 10px; } .banking-section-title h2 { @@ -3969,10 +3981,10 @@ button { } .banking-transactions { - padding: 0 3px; + padding: 0; } .banking-transactions--all { - margin-top: 18px; + margin-top: var(--banking-section-gap); } .banking-transaction-list b { @@ -3989,7 +4001,7 @@ button { .banking-no-transactions { padding: 24px 12px; border: 1px solid rgb(255 255 255 / 8%); - border-radius: 22px; + border-radius: var(--banking-card-radius); background: rgb(255 255 255 / 3%); color: rgb(213 225 248 / 47%); font-size: 11px; @@ -4000,7 +4012,7 @@ button { display: flex; align-items: center; flex-direction: column; - padding: 16px 0 20px; + padding: 12px 0 var(--banking-section-gap); text-align: center; } @@ -4098,20 +4110,7 @@ button { .banking-sheet__content { position: relative; width: 100%; - padding: 22px 18px 18px; -} - -.banking-modal__close { - position: absolute; - top: 15px; - right: 15px; - display: grid; - width: 30px; - height: 30px; - place-items: center; - border-radius: 50%; - background: rgb(255 255 255 / 8%); - color: rgb(226 235 255 / 65%); + padding: 8px 18px calc(var(--sky-safe-area-bottom) + 18px); } .banking-modal__icon { @@ -4140,11 +4139,11 @@ button { } .banking-form-list { - margin: 12px 0 0 !important; + margin: var(--banking-section-gap) 0 0 !important; } .banking-contact-picker { - margin-top: 12px; + margin-top: var(--banking-section-gap); } .banking-contact-picker > span { @@ -4174,7 +4173,58 @@ button { align-items: center; justify-content: center; gap: 8px; - margin-top: 15px; + margin-top: var(--banking-section-gap); +} + +.banking-transaction-detail { + text-align: center; +} + +.banking-transaction-detail__icon { + margin: 0 auto; + background: linear-gradient(145deg, #d95867, #972937); +} + +.banking-transaction-detail__icon.is-incoming { + background: linear-gradient(145deg, #38c58d, #147454); + box-shadow: + 0 12px 28px rgb(20 116 84 / 32%), + inset 0 1px rgb(255 255 255 / 28%); +} + +.banking-transaction-detail > .banking-transaction-detail__eyebrow { + margin: 12px 0 0; + color: rgb(213 225 248 / 52%); + font-size: 10px; + font-weight: 650; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.banking-transaction-detail h2 { + margin-top: 4px; +} + +.banking-transaction-detail__amount { + display: block; + margin: 8px 0 18px; + color: #f2a0a8; + font-size: 30px; + font-weight: 650; + letter-spacing: -0.04em; +} + +.banking-transaction-detail__amount.is-incoming { + color: var(--bank-green); +} + +.banking-transaction-detail__list { + margin: 0 !important; + text-align: left; +} + +.banking-transaction-detail__list .sky-list-item__subtitle { + overflow-wrap: anywhere; } .phone-app--light .banking-app { @@ -4209,7 +4259,6 @@ button { .phone-app--light .banking-activity-hero small, .phone-app--light .banking-chart-legend, .phone-app--light .banking-chart__day span, -.phone-app--light .banking-modal__close, .phone-app--light .banking-sheet__content > p, .phone-app--light .banking-contact-picker > span, .phone-app--light .banking-contact-picker > p { @@ -4217,8 +4266,7 @@ button { } .phone-app--light .banking-balance__label small, -.phone-app--light .banking-no-transactions, -.phone-app--light .banking-modal__close { +.phone-app--light .banking-no-transactions { border-color: var(--sky-hairline); background: rgb(255 255 255 / 52%); } diff --git a/frontend/src/stores/banking.test.ts b/frontend/src/stores/banking.test.ts index bfea423..b70766b 100644 --- a/frontend/src/stores/banking.test.ts +++ b/frontend/src/stores/banking.test.ts @@ -61,7 +61,7 @@ describe('banking store', () => { expect(banking.error).toBe('insufficient_funds') }) - it('does not let an older response overwrite the newest overview', async () => { + it('does not let an older overview response overwrite a newer transfer', async () => { let resolveOlder!: (response: NuiResponse) => void const olderResponse = new Promise>( (resolve) => { @@ -75,7 +75,7 @@ describe('banking store', () => { const banking = useBankingStore() const olderRequest = banking.load() - await banking.load() + await banking.perform('transfer', 1787, '5551234567') resolveOlder({ data: { ...overview, bank: 1 }, success: true }) await olderRequest @@ -83,15 +83,62 @@ describe('banking store', () => { expect(banking.isLoading).toBe(false) }) - it('blocks every banking request while a reload cooldown is active', async () => { + it('coalesces concurrent overview reloads into one NUI request', async () => { + let resolveLoad!: (response: NuiResponse) => void + mockNuiCall.mockReturnValueOnce( + new Promise>((resolve) => { + resolveLoad = resolve + }), + ) + const banking = useBankingStore() + + const firstLoad = banking.load() + const secondLoad = banking.load() + resolveLoad({ data: overview, success: true }) + + expect(await firstLoad).toBe(true) + expect(await secondLoad).toBe(true) + expect(mockNuiCall).toHaveBeenCalledTimes(1) + }) + + it('queues one fresh overview after a server-side balance change', async () => { + let resolveActive!: (response: NuiResponse) => void + mockNuiCall + .mockReturnValueOnce( + new Promise>((resolve) => { + resolveActive = resolve + }), + ) + .mockResolvedValueOnce({ + data: { ...overview, bank: overview.bank + 500 }, + success: true, + }) + const banking = useBankingStore() + + const activeLoad = banking.load() + const changedLoad = banking.load(false, true) + const duplicateChangedLoad = banking.load(false, true) + resolveActive({ data: overview, success: true }) + + await activeLoad + expect(await changedLoad).toBe(true) + expect(await duplicateChangedLoad).toBe(true) + expect(banking.overview?.bank).toBe(overview.bank + 500) + expect(mockNuiCall).toHaveBeenCalledTimes(2) + }) + + it('limits manual refreshes without blocking automatic loads or transfers', async () => { const banking = useBankingStore() banking.cooldownUntil = Date.now() + 10_000 + mockNuiCall + .mockResolvedValueOnce({ data: overview, success: true }) + .mockResolvedValueOnce({ data: overview, success: true }) - expect(await banking.load()).toBe(false) - expect(await banking.perform('transfer', 100, '5551234567')).toEqual({ - error: 'reload_cooldown', - success: false, - }) - expect(mockNuiCall).not.toHaveBeenCalled() + expect(await banking.load(true)).toBe(false) + expect(await banking.load()).toBe(true) + expect((await banking.perform('transfer', 100, '5551234567')).success).toBe( + true, + ) + expect(mockNuiCall).toHaveBeenCalledTimes(2) }) }) diff --git a/frontend/src/stores/banking.ts b/frontend/src/stores/banking.ts index fd5be78..3e74175 100644 --- a/frontend/src/stores/banking.ts +++ b/frontend/src/stores/banking.ts @@ -8,6 +8,9 @@ import { RELOAD_COOLDOWN_ERROR, } from '@/utils/reload-cooldown' +let activeOverviewLoad: Promise | null = null +let queuedOverviewLoad: Promise | null = null + export const useBankingStore = defineStore('banking', { state: () => ({ error: '', @@ -19,41 +22,59 @@ export const useBankingStore = defineStore('banking', { reloadAttempts: [] as number[], }), actions: { - async load(manualReload = false): Promise { + async load(manualReload = false, ensureFresh = false): Promise { + if (activeOverviewLoad) { + if (!ensureFresh) return activeOverviewLoad + if (!queuedOverviewLoad) { + const activeRequest = activeOverviewLoad + const queuedRequest = activeRequest.then( + () => this.load(), + () => this.load(), + ) + const trackedQueuedRequest = queuedRequest.finally(() => { + queuedOverviewLoad = null + }) + queuedOverviewLoad = trackedQueuedRequest + } + return queuedOverviewLoad + } if ( - isReloadCooldownActive(this) || - (manualReload && !allowManualReload(this)) + manualReload && + (isReloadCooldownActive(this) || !allowManualReload(this)) ) { this.error = RELOAD_COOLDOWN_ERROR return false } - const generation = ++this.requestGeneration - this.pendingRequests += 1 - this.isLoading = true - const response = await nuiCall('banking:overview').finally( - () => { + const request = (async () => { + const generation = ++this.requestGeneration + this.pendingRequests += 1 + this.isLoading = true + const response = await nuiCall( + 'banking:overview', + ).finally(() => { this.pendingRequests = Math.max(0, this.pendingRequests - 1) this.isLoading = this.pendingRequests > 0 - }, - ) - if (generation !== this.requestGeneration) return response.success - if (response.success && response.data) { - this.overview = response.data - this.error = '' - return true - } - this.error = response.error ?? 'request_failed' - return false + }) + if (generation !== this.requestGeneration) return response.success + if (response.success && response.data) { + this.overview = response.data + this.error = '' + return true + } + this.error = response.error ?? 'request_failed' + return false + })() + const trackedRequest = request.finally(() => { + activeOverviewLoad = null + }) + activeOverviewLoad = trackedRequest + return trackedRequest }, async perform( action: BankingAction, amount: number, phoneNumber?: string, ): Promise> { - if (isReloadCooldownActive(this)) { - this.error = RELOAD_COOLDOWN_ERROR - return { error: RELOAD_COOLDOWN_ERROR, success: false } - } const generation = ++this.requestGeneration this.pendingRequests += 1 this.isLoading = true diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index ad69657..475aa8e 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -2284,6 +2284,14 @@ const defaultLocales: LocaleTree = { noContacts: 'No contacts saved yet.', amount: 'Amount', amountPlaceholder: 'Enter an amount', + transactionDetails: 'Transaction details', + transactionDate: 'Date', + transactionDirection: 'Direction', + transactionReference: 'Reference', + notifications: { + receivedTitle: 'Money received', + received: 'You received {amount} from {sender}.', + }, transactions: { deposit: 'Cash deposit', withdrawal: 'Cash withdrawal', diff --git a/frontend/src/utils/bankingAmount.test.ts b/frontend/src/utils/bankingAmount.test.ts new file mode 100644 index 0000000..fc0dfbc --- /dev/null +++ b/frontend/src/utils/bankingAmount.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' + +import { + normalizeBankingAmountInput, + parseBankingAmount, +} from '@/utils/bankingAmount' + +describe('banking amount input', () => { + it('uses a decimal comma and strips native number-input noise', () => { + expect(normalizeBankingAmountInput('1.')).toBe('1,') + expect(normalizeBankingAmountInput('12.00')).toBe('12,00') + expect(normalizeBankingAmountInput('EUR 12,00')).toBe('12,00') + expect(normalizeBankingAmountInput('12,0,0')).toBe('12,00') + }) + + it('keeps the server-authoritative whole-money contract', () => { + expect(parseBankingAmount('125')).toBe(125) + expect(parseBankingAmount('125,00')).toBe(125) + expect(parseBankingAmount('125,50')).toBeNull() + expect(parseBankingAmount('0')).toBeNull() + expect(parseBankingAmount('')).toBeNull() + }) +}) diff --git a/frontend/src/utils/bankingAmount.ts b/frontend/src/utils/bankingAmount.ts new file mode 100644 index 0000000..d1673a9 --- /dev/null +++ b/frontend/src/utils/bankingAmount.ts @@ -0,0 +1,24 @@ +export function normalizeBankingAmountInput(value: string): string { + const normalizedSeparators = value.replace(/\./g, ',') + const digitsAndSeparators = normalizedSeparators.replace(/[^\d,]/g, '') + const separatorIndex = digitsAndSeparators.indexOf(',') + + if (separatorIndex < 0) return digitsAndSeparators + + const whole = digitsAndSeparators.slice(0, separatorIndex) + const decimal = digitsAndSeparators + .slice(separatorIndex + 1) + .replace(/,/g, '') + .slice(0, 2) + + return `${whole},${decimal}` +} + +export function parseBankingAmount(value: string): number | null { + if (!/^\d+(?:,\d{1,2})?$/.test(value)) return null + + const amount = Number(value.replace(',', '.')) + if (!Number.isSafeInteger(amount) || amount <= 0) return null + + return amount +} diff --git a/frontend/src/views/apps/BankingApp.contract.test.ts b/frontend/src/views/apps/BankingApp.contract.test.ts index 2587e7c..657fb26 100644 --- a/frontend/src/views/apps/BankingApp.contract.test.ts +++ b/frontend/src/views/apps/BankingApp.contract.test.ts @@ -6,6 +6,22 @@ const source = readFileSync( new URL('./BankingApp.vue', import.meta.url), 'utf8', ) +const styles = readFileSync( + new URL('../../assets/main.css', import.meta.url), + 'utf8', +) +const appSource = readFileSync( + new URL('../../App.vue', import.meta.url), + 'utf8', +) +const clientSource = readFileSync( + new URL('../../../../sky_phone/source/client/main.lua', import.meta.url), + 'utf8', +) +const serverSource = readFileSync( + new URL('../../../../sky_phone/source/server/banking.lua', import.meta.url), + 'utf8', +) describe('Banking app Sky UI migration', () => { it('uses Sky UI instead of Konsta components', () => { @@ -37,4 +53,60 @@ describe('Banking app Sky UI migration', () => { expect(source).not.toContain('handleSheetKeydown') expect(source).not.toContain('handleWindowKeydown') }) + + it('uses a comma-first text amount field without native stepper arrows', () => { + expect(source).toContain('class="banking-amount-field"') + expect(source).toContain('inputmode="decimal"') + expect(source).toContain('type="text"') + expect(source).not.toMatch( + /input-id="banking-transfer-amount"[\s\S]*?type="number"/, + ) + expect(source).toContain('normalizeBankingAmountInput') + expect(source).toContain('parseBankingAmount') + }) + + it('turns received phone transfers into routed Banking notifications', () => { + expect(serverSource).toContain('kind = "transfer_in"') + expect(serverSource).toContain('currency = Config.Banking.Currency') + expect(clientSource).toContain( + 'SendNUIMessage({ type = "banking:changed", data = data })', + ) + expect(appSource).toContain("data?.kind === 'transfer_in'") + expect(appSource).toContain("appId: 'banking'") + expect(appSource).toContain("route: '/apps/banking'") + expect(appSource).toContain("phone.t('Apps.banking.notifications.received'") + }) + + it('resets tab scroll and locks the background behind sheets', () => { + expect(source).toContain('async function selectTab(nextTab: BankingTab)') + expect(source).toContain('bankingScroll.value.scrollTop = 0') + expect(source).toContain(':class="{ \'is-locked\': overlayOpened }"') + expect(source).toContain('@click="selectTab(\'activity\')"') + expect(source).toContain('@click="selectTab(\'home\')"') + expect(styles).toMatch( + /\.banking-scroll\s*\{[^}]*overscroll-behavior-y:\s*contain;[^}]*touch-action:\s*pan-y;/s, + ) + expect(styles).toMatch( + /\.banking-scroll\.is-locked\s*\{[^}]*overflow-y:\s*hidden;[^}]*touch-action:\s*none;/s, + ) + }) + + it('opens every transaction in an accessible Sky detail sheet', () => { + expect(source).toContain( + 'const selectedTransaction = ref(null)', + ) + expect(source).toContain( + 'function openTransaction(transaction: BankingTransaction)', + ) + expect(source).toContain('@click="openTransaction(transaction)"') + expect(source).toContain('link-component="button"') + expect(source).toContain(':opened="overlayOpened"') + expect(source).toContain('swipe-to-close') + expect(source).toContain('grabber-clickable') + expect(source).toContain('@swipeclose="closeAction"') + expect(source).not.toContain('banking-modal__close') + expect(source).toContain('id="banking-transaction-detail-title"') + expect(source).toContain('banking-transaction-detail__amount') + expect(source).toContain("phone.t('Apps.banking.transactionReference')") + }) }) diff --git a/frontend/src/views/apps/BankingApp.vue b/frontend/src/views/apps/BankingApp.vue index c5d7118..60278e3 100644 --- a/frontend/src/views/apps/BankingApp.vue +++ b/frontend/src/views/apps/BankingApp.vue @@ -10,7 +10,6 @@ import { Landmark, Send, WalletCards, - X, } from 'lucide-vue-next' import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' @@ -23,6 +22,10 @@ import type { BankingTransactionKind, } from '@/types/banking' import type { PhoneContact } from '@/types/phone' +import { + normalizeBankingAmountInput, + parseBankingAmount, +} from '@/utils/bankingAmount' import { handleEnterAction } from '@/utils/keyboard' import { formatPhoneNumber, normalizePhoneNumber } from '@/utils/phone' import { @@ -50,6 +53,7 @@ const banking = useBankingStore() const calls = useCallsStore() const activeTab = ref('home') const action = ref(null) +const selectedTransaction = ref(null) const amount = ref('') const target = ref('') const formError = ref('') @@ -57,6 +61,9 @@ const bankingScroll = ref(null) const isRefreshing = ref(false) const pullDistance = ref(0) const cooldownToastOpened = ref(false) +const overlayOpened = computed(() => + Boolean(action.value || selectedTransaction.value), +) const pullThreshold = 56 let pullStartY = 0 @@ -145,11 +152,15 @@ function transactionTitle(transaction: BankingTransaction): string { return phone.t(`Apps.banking.transactions.${transaction.kind}`) } +async function selectTab(nextTab: BankingTab): Promise { + if (activeTab.value === nextTab) return + activeTab.value = nextTab + await nextTick() + if (bankingScroll.value) bankingScroll.value.scrollTop = 0 +} + function openAction(nextAction: BankingAction): void { - if (banking.cooldownUntil > Date.now()) { - banking.error = 'reload_cooldown' - return - } + selectedTransaction.value = null action.value = nextAction amount.value = '' target.value = '' @@ -158,8 +169,14 @@ function openAction(nextAction: BankingAction): void { } function closeAction(): void { - if (banking.isLoading) return + if (action.value && banking.isLoading) return action.value = null + selectedTransaction.value = null +} + +function openTransaction(transaction: BankingTransaction): void { + action.value = null + selectedTransaction.value = transaction } function updateTarget(event: Event): void { @@ -186,7 +203,11 @@ function updateAmount(event: Event): void { console.error('[banking] Amount input emitted without an input target.') return } - amount.value = event.target.value + const normalizedAmount = normalizeBankingAmountInput(event.target.value) + if (event.target.value !== normalizedAmount) { + event.target.value = normalizedAmount + } + amount.value = normalizedAmount formError.value = '' } @@ -248,14 +269,10 @@ function errorMessage(code: string): string { async function submitAction(): Promise { if (!action.value) return - const parsedAmount = Number(amount.value) + const parsedAmount = parseBankingAmount(amount.value) const phoneNumber = action.value === 'transfer' ? normalizePhoneNumber(target.value) : undefined - if ( - !Number.isSafeInteger(parsedAmount) || - parsedAmount <= 0 || - (action.value === 'transfer' && !phoneNumber) - ) { + if (parsedAmount === null || (action.value === 'transfer' && !phoneNumber)) { formError.value = phone.t('Apps.banking.errors.invalid_request') return } @@ -310,8 +327,8 @@ onBeforeUnmount(() => { @@ -319,8 +336,8 @@ onBeforeUnmount(() => {
{{ phone.t('Common.loading') }} @@ -329,9 +346,9 @@ onBeforeUnmount(() => { @@ -346,8 +363,9 @@ onBeforeUnmount(() => { v-else ref="bankingScroll" class="banking-scroll" - :aria-hidden="Boolean(action)" - :inert="Boolean(action)" + :class="{ 'is-locked': overlayOpened }" + :aria-hidden="overlayOpened" + :inert="overlayOpened" @touchend="finishPull" @touchmove.passive="movePull" @touchstart.passive="startPull" @@ -418,7 +436,7 @@ onBeforeUnmount(() => { {{ phone.t('Apps.banking.viewAll') }} @@ -434,6 +452,9 @@ onBeforeUnmount(() => { :key="transaction.id" :subtitle="formatDate(transaction.createdAt)" :title="transactionTitle(transaction)" + link + link-component="button" + @click="openTransaction(transaction)" >