This commit is contained in:
Nyavokevin 2025-10-14 17:50:12 +03:00
parent ab3690e951
commit 83ed2ba44c
22 changed files with 2372 additions and 127 deletions

View File

@ -63,3 +63,20 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
# Stripe Payment Configuration
STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY=
STRIPE_WEBHOOK_SECRET=
VITE_STRIPE_PUBLISHABLE_KEY="${STRIPE_PUBLISHABLE_KEY}"
# Wise Payment Configuration (Simplified - Using Payment Links)
# Get your payment links from your Wise business account
# Each link is for a specific product/amount
WISE_PAYMENT_LINK_6_CARDS=https://wise.com/pay/r/W2k1NqQySdc9HW8
WISE_PAYMENT_LINK_18_CARDS=https://wise.com/pay/r/YOUR_18_CARDS_LINK
# Wise Webhook Configuration (for payment verification)
# Create webhook at: https://wise.com/settings/webhooks
# Point it to: https://yourdomain.com/wise/webhook
WISE_WEBHOOK_SECRET=

143
README_WISE.md Normal file
View File

@ -0,0 +1,143 @@
# Wise Payment Integration - README
## 🎉 Implementation Complete!
Your Wise payment integration is **ready to use** with the payment link your client provided.
## 📋 What You Have
A **simplified Wise integration** that:
- ✅ Redirects users to Wise payment links
- ✅ Tracks payment status in database
- ✅ Verifies payments before allowing card access
- ✅ Supports webhook notifications (optional)
- ✅ Works alongside existing Stripe integration
## 🚀 Quick Start
### 1. Add to `.env`:
```bash
WISE_PAYMENT_LINK_6_CARDS=https://wise.com/pay/r/W2k1NqQySdc9HW8
WISE_PAYMENT_LINK_18_CARDS=https://wise.com/pay/r/YOUR_OTHER_LINK
WISE_WEBHOOK_SECRET=
```
### 2. That's It!
Users can now:
1. Select a paid card option
2. Click "Wise" button (dark blue)
3. Pay on Wise
4. Return and verify payment
## 📚 Documentation
- **WISE_SIMPLE_SETUP.md****START HERE** for step-by-step guide
- **WISE_SETUP.md** - Detailed technical setup
- **WISE_FLOW_DIAGRAM.md** - Visual flow diagrams
- **WISE_IMPLEMENTATION_SUMMARY.md** - What was built
## 🎯 Key Features
### For Users
- Dual payment buttons: Stripe or Wise
- Seamless redirect to Wise
- Payment verification page
- Access to card drawing after payment
### For You
- Simple configuration (just payment links)
- Database tracking of all payments
- Webhook support for automatic verification
- Easy testing without real payments
## 🔧 Technical Details
### Routes
- `POST /create-wise-payment` - Create payment & redirect
- `GET /wise/verify` - Verification page
- `POST /wise/webhook` - Webhook handler
- `GET /wise/validate-payment` - Check payment status
### Database
All payments stored in `payments` table with:
- `payment_provider` = 'wise'
- `status` = 'pending' | 'succeeded' | 'failed'
- `client_session_id` = unique identifier
### Security
- Payment verification before card access
- Webhook signature validation
- CSRF protection disabled for webhooks only
## 🧪 Testing
### Without Real Payment
```bash
php artisan tinker
# Get latest Wise payment
$payment = App\Models\Payment::where('payment_provider', 'wise')
->latest()->first();
# Mark as succeeded
$payment->update(['status' => 'succeeded']);
```
### With Real Payment
1. Click "Wise" button
2. Complete payment on Wise
3. Visit `/wise/verify`
4. System checks and confirms payment
## 📱 User Flow
```
Card Selection → Click Wise → Redirect to Wise → Complete Payment
Payment Record Created (status: pending)
User Returns → /wise/verify → Status Check
If Paid → Success → Draw Cards!
```
## 🛠 Configuration
### Required
- `WISE_PAYMENT_LINK_6_CARDS` - Link for 6-card option
- `WISE_PAYMENT_LINK_18_CARDS` - Link for 18-card option
### Optional (for automatic verification)
- `WISE_WEBHOOK_SECRET` - Webhook signature verification
## 💡 Tips
1. **Ask your client** for the 18-card payment link
2. **Set up webhooks** in production for automatic verification
3. **Test manually** first with the simulated payment method
4. **Check logs** if something doesn't work: `storage/logs/laravel.log`
## 🆘 Common Issues
### "Payment link not configured"
→ Add `WISE_PAYMENT_LINK_6_CARDS` to `.env`
### Payment stuck on pending
→ Use `php artisan tinker` to manually mark as succeeded (for testing)
### Webhook not firing
→ Check webhook URL is publicly accessible and secret is configured
## ✨ Next Steps
1. ✅ Code is ready
2. 📝 Add payment links to `.env`
3. 🧪 Test the flow
4. 🔗 Set up webhooks (production)
5. 🎉 Go live!
---
**Read WISE_SIMPLE_SETUP.md for the complete guide!**

282
WISE_FLOW_DIAGRAM.md Normal file
View File

