Add payment methods management page
List Stripe payment methods with set-default and remove actions. Includes delete confirmation dialog, breadcrumb navigation, and sidebar nav entry. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,35 @@ class BillingController extends Controller
|
|||||||
return back()->with('success', 'Default payment method updated.');
|
return back()->with('success', 'Default payment method updated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function paymentMethods(Request $request): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$paymentMethods = [];
|
||||||
|
$defaultPaymentMethod = null;
|
||||||
|
|
||||||
|
if ($user->hasStripeId()) {
|
||||||
|
$methods = $user->paymentMethods();
|
||||||
|
$defaultPm = $user->defaultPaymentMethod();
|
||||||
|
$defaultPaymentMethod = $defaultPm?->id;
|
||||||
|
|
||||||
|
foreach ($methods as $method) {
|
||||||
|
$paymentMethods[] = [
|
||||||
|
'id' => $method->id,
|
||||||
|
'brand' => $method->card->brand ?? 'unknown',
|
||||||
|
'last_four' => $method->card->last_four ?? '****',
|
||||||
|
'exp_month' => $method->card->exp_month,
|
||||||
|
'exp_year' => $method->card->exp_year,
|
||||||
|
'is_default' => $method->id === $defaultPaymentMethod,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inertia::render('Billing/PaymentMethods', [
|
||||||
|
'paymentMethods' => $paymentMethods,
|
||||||
|
'defaultPaymentMethod' => $defaultPaymentMethod,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function invoices(Request $request): Response
|
public function invoices(Request $request): Response
|
||||||
{
|
{
|
||||||
$invoices = $request->user()
|
$invoices = $request->user()
|
||||||
|
|||||||
229
website/resources/ts/Pages/Billing/PaymentMethods.vue
Normal file
229
website/resources/ts/Pages/Billing/PaymentMethods.vue
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useForm, Link } from '@inertiajs/vue3'
|
||||||
|
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||||
|
import type { PaymentMethod } from '@/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
paymentMethods: PaymentMethod[]
|
||||||
|
defaultPaymentMethod: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({ layout: AccountLayout })
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const defaultForm = useForm({
|
||||||
|
payment_method_id: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteForm = useForm({
|
||||||
|
payment_method_id: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const showDeleteDialog = ref<boolean>(false)
|
||||||
|
const deletingMethod = ref<PaymentMethod | null>(null)
|
||||||
|
|
||||||
|
function setDefault(id: string): void {
|
||||||
|
defaultForm.payment_method_id = id
|
||||||
|
defaultForm.post('/billing/payment-methods/default')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteDialog(pm: PaymentMethod): void {
|
||||||
|
deletingMethod.value = pm
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(): void {
|
||||||
|
if (!deletingMethod.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteForm.payment_method_id = deletingMethod.value.id
|
||||||
|
deleteForm.delete('/billing/payment-methods/' + deletingMethod.value.id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
deletingMethod.value = null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBrandIcon(brand: string): string {
|
||||||
|
const brandMap: Record<string, string> = {
|
||||||
|
visa: 'tabler-brand-visa',
|
||||||
|
mastercard: 'tabler-brand-mastercard',
|
||||||
|
amex: 'tabler-credit-card',
|
||||||
|
discover: 'tabler-credit-card',
|
||||||
|
diners: 'tabler-credit-card',
|
||||||
|
jcb: 'tabler-credit-card',
|
||||||
|
unionpay: 'tabler-credit-card',
|
||||||
|
}
|
||||||
|
|
||||||
|
return brandMap[brand.toLowerCase()] || 'tabler-credit-card'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div class="d-flex align-center ga-2 mb-4">
|
||||||
|
<Link href="/billing" class="text-decoration-none">
|
||||||
|
<VBtn variant="text" size="small" color="primary">
|
||||||
|
<VIcon icon="tabler-arrow-left" start />
|
||||||
|
Billing
|
||||||
|
</VBtn>
|
||||||
|
</Link>
|
||||||
|
<VIcon icon="tabler-chevron-right" size="16" color="disabled" />
|
||||||
|
<span class="text-body-2 text-medium-emphasis">Payment Methods</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-h4 font-weight-bold mb-6">Payment Methods</div>
|
||||||
|
|
||||||
|
<!-- Payment Methods List -->
|
||||||
|
<VCard class="mb-6">
|
||||||
|
<VCardTitle>Your Cards</VCardTitle>
|
||||||
|
<VCardText>
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div v-if="paymentMethods.length === 0" class="text-center py-8">
|
||||||
|
<VIcon icon="tabler-credit-card-off" size="48" color="disabled" class="mb-4" />
|
||||||
|
<div class="text-body-1 text-medium-emphasis mb-2">No payment methods on file</div>
|
||||||
|
<div class="text-body-2 text-disabled">
|
||||||
|
Payment methods are added automatically during checkout.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payment Methods -->
|
||||||
|
<div v-else class="d-flex flex-column ga-3">
|
||||||
|
<VSheet
|
||||||
|
v-for="pm in paymentMethods"
|
||||||
|
:key="pm.id"
|
||||||
|
rounded
|
||||||
|
border
|
||||||
|
class="pa-4 d-flex align-center justify-space-between"
|
||||||
|
:class="pm.is_default ? 'border-primary' : ''"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center ga-3">
|
||||||
|
<VAvatar
|
||||||
|
:icon="resolveBrandIcon(pm.brand)"
|
||||||
|
size="40"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<span class="text-body-1 font-weight-medium text-capitalize">{{ pm.brand }}</span>
|
||||||
|
<span class="text-body-2 text-medium-emphasis">ending in {{ pm.last_four }}</span>
|
||||||
|
<VChip v-if="pm.is_default" color="primary" size="x-small">Default</VChip>
|
||||||
|
</div>
|
||||||
|
<div class="text-body-2 text-disabled mt-1">
|
||||||
|
Expires {{ pm.exp_month }}/{{ pm.exp_year }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<VBtn
|
||||||
|
v-if="!pm.is_default"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="defaultForm.processing && defaultForm.payment_method_id === pm.id"
|
||||||
|
:disabled="defaultForm.processing"
|
||||||
|
prepend-icon="tabler-star"
|
||||||
|
@click="setDefault(pm.id)"
|
||||||
|
>
|
||||||
|
Set as Default
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
variant="tonal"
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
prepend-icon="tabler-trash"
|
||||||
|
@click="openDeleteDialog(pm)"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</VSheet>
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
|
<!-- Add New Card Info -->
|
||||||
|
<VCard>
|
||||||
|
<VCardTitle>Add a New Card</VCardTitle>
|
||||||
|
<VCardText>
|
||||||
|
<VAlert type="info" variant="tonal">
|
||||||
|
<div class="text-body-2">
|
||||||
|
To add a new payment method, please use the checkout flow when purchasing a new plan,
|
||||||
|
or contact our support team for assistance.
|
||||||
|
</div>
|
||||||
|
</VAlert>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<VDialog v-model="showDeleteDialog" max-width="450">
|
||||||
|
<VCard>
|
||||||
|
<VCardTitle class="d-flex align-center ga-2">
|
||||||
|
<VIcon icon="tabler-alert-triangle" color="error" />
|
||||||
|
Remove Payment Method
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardText>
|
||||||
|
<p class="text-body-1 mb-4">
|
||||||
|
Are you sure you want to remove this payment method?
|
||||||
|
</p>
|
||||||
|
<VSheet
|
||||||
|
v-if="deletingMethod"
|
||||||
|
rounded
|
||||||
|
border
|
||||||
|
class="pa-3 d-flex align-center ga-3"
|
||||||
|
>
|
||||||
|
<VAvatar
|
||||||
|
:icon="resolveBrandIcon(deletingMethod.brand)"
|
||||||
|
size="36"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div class="text-body-2 font-weight-medium text-capitalize">
|
||||||
|
{{ deletingMethod.brand }} ending in {{ deletingMethod.last_four }}
|
||||||
|
</div>
|
||||||
|
<div class="text-caption text-disabled">
|
||||||
|
Expires {{ deletingMethod.exp_month }}/{{ deletingMethod.exp_year }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VSheet>
|
||||||
|
|
||||||
|
<VAlert
|
||||||
|
v-if="deletingMethod?.is_default"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<div class="text-body-2">
|
||||||
|
This is your default payment method. Removing it may affect active subscriptions.
|
||||||
|
</div>
|
||||||
|
</VAlert>
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions class="pa-4 pt-0">
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
variant="outlined"
|
||||||
|
@click="showDeleteDialog = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</VBtn>
|
||||||
|
<VBtn
|
||||||
|
color="error"
|
||||||
|
:loading="deleteForm.processing"
|
||||||
|
@click="confirmDelete"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -8,6 +8,7 @@ export const accountNavItems: VerticalNavItems = [
|
|||||||
|
|
||||||
{ heading: 'Billing' },
|
{ heading: 'Billing' },
|
||||||
{ title: 'Billing', to: '/billing', icon: 'tabler-credit-card' },
|
{ title: 'Billing', to: '/billing', icon: 'tabler-credit-card' },
|
||||||
|
{ title: 'Payment Methods', to: '/billing/payment-methods', icon: 'tabler-wallet' },
|
||||||
{ title: 'Plans', to: '/plans', icon: 'tabler-package' },
|
{ title: 'Plans', to: '/plans', icon: 'tabler-package' },
|
||||||
|
|
||||||
{ heading: 'Support' },
|
{ heading: 'Support' },
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Route::get('/billing', [BillingController::class, 'index'])->name('account.billi
|
|||||||
Route::post('/billing/payment-methods', [BillingController::class, 'addPaymentMethod'])->name('account.billing.add-payment-method');
|
Route::post('/billing/payment-methods', [BillingController::class, 'addPaymentMethod'])->name('account.billing.add-payment-method');
|
||||||
Route::delete('/billing/payment-methods/{paymentMethod}', [BillingController::class, 'removePaymentMethod'])->name('account.billing.remove-payment-method');
|
Route::delete('/billing/payment-methods/{paymentMethod}', [BillingController::class, 'removePaymentMethod'])->name('account.billing.remove-payment-method');
|
||||||
Route::post('/billing/payment-methods/default', [BillingController::class, 'setDefaultPaymentMethod'])->name('account.billing.set-default-payment-method');
|
Route::post('/billing/payment-methods/default', [BillingController::class, 'setDefaultPaymentMethod'])->name('account.billing.set-default-payment-method');
|
||||||
|
Route::get('/billing/payment-methods', [BillingController::class, 'paymentMethods'])->name('account.billing.payment-methods');
|
||||||
Route::get('/billing/invoices', [BillingController::class, 'invoices'])->name('account.billing.invoices');
|
Route::get('/billing/invoices', [BillingController::class, 'invoices'])->name('account.billing.invoices');
|
||||||
Route::get('/billing/invoices/{invoice}/download', [BillingController::class, 'downloadInvoice'])->name('account.billing.invoices.download');
|
Route::get('/billing/invoices/{invoice}/download', [BillingController::class, 'downloadInvoice'])->name('account.billing.invoices.download');
|
||||||
Route::get('/billing/transactions', [BillingController::class, 'transactions'])->name('account.billing.transactions');
|
Route::get('/billing/transactions', [BillingController::class, 'transactions'])->name('account.billing.transactions');
|
||||||
|
|||||||
Reference in New Issue
Block a user