Deuna App Wallet via Widget
Prerequisites
Before implementing Deuna App Wallet payments, ensure you have:
- Enabled Deuna App Wallet in the Deuna Admin Panel
- Generated an order token via the Deuna API
- Integrated the Deuna SDK in your project
- Reviewed the Payment Widget documentation for your platform
- Selected your preferred payment mode (Numeric Code or QR) in the Admin Panel
Payment Widget Configuration
Basic Implementation
Initialize the Payment Widget with the deuna_wallet processor:
DeunaSDK.initPaymentWidget({
orderToken: '',
paymentMethods: [
{
paymentMethod: 'voucher',
processors: ['deuna_wallet'],
},
],
callbacks: {
onSuccess: (data) => {
console.log('Payment successful:', data);
// Redirect to success page
},
onError: (error) => {
console.log('Payment failed:', error);
// Show error message to user
},
onPending: (data) => {
console.log('Payment pending confirmation:', data);
// Show waiting screen
},
},
});Complete Example with All Options
DeunaSDK.initPaymentWidget({
orderToken: '',
paymentMethods: [
{
paymentMethod: 'voucher',
processors: ['deuna_wallet'],
metadata: {
// Optional: Pass additional data
orderId: 'your-internal-order-id',
},
},
],
callbacks: {
onSuccess: (response) => {
console.log('Payment confirmed:', {
orderId: response.order_id,
orderToken: response.order_token,
status: response.status,
amount: response.amount,
currency: response.currency,
});
// Verify payment on your backend
fetch('/api/verify-payment', {
method: 'POST',
body: JSON.stringify({
orderToken: response.order_token,
orderId: response.order_id,
}),
})
.then(res => res.json())
.then(data => {
if (data.verified) {
window.location.href = '/order-confirmation';
}
});
},
onError: (error) => {
console.error('Payment error:', {
code: error.code,
message: error.message,
});
// Show user-friendly error message
showErrorModal({
title: 'Payment Failed',
message: 'Unable to process your payment. Please try again.',
});
},
onPending: (response) => {
console.log('Payment pending user confirmation:', {
orderId: response.order_id,
expiresAt: response.expires_at,
});
},
onExpired: (response) => {
console.log('Payment code/QR expired:', {
orderId: response.order_id,
expiredAt: response.expired_at,
});
},
},
});Payment Modes
Understanding the Two Modes
Your merchant account is configured with one of these modes in the Admin Panel:
Mode 1: Numeric Code (3-Minute Window)
What the customer sees:
- A 6-digit numeric code displayed on screen
- Countdown timer showing remaining time (0:00-3:00)
- Instructions to open Deuna App and enter the code
Customer flow:
- Notes the 6-digit code
- Opens Deuna App
- Selects "Pay with Code"
- Enters the 6-digit code
- Confirms payment in app
- Payment confirmed immediately via webhook
Configuration: Set in Admin Panel under Deuna App Wallet processor settings
Mode 2: QR Code (15-Minute Window)
What the customer sees:
- A dynamic QR code on screen
- Countdown timer showing remaining time (0:00-15:00)
- Two options to proceed:
- Scan QR with Deuna App camera
- Click QR to open app directly
Customer flow:
-
Option A - Scan:
- Opens Deuna App camera
- Scans the QR code
- Payment page opens automatically
- Confirms payment in app
- Payment confirmed via webhook
-
Option B - Click:
- Clicks QR code or button
- Opens Deuna App directly
- Shows payment confirmation screen
- Confirms payment
- Payment confirmed via webhook
Configuration: Set in Admin Panel under Deuna App Wallet processor settings
Note: Mode selection happens once during initial setup in the Admin Panel. All subsequent orders will use the configured mode. Contact Deuna support to change modes.
Handling Payment Confirmation
Webhook-Based Confirmation (Primary)
The Deuna App Wallet sends a webhook notification to your backend when the customer confirms payment:
// Your backend webhook endpoint
app.post('/webhooks/deuna-wallet', (req, res) => {
const { order_id, order_token, status, amount, currency } = req.body;
// Verify webhook signature (recommended)
const isValid = verifyWebhookSignature(req);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Handle confirmation
if (status === 'confirmed') {
// Update your database
updateOrder(order_id, {
status: 'paid',
paidAt: new Date(),
amount: amount,
currency: currency,
});
// Send confirmation email to customer
sendConfirmationEmail(order_id);
}
// Acknowledge receipt
res.json({ success: true });
});Polling Fallback (Secondary)
If webhook delivery is delayed or fails, the system polls the /order endpoint every 3 seconds:
// Client-side polling (handled automatically by SDK)
// But you can also implement it manually if needed
const pollOrderStatus = async (orderToken) => {
const maxAttempts = 60; // 3 minutes for numeric code
let attempts = 0;
while (attempts < maxAttempts) {
try {
const response = await fetch(`/api/order/${orderToken}`);
const order = await response.json();
if (order.status === 'confirmed') {
handlePaymentSuccess(order);
return;
}
if (order.status === 'expired') {
handlePaymentExpired(order);
return;
}
// Wait 3 seconds before polling again
await new Promise(resolve => setTimeout(resolve, 3000));
attempts++;
} catch (error) {
console.error('Polling error:', error);
}
}
handlePaymentExpired();
};Response Objects
Success Response
When payment is confirmed, onSuccess callback receives:
{
order_id: "order_abc123def456",
order_token: "token_xyz789",
status: "confirmed",
payment_method: "deuna_wallet",
amount: 99.99,
currency: "USD",
timestamp: "2024-03-20T14:30:45Z",
metadata: {
// Any metadata passed during initialization
}
}Error Response
When payment fails, onError callback receives:
{
code: "PAYMENT_FAILED",
message: "Unable to process payment",
order_id: "order_abc123def456",
details: {
reason: "Insufficient funds in Deuna wallet"
}
}Pending Response
When waiting for user confirmation, onPending callback receives:
{
order_id: "order_abc123def456",
order_token: "token_xyz789",
status: "pending",
payment_method: "deuna_wallet",
code_or_qr: "123456", // For numeric code mode
expires_at: "2024-03-20T14:33:45Z",
expires_in_seconds: 180
}Expired Response
When code/QR expires, onExpired callback receives:
{
order_id: "order_abc123def456",
order_token: "token_xyz789",
status: "expired",
payment_method: "deuna_wallet",
expired_at: "2024-03-20T14:33:45Z",
message: "Code has expired"
}UI Best Practices
For Numeric Code Mode
- Display the code prominently - Large, clear font
- Show countdown timer - Update every second
- Display clear instructions:
- "Open the Deuna App"
- "Select Pay with Code"
- "Enter this 6-digit code"
- "Confirm payment"
- Show expiration state - When code expires after 3 minutes
- Provide regenerate option - "Generate new code" button
For QR Code Mode
- Display QR code prominently - Centered, scalable
- Show countdown timer - Update every second
- Provide two interaction options:
- Scan QR with Deuna App
- Click QR to open app directly
- Display clear instructions
- Show expiration state - When QR expires after 15 minutes
- Provide regenerate option - "Generate new code" button
General Guidelines
- Keep modal/screen open - Do not redirect or close until payment confirmed or expired
- Disable other payment methods - Prevent selecting another method while waiting
- Mobile responsive - Ensure readability on small screens
- Accessibility - Include alt text for QR, clear color contrast for timers
- Loading states - Show spinner while generating code/QR
Common Implementation Patterns
React Hook Pattern
import { useEffect, useState } from 'react';
export const useDeunaWalletPayment = (orderToken) => {
const [status, setStatus] = useState('idle'); // idle | pending | success | error | expired
const [expiresAt, setExpiresAt] = useState(null);
useEffect(() => {
if (!orderToken) return;
DeunaSDK.initPaymentWidget({
orderToken,
paymentMethods: [
{
paymentMethod: 'voucher',
processors: ['deuna_wallet'],
},
],
callbacks: {
onPending: (data) => {
setStatus('pending');
setExpiresAt(data.expires_at);
},
onSuccess: (data) => {
setStatus('success');
},
onError: (error) => {
setStatus('error');
},
onExpired: () => {
setStatus('expired');
},
},
});
}, [orderToken]);
const regenerate = () => {
setStatus('idle');
window.location.reload();
};
return { status, expiresAt, regenerate };
};Updated about 12 hours ago