@ -0,0 +1,282 @@
# Wise Payment Integration - Flow Diagram
## Complete User Journey
```
┌─────────────────────────────────────────────────────────────────┐
│ User Visits Card Selection │
│ (cardSelection.vue) │
└──────────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Three Options Displayed: │
│ │
│ ┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Free Draw │ │ Profilage (6) │ │ Quadrige (18) │ │
│ │ Gratuit │ │ 9.99€ │ │ 15.90€ │ │
│ │ │ │ [Stripe][Wise] │ │ [Stripe][Wise] │ │
│ └──────┬───────┘ └────────┬────────┘ └────────┬─────────┘ │
└─────────┼──────────────────┼────────────────────┼──────────────┘
│ │ │
│ (Free) │ (Paid) │ (Paid)
│ │ │
▼ ▼ ▼
Proceed to Click "Wise" Click "Wise"
Card Drawing Button Button
│ │ │
│ └────────┬───────────┘
│ │
│ ▼
│ ┌────────────────────────────────┐
│ │ POST /create-wise-payment │
│ │ WiseController::create...() │
│ └────────────┬───────────────────┘
│ │
│ ▼
│ ┌────────────────────────────────┐
│ │ Wise API Calls: │
│ │ 1. Create Quote │
│ │ 2. Create Recipient │
│ │ 3. Create Transfer │
│ └────────────┬───────────────────┘
│ │
│ ▼
│ ┌────────────────────────────────┐
│ │ Save to Database: │
│ │ - payment_provider: 'wise' │
│ │ - status: 'pending' │
│ │ - wise_payment_id: 12345 │
│ │ - client_session_id: UUID │
│ └────────────┬───────────────────┘
│ │
│ ▼
│ ┌────────────────────────────────┐
│ │ Redirect User to: │
│ │ Wise Payment Page │
│ │ (or Pending page) │
│ └────────────┬───────────────────┘
│ │
│ ┌───────────┴───────────┐
│ │ │
│ ▼ ▼
│ User Completes User on Pending
│ Payment on Wise Page (polls status)
│ │ │
│ │ │ (auto-refresh
│ │ │ every 10s)
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Wise Processes │ │
│ │ Payment │ │
│ └─────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ POST /wise/ │ │
│ │ webhook │ │
│ │ (from Wise) │ │
│ └─────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ WiseController:: │ │
│ │ handleWebhook() │ │
│ │ 1. Verify signature │ │
│ │ 2. Check event type │ │
│ │ 3. Update payment │ │
│ └─────────┬────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Database Update: │ │
│ │ status: 'succeeded' │ │
│ └─────────┬────────────────┘ │
│ │ │
│ │◄───────────────────┘
│ │ (polling detects
│ │ success)
│ │
│ ▼
│ ┌──────────────────────────┐
│ │ Redirect to: │
│ │ /success?client_ │
│ │ session_id=UUID │
│ └─────────┬────────────────┘
│ │
└──────────────┴───────────────────────┐
┌────────────────────────┘
┌────────────────────────────────────────┐
│ Success Page │
│ 1. Validates payment status │
│ 2. Shows success message │
│ 3. Enables "Proceed to Cards" button │
└────────────┬───────────────────────────┘
┌────────────────────────────────────────┐
│ User Can Now Draw Cards! │
│ - Access granted based on payment │
│ - Cards drawn from database │
│ - Results displayed │
└────────────────────────────────────────┘
```
## Payment States
```
┌──────────┐ Payment Created ┌──────────┐
│ │─────────────────────▶ │ │
│ NEW │ │ PENDING │
│ │ │ │
└──────────┘ └────┬─────┘
┌───────────────────┼───────────────────┐
│ │ │
Webhook: │ Webhook: │ Webhook: │
Success │ Refund │ Failed │
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ │ │ │ │
│SUCCEEDED │ │ REFUNDED │ │ FAILED │
│ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘
┌─────────────────────┐
│ User Can Draw │
│ Cards │
└─────────────────────┘
```
## Key Components Interaction
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Vue.js) │
│ │
│ ┌────────────────┐ ┌───────────────┐ ┌─────────────────┐ │
│ │ cardSelection │ │ Pending.vue │ │ Success.vue │ │
│ │ .vue │──▶│ (auto-refresh)│──▶│ (validate) │ │
│ └────────┬───────┘ └───────────────┘ └─────────────────┘ │
└───────────┼──────────────────────────────────────────────────────┘
│ POST /create-wise-payment
┌─────────────────────────────────────────────────────────────────┐
│ Backend (Laravel) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ WiseController │ │
│ │ │ │
│ │ createPaymentSession() ─────▶ Wise API │ │
│ │ handleWebhook() ◀───────────── Wise Webhook │ │
│ │ validatePayment() ──────────▶ Database Query │ │
│ └──────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Payment Model │ │
│ │ │ │
│ │ payment_provider: 'wise' │ │
│ │ wise_payment_id: string │ │
│ │ wise_session_id: string │ │
│ │ status: pending|succeeded|failed|refunded │ │
│ │ draw_count: int │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Database (SQLite) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ payments table │ │
│ │ │ │
│ │ - All payment records with provider tracking │ │
│ │ - Status management │ │
│ │ - Card draw count tracking │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ External Service │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Wise API │ │
│ │ │ │
│ │ - Receives payment requests │ │
│ │ - Processes transfers │ │
│ │ - Sends webhooks on status change │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Security Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ Webhook Security │
└─────────────────────────────────────────────────────────────────┘
Wise sends webhook:
POST /wise/webhook
Headers:
X-Signature-SHA256: abc123...
Body:
{"event_type": "transfer_state_change", ...}
┌──────────────────────────┐
│ WiseController │
│ handleWebhook() │
└────────┬─────────────────┘
┌──────────────────────────┐
│ Verify Signature: │
│ 1. Get payload │
│ 2. Get signature │
│ 3. Calculate HMAC │
│ 4. Compare hashes │
└────────┬─────────────────┘
┌────┴────┐
│ │
Invalid Valid
│ │
▼ ▼
Return Process
400 Event
& Update DB
```
## Error Handling
```
User Action → Try to Draw Cards
┌───────────────────┐
│ Check Payment │
│ Status │
└────────┬──────────┘
┌────────────┼────────────┐
│ │ │
Missing Pending Succeeded
│ │ │
▼ ▼ ▼
Error Pending Allow
Page Page Drawing
(404) (auto-refresh)
```
---
This diagram shows the complete flow from user selection to successful card drawing, including all the backend components, webhook handling, and state management.

View File

@ -0,0 +1,216 @@
# Wise Payment Integration - Implementation Summary
## ✅ What Has Been Implemented
### 1. Database Changes
- **Migration created and run**: `add_wise_fields_to_payments_table`
- **New columns added to `payments` table**:
- `payment_provider` (default: 'stripe') - Identifies payment source
- `wise_payment_id` - Stores Wise transfer ID
- `wise_session_id` - Stores Wise session identifier
### 2. Backend Components
#### **WiseController** (`app/Http/Controllers/WiseController.php`)
- `createPaymentSession()` - Creates Wise payment and returns payment URL
- `handleWebhook()` - Processes Wise webhook events
- `validatePayment()` - Validates payment status manually
- Webhook signature verification for security
#### **Routes** (`routes/web.php`)
- `POST /create-wise-payment` - Create new Wise payment
- `POST /wise/webhook` - Receive Wise webhooks
- `GET /wise/validate-payment` - Check payment status
- Updated `/success` route to handle both Stripe and Wise
#### **Payment Model** (`app/Models/Payment.php`)
- Added Wise fields to `$fillable` array
### 3. Frontend Components
#### **Card Selection** (`resources/js/pages/cards/cardSelection.vue`)
- Split payment buttons: Users can choose "Stripe" or "Wise"
- `redirectToWisePayment()` function to handle Wise payment flow
- Visual distinction: Gold button for Stripe, Dark blue for Wise
#### **Pending Page** (`resources/js/pages/payments/Pending.vue`)
- New page for processing payments (especially Wise)
- Auto-polls payment status every 10 seconds
- Shows progress and provider information
- User-friendly interface with refresh option
### 4. Configuration
- `.env.example` updated with Wise configuration variables
- Comprehensive setup documentation in `WISE_SETUP.md`
## 🔄 Payment Flow
```
User selects paid option (6 or 18 cards)
User clicks "Wise" button
Frontend calls POST /create-wise-payment
Backend creates Wise transfer & saves to DB (status: pending)
User redirected to Wise payment page
User completes payment on Wise
Wise sends webhook to POST /wise/webhook
Backend updates payment status to 'succeeded'
User returns to /success page
Success page validates payment
If succeeded → User can draw cards
If pending → Show Pending page with auto-refresh
```
## 📋 Next Steps to Go Live
### 1. Set Up Wise Account
```bash path=null start=null
# Create Wise Business account at https://wise.com
# Navigate to Settings → API tokens
# Create API token and get Profile ID
```
### 2. Configure Environment Variables
Add to your `.env` file:
```bash path=null start=null
WISE_API_URL=https://api.wise.com
WISE_API_TOKEN=your_api_token_here
WISE_PROFILE_ID=your_profile_id
WISE_WEBHOOK_SECRET=your_webhook_secret
WISE_RECIPIENT_NAME="Your Business Name"
WISE_RECIPIENT_EMAIL=payments@yourbusiness.com
```
### 3. Set Up Webhooks
1. Go to Wise Settings → Webhooks
2. Create webhook: `https://yourdomain.com/wise/webhook`
3. Subscribe to: `transfers#state-change` and `balance_credit`
4. Copy webhook secret to `.env`
### 4. Testing
For testing without real Wise account:
```bash path=null start=null
# Use sandbox environment
WISE_API_URL=https://sandbox.transferwise.tech
# Or test by manually updating payment status:
php artisan tinker
>>> $payment = App\Models\Payment::where('client_session_id', 'YOUR-UUID')->first();
>>> $payment->update(['status' => 'succeeded']);
```
### 5. Make Webhook Accessible
Ensure your webhook endpoint is publicly accessible:
- Remove CSRF protection for `/wise/webhook` route (already handled in controller)
- Make sure your server accepts POST requests on this route
- Test with Wise's webhook testing tool
## 🎨 User Interface
### Card Selection Screen
- **Free Option**: Single "Commencer" button
- **Paid Options (6 & 18 cards)**:
- Split into two buttons side-by-side
- Left button (Gold): "Stripe"
- Right button (Dark Blue): "Wise"
### Payment Processing
- **Stripe**: Redirects to Stripe Checkout (existing flow)
- **Wise**: Redirects to Wise payment page
- If payment pending: Shows Pending page with auto-refresh
- If payment succeeded: Shows Success page
## 🔒 Security Features
1. **Webhook Signature Verification**: HMAC-SHA256 validation
2. **Payment Status Checks**: Before allowing card draws
3. **One-time Use**: Cards can only be drawn once per payment
4. **Provider Tracking**: Each payment tagged with provider
## 📊 Database Schema
```sql
payments table:
- id
- amount
- currency
- stripe_session_id (existing)
- client_session_id (existing)
- draw_count
- status (pending/succeeded/failed/refunded)
- cards
- appointment_date
- payment_provider (NEW: 'stripe' or 'wise')
- wise_payment_id (NEW)
- wise_session_id (NEW)
- created_at
- updated_at
```
## 🐛 Troubleshooting
### Webhook Not Working
```bash path=null start=null
# Check Laravel logs
tail -f storage/logs/laravel.log
# Test webhook manually
curl -X POST https://yourdomain.com/wise/webhook \
-H "Content-Type: application/json" \
-H "X-Signature-SHA256: test" \
-d '{"event_type":"test"}'
```
### Payment Status Not Updating
```bash path=null start=null
# Check payment in database
php artisan tinker
>>> App\Models\Payment::where('payment_provider', 'wise')->latest()->get();
```
## 📚 Documentation
- **Setup Guide**: See `WISE_SETUP.md` for detailed configuration
- **Wise API Docs**: https://docs.wise.com/api-docs/
- **Wise Sandbox**: https://sandbox.transferwise.tech
## 🎯 Key Files Modified/Created
### Backend
- ✅ `app/Http/Controllers/WiseController.php` (NEW)
- ✅ `app/Models/Payment.php` (MODIFIED)
- ✅ `routes/web.php` (MODIFIED)
- ✅ `database/migrations/2025_10_14_130637_add_wise_fields_to_payments_table.php` (NEW)
### Frontend
- ✅ `resources/js/pages/cards/cardSelection.vue` (MODIFIED)
- ✅ `resources/js/pages/payments/Pending.vue` (NEW)
### Configuration
- ✅ `.env.example` (MODIFIED)
- ✅ `WISE_SETUP.md` (NEW)
- ✅ `WISE_IMPLEMENTATION_SUMMARY.md` (NEW - this file)
## ✨ Features
- ✅ Dual payment provider support (Stripe + Wise)
- ✅ Webhook integration for automatic status updates
- ✅ Payment status validation before card drawing
- ✅ Pending payment page with auto-refresh
- ✅ Secure webhook signature verification
- ✅ User-friendly payment provider selection
- ✅ Comprehensive error handling
- ✅ Support for both sandbox and production environments
---
**Status**: ✅ Implementation Complete - Ready for Configuration & Testing

217
WISE_QUICKSTART.md Normal file
View File

