fix: Delete pending connection and show toast on cancelled bank authorization (#111)

## Summary
- When a user cancels the bank authorization flow (e.g. clicks cancel on
the bank's page), the pending `BankingConnection` is now soft-deleted so
it doesn't remain stuck in `pending` status
- Flash messages (`success`/`error`) are now shared with the frontend
via Inertia shared data
- The connections settings page shows a toast notification with the
error message on redirect

## Test plan
- [ ] Start a bank connection flow, cancel on the bank's authorization
page, and verify:
  - A toast error appears on the connections page
  - The pending connection is removed from the list
- [x] Complete a successful bank connection and verify the success toast
appears
- [x] Run `php artisan test
tests/Feature/OpenBanking/AuthorizationControllerTest.php`
This commit is contained in:
Víctor Falcón 2026-02-12 11:10:15 +01:00 committed by GitHub
parent 03fec11705
commit c7f3f1a978
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 36 additions and 3 deletions

View File

@ -60,6 +60,12 @@ class AuthorizationController extends Controller
'description' => $request->query('error_description'),
]);
auth()->user()->bankingConnections()
->where('status', BankingConnectionStatus::Pending)
->latest()
->first()
?->delete();
return redirect()->route('settings.connections.index')
->with('error', $request->query('error_description', 'Authorization was denied or cancelled.'));
}

View File

@ -45,6 +45,10 @@ class HandleInertiaRequests extends Middleware
return [
...parent::share($request),
'flash' => [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
],
'name' => config('app.name'),
'appUrl' => config('app.url'),
'version' => json_decode(file_get_contents(base_path('package.json')))->version ?? '0.0.0',

View File

@ -25,13 +25,14 @@ import { __ } from '@/utils/i18n';
import { Head, router, usePage, usePoll } from '@inertiajs/react';
import { ArrowRight, MoreHorizontal, RefreshCw, Unplug } from 'lucide-react';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
interface Props {
connections: BankingConnection[];
}
export default function ConnectionsPage({ connections }: Props) {
const { auth } = usePage<SharedData>().props;
const { auth, flash } = usePage<SharedData>().props;
const isDemoAccount = auth?.isDemoAccount ?? false;
const [connectDialogOpen, setConnectDialogOpen] = useState(false);
const [disconnectConnection, setDisconnectConnection] =
@ -43,6 +44,15 @@ export default function ConnectionsPage({ connections }: Props) {
const { start, stop } = usePoll(5000, {}, { autoStart: false });
useEffect(() => {
if (flash?.error) {
toast.error(flash.error);
}
if (flash?.success) {
toast.success(flash.success);
}
}, [flash?.error, flash?.success]);
useEffect(() => {
if (hasSyncing) {
start();

View File

@ -45,12 +45,18 @@ export interface Features {
'account-mapping': boolean;
}
export interface Flash {
success: string | null;
error: string | null;
}
export interface SharedData {
name: string;
appUrl: string;
version: string;
quote: { message: string; author: string };
auth: Auth;
flash: Flash;
subscriptionsEnabled: boolean;
pricing: PricingConfig;
sidebarOpen: boolean;

View File

@ -57,15 +57,22 @@ test('authorization requires aspsp_name and country', function () {
$response->assertJsonValidationErrors(['aspsp_name', 'country']);
});
test('callback with error redirects with error message', function () {
test('callback with error redirects with error message and deletes pending connection', function () {
$user = User::factory()->onboarded()->create();
Feature::for($user)->activate('open-banking');
$connection = BankingConnection::factory()->pending()->create([
'user_id' => $user->id,
]);
$response = $this->actingAs($user)
->get('/open-banking/callback?error=access_denied&error_description=User+denied+access');
$response->assertRedirect(route('settings.connections.index'));
$response->assertSessionHas('error');
$response->assertSessionHas('error', 'User denied access');
$connection->refresh();
expect($connection->trashed())->toBeTrue();
});
test('callback without code redirects with error', function () {