@ -0,0 +1,217 @@
# Wise Payment Integration - Quick Start Guide
## ✅ Installation Complete!
The Wise payment integration has been successfully implemented. Here's what you need to do to get it running.
## 🚀 Quick Setup (5 Minutes)
### Step 1: Add Environment Variables
Add these to your `.env` file:
```bash
# Wise Payment Configuration
WISE_API_URL=https://api.wise.com
WISE_API_TOKEN=your_token_here
WISE_PROFILE_ID=your_profile_id_here
WISE_WEBHOOK_SECRET=your_webhook_secret
WISE_RECIPIENT_NAME="Your Business Name"
WISE_RECIPIENT_EMAIL=payments@yourbusiness.com
```
### Step 2: Database is Ready ✅
The migration has already been run. Your `payments` table now has:
- `payment_provider` column
- `wise_payment_id` column
- `wise_session_id` column
### Step 3: Test It!
#### Option A: Test Without Wise Account (Development)
You can test the UI flow immediately:
1. Visit your card selection page
2. Select a paid option (6 or 18 cards)
3. You'll see two buttons: "Stripe" and "Wise"
4. Click "Wise" to test the flow
To simulate a successful payment during testing:
```bash
php artisan tinker
```
Then in Tinker:
```php
// Find the pending payment
$payment = App\Models\Payment::where('status', 'pending')->where('payment_provider', 'wise')->latest()->first();
// Mark it as succeeded
$payment->update(['status' => 'succeeded']);
```
Refresh the page and you'll be able to proceed to card drawing!
#### Option B: Test With Wise Sandbox
1. Create account at https://sandbox.transferwise.tech
2. Get your sandbox API token
3. Update `.env`:
```bash
WISE_API_URL=https://sandbox.transferwise.tech
```
## 🎯 How It Works
### User Experience
1. **User visits card selection page**
- Sees free option (1 card)
- Sees paid options (6 & 18 cards) with dual payment buttons
2. **User clicks "Wise" button**
- System creates payment record in database
- User is redirected to payment page
3. **After payment**
- Webhook updates payment status automatically
- User can draw their cards once payment succeeds
### Behind the Scenes
```
cardSelection.vue
↓ (clicks Wise)
POST /create-wise-payment
WiseController::createPaymentSession()
↓ (creates payment record)
Database: status = 'pending'
↓ (user pays)
POST /wise/webhook
WiseController::handleWebhook()
Database: status = 'succeeded'
User can draw cards!
```
## 📁 What Was Changed
### Backend (PHP/Laravel)
- ✅ `app/Http/Controllers/WiseController.php` - New controller
- ✅ `app/Models/Payment.php` - Added Wise fields
- ✅ `routes/web.php` - Added Wise routes
- ✅ `bootstrap/app.php` - Added CSRF exception for webhooks
- ✅ Database migration - Already run
### Frontend (Vue/TypeScript)
- ✅ `resources/js/pages/cards/cardSelection.vue` - Dual payment buttons
- ✅ `resources/js/pages/payments/Pending.vue` - New pending page
### Configuration
- ✅ `.env.example` - Added Wise variables
- ✅ All code formatted with Pint & Prettier
## 🧪 Testing Checklist
- [ ] Environment variables added to `.env`
- [ ] Can see dual payment buttons on card selection
- [ ] Clicking "Wise" creates payment record
- [ ] Webhook endpoint is accessible (for production)
- [ ] Payment status updates correctly
- [ ] User can draw cards after payment succeeds
## 🔧 Production Setup
When you're ready to go live:
1. **Get Wise Business Account**
- Sign up at https://wise.com
- Complete business verification
2. **Get API Credentials**
- Settings → API tokens
- Create token with appropriate permissions
- Note your Profile ID
3. **Configure Webhooks**
- Settings → Webhooks
- Add: `https://yourdomain.com/wise/webhook`
- Subscribe to: `transfers#state-change` and `balance_credit`
- Copy webhook secret
4. **Update `.env` for Production**
```bash
WISE_API_URL=https://api.wise.com
WISE_API_TOKEN=your_production_token
WISE_PROFILE_ID=your_production_profile_id
WISE_WEBHOOK_SECRET=your_production_webhook_secret
```
5. **Deploy & Test**
- Deploy your changes
- Test with a real small payment
- Verify webhook is receiving events
- Check logs: `storage/logs/laravel.log`
## 🆘 Troubleshooting
### "Payment not found" error
```bash
# Check if payment was created
php artisan tinker
>>> App\Models\Payment::latest()->first();
```
### Webhook not working
```bash
# Check logs
tail -f storage/logs/laravel.log
# Verify webhook endpoint is accessible
curl -X POST https://yourdomain.com/wise/webhook -v
```
### Payment stuck on "pending"
```bash
# Manually mark as succeeded for testing
php artisan tinker
>>> $payment = App\Models\Payment::where('client_session_id', 'YOUR-UUID')->first();
>>> $payment->update(['status' => 'succeeded']);
```
## 📚 Documentation
- **Full Setup Guide**: See `WISE_SETUP.md`
- **Implementation Details**: See `WISE_IMPLEMENTATION_SUMMARY.md`
- **Wise API Docs**: https://docs.wise.com/api-docs/
## 💡 Tips
1. **Start with sandbox** - Test everything before going live
2. **Monitor logs** - Check `storage/logs/laravel.log` regularly
3. **Test webhooks** - Use Wise's webhook testing tool
4. **Keep secrets safe** - Never commit `.env` to version control
## ✨ Features You Now Have
- ✅ Dual payment provider support (Stripe + Wise)
- ✅ Automatic webhook processing
- ✅ Payment validation before card drawing
- ✅ Pending payment page with auto-refresh
- ✅ Secure webhook signature verification
- ✅ User-friendly payment selection
- ✅ Full error handling
---
**Need Help?** Check the detailed guides:
- Setup: `WISE_SETUP.md`
- Implementation: `WISE_IMPLEMENTATION_SUMMARY.md`
**Ready to test?** Just add your Wise credentials to `.env` and visit your card selection page! 🎴

233
WISE_SETUP.md Normal file
View File

@ -0,0 +1,233 @@
# Wise Payment Integration Setup Guide
This guide explains how to set up and use the Wise payment integration for your tarot card reading application.
## Overview
The Wise integration allows users to pay for card readings using Wise (formerly TransferWise) alongside the existing Stripe integration. When a user selects a paid option, they can now choose between Stripe and Wise as their payment provider.
## Flow
1. **Card Selection** - User selects a paid reading option (6 or 18 cards)
2. **Payment Provider Choice** - User clicks either "Stripe" or "Wise" button
3. **Payment Creation** - Backend creates a payment session with Wise API
4. **Payment Redirect** - User is redirected to Wise payment page
5. **Webhook Verification** - Wise sends webhook to confirm payment
6. **Access to Tirage** - Once payment is verified (status = 'succeeded'), user can draw cards
## Database Changes
### Migration
Run the migration to add Wise fields to the payments table:
```bash path=null start=null
php artisan migrate
```
This adds:
- `payment_provider` - Identifies whether payment is 'stripe' or 'wise'
- `wise_payment_id` - Wise transfer ID
- `wise_session_id` - Wise session identifier
## Configuration
### 1. Set Up Wise Account
1. Create a Wise Business account at https://wise.com
2. Navigate to Settings → API tokens
3. Create a new API token with appropriate permissions
4. Get your Profile ID from the API settings
### 2. Environment Variables
Add these to your `.env` file:
```bash path=null start=null
# Wise Payment Configuration
WISE_API_URL=https://api.wise.com # Use https://sandbox.transferwise.tech for testing
WISE_API_TOKEN=your_wise_api_token_here
WISE_PROFILE_ID=your_profile_id_here
WISE_WEBHOOK_SECRET=your_webhook_secret_here
WISE_RECIPIENT_NAME="Your Business Name"
WISE_RECIPIENT_EMAIL=payments@yourbusiness.com
```
**For Testing:**
- Use `WISE_API_URL=https://sandbox.transferwise.tech` for sandbox environment
- Create a sandbox account at https://sandbox.transferwise.tech
### 3. Configure Webhooks
In your Wise account:
1. Go to Settings → Webhooks
2. Create a new webhook
3. Set the URL to: `https://yourdomain.com/wise/webhook`
4. Subscribe to these events:
- `transfers#state-change` - For transfer status updates
- `balance_credit` - For balance credit notifications
5. Copy the webhook secret and add it to your `.env` as `WISE_WEBHOOK_SECRET`
## API Endpoints
### Payment Creation
```bash path=null start=null
POST /create-wise-payment
```
**Request:**
```json
{
"count": 6 // or 18
}
```
**Response:**
```json
{
"success": true,
"paymentUrl": "https://api.wise.com/pay/12345",
"transferId": "12345",
"clientSessionId": "uuid-here"
}
```
### Webhook Handler
```bash path=null start=null
POST /wise/webhook
```
Receives Wise webhook events and updates payment status.
### Payment Validation
```bash path=null start=null
GET /wise/validate-payment?client_session_id={uuid}
```
**Response:**
```json
{
"success": true,
"drawCount": 6
}
```
## Frontend Changes
The `cardSelection.vue` component now has dual payment buttons:
- **Stripe Button** (Gold) - Uses existing Stripe flow
- **Wise Button** (Dark Blue) - Uses new Wise flow
Both buttons appear for the paid options (6 cards and 18 cards).
## How It Works
### Payment Flow
1. User clicks "Wise" button on card selection
2. `redirectToWisePayment()` function calls `/create-wise-payment`
3. Backend:
- Creates Wise quote for the amount
- Creates recipient account
- Creates transfer with customer transaction ID
- Stores payment record with `status='pending'` and `payment_provider='wise'`
4. User is redirected to Wise payment page
5. User completes payment on Wise
6. Wise sends webhook to `/wise/webhook`
7. Webhook handler updates payment status to `'succeeded'`
8. User returns to success page
9. Success page validates payment status
10. If payment succeeded, user can proceed to draw cards
### Webhook Handling
The `WiseController::handleWebhook()` method processes:
- **transfer_state_change**: Updates payment status based on transfer state
- `outgoing_payment_sent``processing`
- `funds_refunded``refunded`
- `bounced_back``failed`
- **balance_credit**: Marks payment as `succeeded` when funds are received
### Payment Verification
Before allowing card drawing, the system checks:
1. Payment exists in database
2. `client_session_id` matches
3. `status` is `'succeeded'`
4. `payment_provider` is `'wise'`
This happens in the `/success` route and ensures users can only draw cards after payment is confirmed.
## Important Notes
### Wise API Differences from Stripe
Unlike Stripe Checkout, Wise doesn't have a hosted payment page URL that you redirect to directly. The implementation provided creates the transfer but you'll need to implement one of these options:
**Option 1: Manual Payment Instructions**
- Show users the transfer details and ask them to pay manually
- Check status via webhooks or polling
**Option 2: Wise Payment Links** (Recommended)
- Use Wise's payment link feature (if available in your region)
- Contact Wise support to enable this feature
**Option 3: Custom Integration**
- Build a custom payment form that collects payment method details
- Use Wise API to process the payment
### Testing
For testing, you can:
1. Use Wise sandbox environment
2. Manually update payment status in database:
```sql
UPDATE payments SET status='succeeded' WHERE client_session_id='your-uuid';
```
3. Test webhook with Wise's webhook testing tool
### Security
- **Webhook signatures** are verified using HMAC-SHA256
- **Payment validation** checks status before allowing card draws
- **One-time use** - Cards can only be drawn once per payment
## Troubleshooting
### Webhook not receiving events
1. Check your webhook URL is publicly accessible
2. Verify `WISE_WEBHOOK_SECRET` matches Wise dashboard
3. Check Laravel logs: `storage/logs/laravel.log`
### Payment not marking as succeeded
1. Check webhook is being received (check logs)
2. Verify payment record exists with correct `wise_payment_id`
3. Manually check transfer status via Wise dashboard
### User can't draw cards after payment
1. Verify payment status is `'succeeded'` in database
2. Check `client_session_id` is being passed correctly in URL
3. Verify payment provider is set to `'wise'`
## Next Steps
1. **Run the migration**: `php artisan migrate`
2. **Configure .env**: Add all Wise environment variables
3. **Set up webhooks**: Configure in Wise dashboard
4. **Test in sandbox**: Use Wise sandbox for testing
5. **Go live**: Switch to production API URL and credentials
## Support
For Wise API documentation, visit:
- API Docs: https://docs.wise.com/api-docs/
- Sandbox: https://sandbox.transferwise.tech
- Support: https://wise.com/help/
For issues with this integration, check the Laravel logs and Wise dashboard for detailed error messages.

244
WISE_SIMPLE_SETUP.md Normal file
View File

@ -0,0 +1,244 @@
# Wise Payment Integration - Simple Setup (Payment Links)
## 🎯 Overview
This is a **simplified implementation** using Wise payment links that your client has already provided. Users click a button, get redirected to Wise to pay, and then we verify the payment afterward.
## ✅ What's Already Done
All the code is ready! You just need to configure the payment links.
## 🚀 Quick Setup (2 Minutes)
### Step 1: Add Payment Links to `.env`
Your client provided you with a link like: `https://wise.com/pay/r/W2k1NqQySdc9HW8`
Add these to your `.env` file:
```bash
# Wise Payment Links (one for each product)
WISE_PAYMENT_LINK_6_CARDS=https://wise.com/pay/r/W2k1NqQySdc9HW8
WISE_PAYMENT_LINK_18_CARDS=https://wise.com/pay/r/YOUR_OTHER_LINK_HERE
# Webhook secret (optional for now, needed later for automatic verification)
WISE_WEBHOOK_SECRET=
```
### Step 2: Test It!
That's it! You can now test:
1. Visit your card selection page
2. Select a paid option (6 or 18 cards)
3. Click the "Wise" button
4. You'll be redirected to the Wise payment page
## 🔄 How It Works
```
┌─────────────────────────────────────────────────────────────┐
│ 1. User clicks "Wise" button on card selection │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. Backend creates payment record in database │
│ - status: 'pending' │
│ - payment_provider: 'wise' │
│ - Saves client_session_id (UUID) │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. User redirected to Wise payment link │
│ Example: https://wise.com/pay/r/W2k1NqQySdc9HW8 │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. User completes payment on Wise website │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. User manually returns to your site │
│ Goes to: /wise/verify │
└───────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 6. System checks payment status in database │
│ - If succeeded → Redirect to success page │
│ - If still pending → Show "check again" button │
└─────────────────────────────────────────────────────────────┘
```
## 📱 User Experience
### Step 1: Card Selection
User sees three options with dual payment buttons:
- **Free option**: Single "Commencer" button
- **6 cards (9.99€)**: [Stripe] [Wise] buttons
- **18 cards (15.90€)**: [Stripe] [Wise] buttons
### Step 2: Click Wise Button
- Payment record created in database
- User redirected to: `https://wise.com/pay/r/W2k1NqQySdc9HW8`
### Step 3: Payment on Wise
- User completes payment on Wise's secure website
- Wise processes the payment
### Step 4: Return & Verification
Two options:
**Option A: User returns via custom link**
- User clicks "Back to site" or similar
- Goes to: `/wise/verify`
- System checks payment status
- If paid → Success! Access granted to cards
**Option B: Webhook (automatic - recommended)**
- Wise sends notification to your server
- Payment status updated automatically
- User can verify anytime
## 🔧 For Production: Set Up Webhooks
To make verification automatic (so users don't have to manually check):
### 1. Set Up Webhook URL
In your Wise business account:
1. Go to Settings → Webhooks
2. Add new webhook: `https://yourdomain.com/wise/webhook`
3. Subscribe to these events:
- `transfers#state-change`
- `balance_credit`
4. Copy the webhook secret
### 2. Add Webhook Secret
Add to your `.env`:
```bash
WISE_WEBHOOK_SECRET=your_webhook_secret_here
```
Now when users pay, the status updates automatically!
## 🧪 Testing Workflow
### Manual Testing (Without Real Payment)
1. **Create test payment**:
```bash
# Click Wise button on card selection
# This creates a pending payment in database
```
2. **Simulate successful payment**:
```bash
php artisan tinker
```
Then in Tinker:
```php
// Get the latest Wise payment
$payment = App\Models\Payment::where('payment_provider', 'wise')
->where('status', 'pending')
->latest()
->first();
// Mark it as succeeded
$payment->update(['status' => 'succeeded']);
// Exit
exit
```
3. **Verify**:
- Go to `/wise/verify` page
- Should show "Payment succeeded!"
- Should redirect to success page
- User can now draw cards
### Testing With Real Payment
1. Click "Wise" button
2. Complete payment on Wise (use smallest amount for test)
3. After payment, go to `/wise/verify`
4. System should detect payment succeeded
## 📂 Key Files
### Backend
- `app/Http/Controllers/WiseController.php` - Handles payment creation and verification
- `routes/web.php` - Routes for payment and webhook
- `.env` - Configuration (payment links and webhook secret)
### Frontend
- `resources/js/pages/cards/cardSelection.vue` - Payment selection with dual buttons
- `resources/js/pages/wise/VerifyPayment.vue` - Verification page after payment
- `resources/js/pages/payments/Success.vue` - Success page (access to cards)
## 🔐 Security
- ✅ Payment records stored before redirect
- ✅ Status verified before allowing card access
- ✅ Webhook signature verification (when configured)
- ✅ Client session ID prevents unauthorized access
## 💡 Important Notes
### Return URL Configuration
After users complete payment on Wise, they need to return to your site. Options:
1. **Manual Return**: User clicks browser back or a link
2. **Wise Return URL**: Ask your client if they can add a return URL to the payment link settings
- Ideal return URL: `https://yourdomain.com/wise/verify`
### Multiple Payment Links
You need separate payment links for:
- **6 cards** (9.99€)
- **18 cards** (15.90€)
Ask your client to create both links in their Wise account.
## 🆘 Troubleshooting
### "Payment link not configured"
- Check `.env` has `WISE_PAYMENT_LINK_6_CARDS` and `WISE_PAYMENT_LINK_18_CARDS`
- Make sure links are valid Wise payment URLs
### Payment status stuck on "pending"
```bash
# Check payment was created
php artisan tinker
>>> App\Models\Payment::where('payment_provider', 'wise')->latest()->first();
# Manually mark as succeeded for testing
>>> $payment = App\Models\Payment::where('client_session_id', 'YOUR-UUID')->first();
>>> $payment->update(['status' => 'succeeded']);
```
### Webhook not working
- Make sure webhook URL is publicly accessible
- Verify `WISE_WEBHOOK_SECRET` in `.env`
- Check logs: `tail -f storage/logs/laravel.log`
## ✨ Next Steps
1. ✅ **Done**: Code is implemented
2. 📝 **Now**: Add payment links to `.env`
3. 🧪 **Test**: Click Wise button and verify redirect
4. 🔗 **Production**: Set up Wise webhooks
5. 🎉 **Launch**: Ready for users!
---
**Need the other payment link?** Ask your client to create a second Wise payment link for the 18-card option (15.90€) and add it to `.env`.
**Questions?** Check the logs: `storage/logs/laravel.log`

View File

@ -0,0 +1,276 @@
<?php
namespace App\Http\Controllers;
use App\Models\Payment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class WiseController extends Controller
{
/**
* Create a Wise payment session for card draws
* Simplified version - just redirects to pre-configured Wise payment links
*/
public function createPaymentSession(Request $request)
{
$count = $request->input('count');
$clientSessionId = Str::uuid();
// Define pricing and payment links for different draw counts
$paymentOptions = [
6 => [
'amount' => 9.99,
'currency' => 'EUR',
'description' => 'Profilage - 6 cartes',
'payment_link' => env('WISE_PAYMENT_LINK_6_CARDS', 'https://wise.com/pay/r/JVNRSE21VZTj8rw'),
],
18 => [
'amount' => 15.90,
'currency' => 'EUR',
'description' => 'Quadrige Doré - 18 cartes',
'payment_link' => env('WISE_PAYMENT_LINK_18_CARDS','https://wise.com/pay/r/W2k1NqQySdc9HW8'),
],
];
if (! isset($paymentOptions[$count])) {
return response()->json(['error' => 'Invalid product selected.'], 400);
}
$option = $paymentOptions[$count];
try {
// Store payment in database with pending status
Payment::create([
'amount' => $option['amount'],
'currency' => $option['currency'],
'wise_session_id' => $clientSessionId,
'client_session_id' => $clientSessionId,
'draw_count' => $count,
'status' => 'pending',
'payment_provider' => 'wise',
]);
Log::info('Wise payment created', [
'client_session_id' => $clientSessionId,
'amount' => $option['amount'],
'draw_count' => $count,
]);
// Return the payment link URL
return response()->json([
'success' => true,
'paymentUrl' => $option['payment_link'],
'clientSessionId' => $clientSessionId,
]);
} catch (\Exception $e) {
Log::error('Wise payment creation failed: '.$e->getMessage());
return response()->json(['error' => 'Could not create payment session.'], 500);
}
}
/**
* Handle Wise webhook notifications
*/
public function handleWebhook(Request $request)
{
$payload = $request->all();
$signature = $request->header('X-Signature-SHA256');
// Verify webhook signature
if (! $this->verifyWebhookSignature($request->getContent(), $signature)) {
Log::error('Wise webhook signature verification failed');
return response()->json(['error' => 'Invalid signature'], 400);
}
try {
$eventType = $payload['event_type'] ?? $payload['type'] ?? null;
Log::info('Wise webhook received', ['event_type' => $eventType, 'payload' => $payload]);
// Handle different Wise event types
switch ($eventType) {
case 'transfer_state_change':
case 'transfers#state-change':
$this->handleTransferStateChange($payload);
break;
case 'balance_credit':
$this->handleBalanceCredit($payload);
break;
default:
Log::info('Unhandled Wise webhook event type: '.$eventType);
break;
}
return response()->json(['status' => 'success'], 200);
} catch (\Exception $e) {
Log::error('Wise webhook processing error: '.$e->getMessage(), ['exception' => $e]);
return response()->json(['error' => 'Server error'], 500);
}
}
/**
* Handle transfer state change events
*/
private function handleTransferStateChange(array $payload)
{
$transferId = $payload['data']['resource']['id'] ?? null;
$currentState = $payload['data']['current_state'] ?? $payload['data']['resource']['status'] ?? null;
if (! $transferId) {
Log::warning('Transfer ID not found in Wise webhook payload');
return;
}
// Find payment by Wise transfer ID
$payment = Payment::where('wise_payment_id', $transferId)
->orWhere('wise_session_id', $payload['data']['resource']['customerTransactionId'] ?? null)
->first();
if (! $payment) {
Log::warning('No payment record found for Wise transfer ID: '.$transferId);
return;
}
// Update payment status based on transfer state
switch ($currentState) {
case 'outgoing_payment_sent':
case 'funds_converted':
case 'incoming_payment_waiting':
// Payment is being processed
$payment->update(['status' => 'processing']);
break;
case 'funds_refunded':
// Payment was refunded
$payment->update(['status' => 'refunded']);
break;
case 'bounced_back':
case 'charged_back':
// Payment failed or was charged back
$payment->update(['status' => 'failed']);
break;
default:
Log::info('Unhandled Wise transfer state: '.$currentState);
break;
}
}
/**
* Handle balance credit events (payment received)
*/
private function handleBalanceCredit(array $payload)
{
$amount = $payload['data']['amount'] ?? null;
$currency = $payload['data']['currency'] ?? null;
$transactionId = $payload['data']['transaction_id'] ?? null;
// Find payment by amount and currency (less reliable, but works for balance credits)
$payment = Payment::where('amount', $amount)
->where('currency', $currency)
->where('status', '!=', 'succeeded')
->where('payment_provider', 'wise')
->first();
if ($payment) {
$payment->update([
'status' => 'succeeded',
'wise_payment_id' => $transactionId,
]);
Log::info('Wise payment succeeded', ['payment_id' => $payment->id, 'transaction_id' => $transactionId]);
}
}
/**
* Verify Wise webhook signature
*/
private function verifyWebhookSignature(string $payload, ?string $signature): bool
{
if (! $signature) {
return false;
}
$webhookSecret = env('WISE_WEBHOOK_SECRET');
if (! $webhookSecret) {
Log::warning('WISE_WEBHOOK_SECRET not configured');
return true; // Allow in development if secret not set
}
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
return hash_equals($expectedSignature, $signature);
}
/**
* Validate payment status manually (for redirect flow)
*/
public function validatePayment(Request $request)
{
$clientSessionId = $request->query('client_session_id');
$payment = Payment::where('client_session_id', $clientSessionId)
->where('payment_provider', 'wise')
->first();
if (! $payment) {
return response()->json([
'success' => false,
'message' => 'Payment not found.',
], 404);
}
// Check if payment is succeeded
if ($payment->status === 'succeeded') {
return response()->json([
'success' => true,
'drawCount' => $payment->draw_count,
]);
}
// If payment is still pending, check with Wise API
if ($payment->status === 'pending' && $payment->wise_payment_id) {
try {
$wiseApiUrl = env('WISE_API_URL', 'https://api.wise.com');
$wiseApiToken = env('WISE_API_TOKEN');
$response = Http::withToken($wiseApiToken)
->get("{$wiseApiUrl}/v1/transfers/{$payment->wise_payment_id}");
if ($response->successful()) {
$transferStatus = $response->json('status');
if ($transferStatus === 'outgoing_payment_sent') {
$payment->update(['status' => 'succeeded']);
return response()->json([
'success' => true,
'drawCount' => $payment->draw_count,
]);
}
}
} catch (\Exception $e) {
Log::error('Wise payment validation failed: '.$e->getMessage());
}
}
return response()->json([
'success' => false,
'message' => 'Payment not validated.',
'status' => $payment->status,
], 402);
}
}

View File

@ -23,6 +23,9 @@ class Payment extends Model
'status',
'cards',
'appointment_date',
'payment_provider',
'wise_payment_id',
'wise_session_id',
];
/**
@ -34,5 +37,4 @@ class Payment extends Model
'amount' => 'decimal:2',
'cards' => 'array',
];
}

View File

@ -25,6 +25,7 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->validateCsrfTokens(except: [
'stripe/*',
'wise/*',
]);
})
->withExceptions(function (Exceptions $exceptions) {

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->string('payment_provider')->default('stripe')->after('status'); // 'stripe' or 'wise'
$table->string('wise_payment_id')->nullable()->after('payment_provider');
$table->string('wise_session_id')->nullable()->after('wise_payment_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->dropColumn(['payment_provider', 'wise_payment_id', 'wise_session_id']);
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->string('stripe_session_id')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->string('stripe_session_id')->nullable(false)->change();
});
}
};

View File

@ -223,7 +223,7 @@
@font-face {
font-family: 'Audrey';
src: url('@/fonts/Audrey/Audrey-Medium.otf') format('truetype');
src: url('@/fonts/Audrey/Audrey-Normal.otf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;

View File

@ -83,6 +83,14 @@ const goToShuffle = () => {
intemporelle de l'Oracle de Kris Saint Ange, un guide stratégique pour naviguer votre destin avec clarté et confiance.
</p>
<p
class="max-w-2xl text-lg text-[var(--linen)] transition-all delay-500 duration-1000"
:class="{ 'translate-y-0 opacity-100': isMounted, 'translate-y-4 opacity-0': !isMounted }"
>
Embrassez la sagesse intemporelle de l'Oracle de Kris Saint Ange, un guide stratégique pour naviguer votre destin avec clarté et
confiance.
</p>
<!-- Animated button with gold effects -->
<button
class="group gold-button relative mt-4 flex h-14 max-w-[480px] min-w-[160px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-gradient-to-b from-[var(--subtle-gold)] to-[#c8a971] px-8 text-base font-bold tracking-wide text-[var(--midnight-blue)] transition-all duration-500"

View File

@ -2,7 +2,7 @@
<section class="py-20 text-center sm:py-24">
<h2 class="text-4xl font-bold text-[var(--midnight-blue)] md:text-5xl">
Plus qu'un guide,
<span class="citadel-script relative text-3xl font-normal text-[var(--subtle-gold)] md:text-7xl">Un manuscrit stratégique</span>
<span class="citadel-script relative text-3xl font-normal text-[var(--subtle-gold)] md:text-7xl">un&nbsp;Manuscrit Stratégique</span>
</h2>
<p class="mx-auto mt-6 max-w-3xl text-lg text-[var(--midnight-blue)]/80">
Conçu pour les Leaders visionnaires, cet oracle n'est pas un simple guide : c'est un Manuscrit se croisent stratégie antique et
@ -24,7 +24,7 @@
</div>
<p class="mx-auto mt-6 max-w-3xl text-lg text-[var(--midnight-blue)]/80">
La souveraineté ne se négocie pas. Elle se conquiert. Et votre empire intérieur attend son couronnement.
<strong> La souveraineté ne se négocie pas. Elle se conquiert. Et votre empire intérieur attend son couronnement. </strong>
</p>
</section>
</template>

View File

@ -1,12 +1,12 @@
<template>
<!-- stripe-checkout -->
<!-- stripe-checkout -->
</template>
<script>
import { StripeCheckout } from '@vue-stripe/vue-stripe';
export default {
components: {
StripeCheckout,
},
components: {
StripeCheckout,
},
};
</script>
</script>

View File

@ -1,12 +1,187 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Citadel Script Fonts Demo</title>
<style type="text/css">
*{margin:0;padding:0;list-style-type:none;}body{font-family:Arial,Helvetica,sans-serif;line-height:1.5;font-size:16px;padding:20px;color:#333;background-image:url(data:image/gif;base64,R0lGODlhCgAKAIAAAP////Dw8CwAAAAACgAKAAACEIwDh5fsv2AzU0UKb5071wIAOw==);}a{color:#66A523;}a:hover,.g:hover{opacity:0.7;}h1{font-size:60px;border-bottom:1px solid #bbb;line-height:20px;color:#000;text-transform:uppercase;margin-top:20px;}h1 a{font-size:18px;color:#66A523;text-decoration:none;text-transform:capitalize;margin-left:10px;}h1 font{font-size:40px;}.info{float:left;font-size:14px;margin-top:30px;color:#666;}.info .inst{font-size:22px;margin-bottom:10px;}.info span,.by span{color:#fff;border-radius:2px;display:inline-block;width:18px;height:18px;text-align:center;margin-right:10px;background-color:#333333;font-size:12px;line-height:18px;}.info .exs{font-size:12px;color:#666;margin-left:35px;display:block;}.info .exs font{color:#f00;line-height:30px;}.demo{float:left;width:100%;height:auto;padding-bottom:10px;margin-top:70px;background-color:#FFF;border-radius:5px;}.demo a{color:#1a73e8;float:left;font-weight:bold;font-size:14px;padding-right:30px;padding-left:30px;position:relative;border-top-width:1px;border-top-style:dotted;border-top-color:#1A73E8;text-decoration:none;border-bottom-width:1px;border-bottom-style:dotted;border-bottom-color:#1A73E8;line-height:40px;margin-top:-40px;}.demo .t{font-size:24px;float:left;width:calc(100% - 20px);color:#000;height:120px;margin-top:20px;line-height:1.2em;overflow-x:hidden;padding:10px;}.by{padding-top:15px;float:left;width:100%;margin-top:10px;margin-right:0;margin-bottom:10px;margin-left:0;font-size:14px;color:#666;}.by .b{padding-top:10px;padding-bottom:10px;color:#333;}.by .a{padding-top:10px;padding-bottom:10px;margin-left:34px;font-size:12px;}.te{width:100%;height:auto;border:0px solid #CCC;resize:none;float:left;margin-top:10px;line-height:16px;overflow:hidden;border-radius:5px;color:#000000;background-color:#FFF;font-size:12px;margin-bottom:20px;text-indent:10px;padding-top:17px;padding-bottom:17px;}pre{color:#000;}
</style>
<style type="text/css">
<head>
<meta charset="utf-8" />
<title>Citadel Script Fonts Demo</title>
<style type="text/css">
* {
margin: 0;
padding: 0;
list-style-type: none;
}
body {
font-family: Arial, Helvetica, sans-serif;
line-height: 1.5;
font-size: 16px;
padding: 20px;
color: #333;
background-image: url(data:image/gif;base64,R0lGODlhCgAKAIAAAP////Dw8CwAAAAACgAKAAACEIwDh5fsv2AzU0UKb5071wIAOw==);
}
a {
color: #66a523;
}
a:hover,
.g:hover {
opacity: 0.7;
}
h1 {
font-size: 60px;
border-bottom: 1px solid #bbb;
line-height: 20px;
color: #000;
text-transform: uppercase;
margin-top: 20px;
}
h1 a {
font-size: 18px;
color: #66a523;
text-decoration: none;
text-transform: capitalize;
margin-left: 10px;
}
h1 font {
font-size: 40px;
}
.info {
float: left;
font-size: 14px;
margin-top: 30px;
color: #666;
}
.info .inst {
font-size: 22px;
margin-bottom: 10px;
}
.info span,
.by span {
color: #fff;
border-radius: 2px;
display: inline-block;
width: 18px;
height: 18px;
text-align: center;
margin-right: 10px;
background-color: #333333;
font-size: 12px;
line-height: 18px;
}
.info .exs {
font-size: 12px;
color: #666;
margin-left: 35px;
display: block;
}
.info .exs font {
color: #f00;
line-height: 30px;
}
.demo {
float: left;
width: 100%;
height: auto;
padding-bottom: 10px;
margin-top: 70px;
background-color: #fff;
border-radius: 5px;
}
.demo a {
color: #1a73e8;
float: left;
font-weight: bold;
font-size: 14px;
padding-right: 30px;
padding-left: 30px;
position: relative;
border-top-width: 1px;
border-top-style: dotted;
border-top-color: #1a73e8;
text-decoration: none;
border-bottom-width: 1px;
border-bottom-style: dotted;
border-bottom-color: #1a73e8;
line-height: 40px;
margin-top: -40px;
}
.demo .t {
font-size: 24px;
float: left;
width: calc(100% - 20px);
color: #000;
height: 120px;
margin-top: 20px;
line-height: 1.2em;
overflow-x: hidden;
padding: 10px;
}
.by {
padding-top: 15px;
float: left;
width: 100%;
margin-top: 10px;
margin-right: 0;
margin-bottom: 10px;
margin-left: 0;
font-size: 14px;
color: #666;
}
.by .b {
padding-top: 10px;
padding-bottom: 10px;
color: #333;
}
.by .a {
padding-top: 10px;
padding-bottom: 10px;
margin-left: 34px;
font-size: 12px;
}
.te {
width: 100%;
height: auto;
border: 0px solid #ccc;
resize: none;
float: left;
margin-top: 10px;
line-height: 16px;
overflow: hidden;
border-radius: 5px;
color: #000000;
background-color: #fff;
font-size: 12px;
margin-bottom: 20px;
text-indent: 10px;
padding-top: 17px;
padding-bottom: 17px;
}
pre {
color: #000;
}
</style>
<style type="text/css">
@font-face {
font-family: 'Citadel Script';
src: url('b9583eae10a51eee606cae74cf802753.eot');
src:
url('b9583eae10a51eee606cae74cf802753.eot?#iefix') format('embedded-opentype'),
url('b9583eae10a51eee606cae74cf802753.woff') format('woff'),
url('b9583eae10a51eee606cae74cf802753.woff2') format('woff2'),
url('b9583eae10a51eee606cae74cf802753.ttf') format('truetype'),
url('b9583eae10a51eee606cae74cf802753.svg#Citadel Script') format('svg');
font-weight: normal;
font-style: normal;
font-display: swap;
}
</style>
</head>
<body>
<h1>
<span>[ <font>Demo</font> ]</span><a href="http://www.onlinewebfonts.com" target="_blank">Web Fonts</a>
</h1>
<div class="info">
<div class="inst">Instructions:</div>
<span>1</span>Use font-face declaration Fonts.
<div class="exs">
<pre>
@font-face{
font-family: "Citadel Script";
@ -21,58 +196,47 @@
font-display:swap;
}
</style>
</head>
<body>
<h1> <span>[ <font>Demo</font> ]</span><a href="http://www.onlinewebfonts.com" target="_blank">Web Fonts</a></h1>
<div class="info">
<div class="inst">Instructions:</div>
<span>1</span>Use font-face declaration Fonts.
<div class="exs">
<pre>
@font-face{
font-family: "Citadel Script";
src: url("b9583eae10a51eee606cae74cf802753.eot");
src: url("b9583eae10a51eee606cae74cf802753.eot?#iefix")format("embedded-opentype"),
url("b9583eae10a51eee606cae74cf802753.woff")format("woff"),
url("b9583eae10a51eee606cae74cf802753.woff2")format("woff2"),
url("b9583eae10a51eee606cae74cf802753.ttf")format("truetype"),
url("b9583eae10a51eee606cae74cf802753.svg#Citadel Script")format("svg");
font-weight:normal;
font-style:normal;
font-display:swap;
}
</pre>
</div>
<span>or</span>To embed a font, copy the code into the head of your html
<div class="exs">
<br/><pre>&lt;link href=&quot;https://db.onlinewebfonts.com/c/b9583eae10a51eee606cae74cf802753?family=Citadel+Script&quot; rel=&quot;stylesheet&quot;&gt;</pre><br/>
</div>
<span>2</span>CSS rules to specify families
<div class="exs"><font>Use example:</font> <br/>
<pre>
</pre
>
</div>
<span>or</span>To embed a font, copy the code into the head of your html
<div class="exs">
<br />
<pre>
&lt;link href=&quot;https://db.onlinewebfonts.com/c/b9583eae10a51eee606cae74cf802753?family=Citadel+Script&quot; rel=&quot;stylesheet&quot;&gt;</pre
>
<br />
</div>
<span>2</span>CSS rules to specify families
<div class="exs">
<font>Use example:</font> <br />
<pre>
font-family: &quot;Citadel Script&quot;;
</pre>
</div>
</div>
<!--demo-->
<div class="demo"><a href="https://www.onlinewebfonts.com/download/b9583eae10a51eee606cae74cf802753" target="_blank">Citadel Script</a>
<div class="t" style='font-family:"Citadel Script"'>
OnlineWebFonts.Com Some fonts provided are trial versions of full versions and may not allow embedding
unless a commercial license is purchased or may contain a limited character set.
Please review any files included with your download,
which will usually include information on the usage and licenses of the fonts.
If no information is provided,
please use at your own discretion or contact the author directly.</div>
</div>
<!--demo-->
<div class="by">
<div class="b"><span>3</span>License and attribution:</div>
<div class="a">You must credit the author Copy this link ( <a href="http://www.onlinewebfonts.com" target="_blank">oNlineWebFonts.Com</a> ) on your web</div>
<div class="b"><span>4</span>Copy the Attribution License:</div>
<div class="te">&lt;div&gt;Fonts made from &lt;a href="http://www.onlinewebfonts.com"&gt;Web Fonts&lt;/a&gt;is licensed by CC BY 4.0&lt;/div&gt;</div>
</div>
</body>
</pre
>
</div>
</div>
<!--demo-->
<div class="demo">
<a href="https://www.onlinewebfonts.com/download/b9583eae10a51eee606cae74cf802753" target="_blank">Citadel Script</a>
<div class="t" style="font-family: 'Citadel Script'">
OnlineWebFonts.Com Some fonts provided are trial versions of full versions and may not allow embedding unless a commercial license is
purchased or may contain a limited character set. Please review any files included with your download, which will usually include
information on the usage and licenses of the fonts. If no information is provided, please use at your own discretion or contact the
author directly.
</div>
</div>
<!--demo-->
<div class="by">
<div class="b"><span>3</span>License and attribution:</div>
<div class="a">
You must credit the author Copy this link ( <a href="http://www.onlinewebfonts.com" target="_blank">oNlineWebFonts.Com</a> ) on your
web
</div>
<div class="b"><span>4</span>Copy the Attribution License:</div>
<div class="te">
&lt;div&gt;Fonts made from &lt;a href="http://www.onlinewebfonts.com"&gt;Web Fonts&lt;/a&gt;is licensed by CC BY 4.0&lt;/div&gt;
</div>
</div>
</body>
</html>

View File

@ -1,50 +1,49 @@
import axios, { type AxiosError, type AxiosInstance } from 'axios'
import axios, { type AxiosError, type AxiosInstance } from 'axios';
// SSR-safe guard for browser-only features
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
function getCsrfTokenFromMeta(): string | null {
if (!isBrowser) return null
const el = document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null
return el?.content ?? null
if (!isBrowser) return null;
const el = document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null;
return el?.content ?? null;
}
// Create a preconfigured Axios instance for the app
const http: AxiosInstance = axios.create({
baseURL: '/',
withCredentials: true, // include cookies for same-origin requests
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
},
// If you use Laravel Sanctum's CSRF cookie, these defaults help automatically send it
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
timeout: 30000,
})
baseURL: '/',
withCredentials: true, // include cookies for same-origin requests
headers: {
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
},
// If you use Laravel Sanctum's CSRF cookie, these defaults help automatically send it
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
timeout: 30000,
});
// Attach CSRF token from Blade <meta name="csrf-token" ...> when present
http.interceptors.request.use((config) => {
const token = getCsrfTokenFromMeta()
if (token) {
// Laravel will accept either X-CSRF-TOKEN (meta) or X-XSRF-TOKEN (cookie)
config.headers = config.headers ?? {}
;(config.headers as Record<string, string>)['X-CSRF-TOKEN'] = token
}
return config
})
const token = getCsrfTokenFromMeta();
if (token) {
// Laravel will accept either X-CSRF-TOKEN (meta) or X-XSRF-TOKEN (cookie)
config.headers = config.headers ?? {};
(config.headers as Record<string, string>)['X-CSRF-TOKEN'] = token;
}
return config;
});
// Basic error passthrough; customize as needed
http.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
// Example handling: if (error.response?.status === 401) { /* redirect to login */ }
// Example handling: if (error.response?.status === 419) { /* CSRF token mismatch */ }
return Promise.reject(error)
}
)
export type { AxiosError, AxiosInstance }
export { http }
export default http
(response) => response,
async (error: AxiosError) => {
// Example handling: if (error.response?.status === 401) { /* redirect to login */ }
// Example handling: if (error.response?.status === 419) { /* CSRF token mismatch */ }
return Promise.reject(error);
},
);
export { http };
export type { AxiosError, AxiosInstance };
export default http;

View File

@ -15,7 +15,7 @@ const emit = defineEmits<{
(e: 'selectDraw', count: number): void;
}>();
const handleSelection = async (count: number) => {
const handleSelection = async (count: number, provider: 'stripe' | 'wise' = 'stripe') => {
drawCount.value = count;
if (count == 1 && tarotStore.freeDrawsRemaining <= 0) {
@ -23,7 +23,11 @@ const handleSelection = async (count: number) => {
return;
}
if (count > 1 && tarotStore.paidDrawsRemaining < count) {
await redirectToStripeCheckout(count);
if (provider === 'wise') {
await redirectToWisePayment(count);
} else {
await redirectToStripeCheckout(count);
}
return;
}
emit('selectDraw', count);
@ -56,6 +60,30 @@ const redirectToStripeCheckout = async (count: number) => {
}
};
const redirectToWisePayment = async (count: number) => {
loading.value = true;
try {
// 1. Create payment record in backend
const res = await axios.post('/create-wise-payment', { count });
const { paymentUrl, clientSessionId } = res.data;
// 2. Store client session ID in sessionStorage to verify later
sessionStorage.setItem('wise_client_session_id', clientSessionId);
// 3. Redirect to Wise payment link
if (paymentUrl) {
window.location.href = paymentUrl;
} else {
alert('Payment link not configured. Please contact support.');
}
} catch (error) {
console.error('Error initiating Wise payment:', error);
alert('Payment processing failed. Please try again.');
} finally {
loading.value = false;
}
};
// Computed to disable the free draw button if used
const isFreeDrawUsed = computed(() => tarotStore.freeDrawsRemaining <= 0);
@ -188,16 +216,18 @@ const clearHover = () => {
Une analyse approfondie de votre situation avec des conseils stratégiques personnalisés.
</p>
</div>
<button
class="group relative mt-2 flex h-12 w-full items-center justify-center overflow-hidden rounded-full bg-[var(--subtle-gold)] px-6 font-bold tracking-wide text-[var(--midnight-blue)] shadow-md transition-all duration-300 hover:shadow-lg"
@click="handleSelection(6)"
>
<!-- Button shine effect -->
<span
class="absolute inset-0 -translate-x-full transform bg-gradient-to-r from-transparent via-white/40 to-transparent transition-transform duration-700 group-hover:translate-x-full"
></span>
<span class="relative">Découvrir</span>
</button>
<div class="mt-2 flex gap-2">
<button
class="group relative flex h-12 flex-1 items-center justify-center overflow-hidden rounded-full bg-[var(--subtle-gold)] px-6 font-bold tracking-wide text-[var(--midnight-blue)] shadow-md transition-all duration-300 hover:shadow-lg"
@click="handleSelection(6, 'wise')"
title="Pay with Stripe"
>
<span
class="absolute inset-0 -translate-x-full transform bg-gradient-to-r from-transparent via-white/40 to-transparent transition-transform duration-700 group-hover:translate-x-full"
></span>
<span class="relative">Découvrir</span>
</button>
</div>
</div>
<!-- Premium plus option -->
@ -239,16 +269,18 @@ const clearHover = () => {
Une lecture complète sur tous les aspects de votre vie : amour, carrière, spiritualité et abondance.
</p>
</div>
<button
class="group relative mt-2 flex h-12 w-full items-center justify-center overflow-hidden rounded-full bg-[var(--subtle-gold)] px-6 font-bold tracking-wide text-[var(--midnight-blue)] shadow-sm transition-all duration-300"
@click="handleSelection(18)"
>
<!-- Button shine effect -->
<span
class="absolute inset-0 -translate-x-full transform bg-gradient-to-r from-transparent via-white/40 to-transparent transition-transform duration-700 group-hover:translate-x-full"
></span>
<span class="relative">Explorer</span>
</button>
<div class="mt-2 flex gap-2">
<button
class="group relative flex h-12 flex-1 items-center justify-center overflow-hidden rounded-full bg-[var(--subtle-gold)] px-6 font-bold tracking-wide text-[var(--midnight-blue)] shadow-sm transition-all duration-300"
@click="handleSelection(18, 'wise')"
title="Pay with Wise"
>
<span
class="absolute inset-0 -translate-x-full transform bg-gradient-to-r from-transparent via-white/40 to-transparent transition-transform duration-700 group-hover:translate-x-full"
></span>
<span class="relative">Explorer</span>
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,148 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import axios from 'axios';
import { onMounted, ref } from 'vue';
interface Props {
message: string;
clientSessionId: string;
paymentProvider: 'stripe' | 'wise';
}
const props = defineProps<Props>();
const checking = ref(true);
const pollCount = ref(0);
const maxPolls = 30; // Poll for 5 minutes (30 * 10 seconds)
// Poll payment status every 10 seconds
const checkPaymentStatus = async () => {
try {
const endpoint = props.paymentProvider === 'wise' ? '/wise/validate-payment' : '/stripe/validate-payment';
const response = await axios.get(endpoint, {
params: { client_session_id: props.clientSessionId },
});
if (response.data.success) {
// Payment succeeded! Redirect to success page
window.location.href = `/success?client_session_id=${props.clientSessionId}`;
} else {
pollCount.value++;
if (pollCount.value >= maxPolls) {
// Stop polling after max attempts
checking.value = false;
} else {
// Continue polling
setTimeout(checkPaymentStatus, 10000); // Check again in 10 seconds
}
}
} catch (error) {
console.error('Error checking payment status:', error);
pollCount.value++;
if (pollCount.value < maxPolls) {
setTimeout(checkPaymentStatus, 10000);
} else {
checking.value = false;
}
}
};
onMounted(() => {
// Start polling after 2 seconds
setTimeout(checkPaymentStatus, 2000);
});
const refreshPage = () => {
window.location.reload();
};
const goHome = () => {
router.visit('/');
};
</script>
<template>
<div class="flex min-h-screen items-center justify-center bg-gradient-to-br from-[var(--light-ivory)] via-white to-[var(--linen)] px-4">
<div class="w-full max-w-md rounded-2xl border border-[var(--linen)] bg-white/90 p-8 shadow-xl backdrop-blur-sm">
<!-- Animated Loading Icon -->
<div class="mb-6 flex justify-center">
<div class="relative h-20 w-20">
<svg class="animate-spin text-[var(--subtle-gold)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
</div>
<!-- Title -->
<h1 class="mb-4 text-center text-2xl font-bold text-[var(--midnight-blue)] md:text-3xl">Payment en cours</h1>
<!-- Message -->
<p class="mb-6 text-center text-base text-[var(--spiritual-earth)]">
{{ message }}
</p>
<!-- Provider Info -->
<div class="mb-6 rounded-lg bg-[var(--light-ivory)] p-4">
<p class="text-center text-sm text-[var(--midnight-blue)]">
<span class="font-semibold">Fournisseur de paiement:</span>
<span class="ml-2 capitalize">{{ paymentProvider }}</span>
</p>
<p v-if="checking" class="mt-2 text-center text-xs text-[var(--spiritual-earth)]">
Vérification automatique en cours... ({{ pollCount }}/{{ maxPolls }})
</p>
</div>
<!-- Status Message -->
<div v-if="!checking" class="mb-6 rounded-lg border border-yellow-300 bg-yellow-50 p-4">
<p class="text-center text-sm text-yellow-800">
Le paiement prend plus de temps que prévu. Cela peut prendre quelques minutes pour que {{ paymentProvider }} traite votre
paiement.
</p>
</div>
<!-- Action Buttons -->
<div class="flex flex-col gap-3">
<button
v-if="!checking"
@click="refreshPage"
class="w-full rounded-full bg-[var(--subtle-gold)] px-6 py-3 font-semibold text-[var(--midnight-blue)] shadow-md transition-all duration-300 hover:shadow-lg"
>
Vérifier à nouveau
</button>
<button
@click="goHome"
class="w-full rounded-full border-2 border-[var(--midnight-blue)] bg-transparent px-6 py-3 font-semibold text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--midnight-blue)] hover:text-white"
>
Retour à l'accueil
</button>
</div>
<!-- Help Text -->
<p class="mt-6 text-center text-xs text-[var(--spiritual-earth)]">
Si le problème persiste, veuillez contacter notre support avec votre ID de session:
<br />
<code class="mt-1 block rounded bg-gray-100 px-2 py-1 text-[10px]">{{ clientSessionId }}</code>
</p>
</div>
</div>
</template>
<style scoped>
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>

View File

@ -0,0 +1,185 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import axios from 'axios';
import { onMounted, ref } from 'vue';
const checking = ref(true);
const paymentStatus = ref<'pending' | 'succeeded' | 'failed' | null>(null);
const errorMessage = ref('');
const clientSessionId = ref('');
// Check payment status
const checkPayment = async () => {
try {
// Get client session ID from sessionStorage
const storedSessionId = sessionStorage.getItem('wise_client_session_id');
if (!storedSessionId) {
errorMessage.value = 'Session non trouvée. Veuillez recommencer le processus de paiement.';
checking.value = false;
return;
}
clientSessionId.value = storedSessionId;
// Check payment status via API
const response = await axios.get('/wise/validate-payment', {
params: { client_session_id: storedSessionId },
});
if (response.data.success) {
paymentStatus.value = 'succeeded';
// Clear session storage
sessionStorage.removeItem('wise_client_session_id');
// Redirect to success page after 2 seconds
setTimeout(() => {
window.location.href = `/success?client_session_id=${storedSessionId}`;
}, 2000);
} else {
paymentStatus.value = response.data.status || 'pending';
errorMessage.value = response.data.message || 'Le paiement est en cours de vérification...';
}
} catch (error: any) {
console.error('Error checking payment:', error);
paymentStatus.value = 'failed';
errorMessage.value = error.response?.data?.message || 'Erreur lors de la vérification du paiement.';
} finally {
checking.value = false;
}
};
const retryCheck = () => {
checking.value = true;
paymentStatus.value = null;
errorMessage.value = '';
setTimeout(checkPayment, 1000);
};
const goHome = () => {
sessionStorage.removeItem('wise_client_session_id');
router.visit('/');
};
onMounted(() => {
// Check payment status after 1 second
setTimeout(checkPayment, 1000);
});
</script>
<template>
<div class="flex min-h-screen items-center justify-center bg-gradient-to-br from-[var(--light-ivory)] via-white to-[var(--linen)] px-4">
<div class="w-full max-w-md rounded-2xl border border-[var(--linen)] bg-white/90 p-8 shadow-xl backdrop-blur-sm">
<!-- Loading State -->
<div v-if="checking" class="text-center">
<div class="mb-6 flex justify-center">
<div class="relative h-20 w-20">
<svg class="animate-spin text-[var(--subtle-gold)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
</div>
<h1 class="mb-4 text-2xl font-bold text-[var(--midnight-blue)]">Vérification du paiement...</h1>
<p class="text-[var(--spiritual-earth)]">Veuillez patienter pendant que nous vérifions votre paiement Wise.</p>
</div>
<!-- Success State -->
<div v-else-if="paymentStatus === 'succeeded'" class="text-center">
<div class="mb-6 flex justify-center">
<div class="flex h-20 w-20 items-center justify-center rounded-full bg-green-100">
<svg class="h-12 w-12 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<h1 class="mb-4 text-2xl font-bold text-green-600">Paiement réussi ! 🎉</h1>
<p class="text-[var(--spiritual-earth)]">Votre paiement a été confirmé. Vous allez être redirigé vers vos cartes...</p>
</div>
<!-- Pending State -->
<div v-else-if="paymentStatus === 'pending'" class="text-center">
<div class="mb-6 flex justify-center">
<div class="flex h-20 w-20 items-center justify-center rounded-full bg-yellow-100">
<svg class="h-12 w-12 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<h1 class="mb-4 text-2xl font-bold text-yellow-600">Paiement en cours</h1>
<p class="mb-6 text-[var(--spiritual-earth)]">
{{ errorMessage || 'Votre paiement est en cours de traitement. Cela peut prendre quelques minutes.' }}
</p>
<div class="flex flex-col gap-3">
<button
@click="retryCheck"
class="w-full rounded-full bg-[var(--subtle-gold)] px-6 py-3 font-semibold text-[var(--midnight-blue)] shadow-md transition-all duration-300 hover:shadow-lg"
>
Vérifier à nouveau
</button>
<button
@click="goHome"
class="w-full rounded-full border-2 border-[var(--midnight-blue)] bg-transparent px-6 py-3 font-semibold text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--midnight-blue)] hover:text-white"
>
Retour à l'accueil
</button>
</div>
</div>
<!-- Failed State -->
<div v-else-if="paymentStatus === 'failed'" class="text-center">
<div class="mb-6 flex justify-center">
<div class="flex h-20 w-20 items-center justify-center rounded-full bg-red-100">
<svg class="h-12 w-12 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
</div>
<h1 class="mb-4 text-2xl font-bold text-red-600">Erreur de paiement</h1>
<p class="mb-6 text-[var(--spiritual-earth)]">
{{ errorMessage || 'Une erreur est survenue lors de la vérification du paiement.' }}
</p>
<div class="flex flex-col gap-3">
<button
@click="retryCheck"
class="w-full rounded-full bg-[var(--subtle-gold)] px-6 py-3 font-semibold text-[var(--midnight-blue)] shadow-md transition-all duration-300 hover:shadow-lg"
>
Réessayer
</button>
<button
@click="goHome"
class="w-full rounded-full border-2 border-[var(--midnight-blue)] bg-transparent px-6 py-3 font-semibold text-[var(--midnight-blue)] transition-all duration-300 hover:bg-[var(--midnight-blue)] hover:text-white"
>
Retour à l'accueil
</button>
</div>
</div>
<!-- Session ID Info -->
<div v-if="clientSessionId && !checking" class="mt-6 rounded-lg bg-gray-50 p-3">
<p class="text-center text-xs text-gray-600">
ID de session:
<br />
<code class="block font-mono text-[10px] break-all">{{ clientSessionId }}</code>
</p>
</div>
</div>
</div>
</template>
<style scoped>
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>

View File

@ -24,6 +24,14 @@ Route::post('/checkout-rendez-vous', [App\Http\Controllers\StripeController::cla
Route::post('/stripe/webhook', [App\Http\Controllers\WebhookController::class, 'handleWebhook']);
// Wise payment routes
Route::post('/create-wise-payment', [App\Http\Controllers\WiseController::class, 'createPaymentSession']);
Route::post('/wise/webhook', [App\Http\Controllers\WiseController::class, 'handleWebhook']);
Route::get('/wise/validate-payment', [App\Http\Controllers\WiseController::class, 'validatePayment']);
Route::get('/wise/verify', function () {
return Inertia::render('wise/VerifyPayment');
})->name('wise.verify');
Route::get('/rendez-vous', [App\Http\Controllers\AppointmentController::class, 'index']);
Route::get('/resultat', [App\Http\Controllers\CardController::class, 'cartResult']);
@ -46,7 +54,19 @@ Route::get('/success', function (Request $request) {
if ($payment) {
return Inertia::render('payments/Success', [
'paymentSuccess' => true,
'drawCount' => $payment->draw_count
'drawCount' => $payment->draw_count,
'paymentProvider' => $payment->payment_provider ?? 'stripe'
]);
}
// If payment not found as succeeded, check if it's pending (especially for Wise)
$pendingPayment = Payment::where('client_session_id', $clientSessionId)->first();
if ($pendingPayment && $pendingPayment->status === 'pending') {
return Inertia::render('payments/Pending', [
'message' => 'Payment is being processed. Please wait...',
'clientSessionId' => $clientSessionId,
'paymentProvider' => $pendingPayment->payment_provider ?? 'stripe'
]);
}