Web-View Integration

Overview

This guide demonstrates how to integrate Amwal Payment Links into your mobile application using web-view for online store transactions. The integration allows customers to complete payments seamlessly within your mobile app without redirecting to external browsers.

Payment Flow

Integration Architecture


Prerequisites

  • Amwal merchant account with API credentials
  • Mobile development environment (iOS/Android/React Native)

API Configuration

Base URL

Production: https://backend.sa.amwal.tech

Authentication

Include your API key in the request headers:

Authorization: your_api_key_here

Optional Environment Key

For sandbox testing, include:

X-Amwal-Key: sandbox-amwal-xxx

Step 1: Create Payment Link

API Endpoint

POST /payment_links/{store_id}/create

Request Example

JavaScript/Node.js

const createPaymentLink = async (orderData) => {
    const response = await fetch('https://backend.sa.amwal.tech/payment_links/your_store_id/create', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'your_api_key_here',
            // 'X-Amwal-Key': 'sandbox-amwal-xxx' // For testing
        },
        body: JSON.stringify({
            amount: orderData.amount,
            title: orderData.title,
            description: orderData.description,
            singleUse: true,
            selectedDate: orderData.expiryDate,
            client_phone_number: orderData.customerPhone,
            client_email: orderData.customerEmail,
            client_first_name: orderData.customerFirstName,
            client_last_name: orderData.customerLastName,
            language: 'en', // or 'ar'
            send_sms: false,
            send_receipt_sms: false,
            callback_url: 'https://yourstore.com/webhook/payment-complete',
            metadata: {
                order_id: orderData.orderId,
                customer_id: orderData.customerId,
                platform: 'mobile_app'
            }
        })
    });
    
    return await response.json();
};

// Usage
const orderData = {
    amount: 299.99,
    title: "Order #12345",
    description: "Premium Headphones - Wireless",
    expiryDate: "2024-12-31T23:59:59Z",
    customerPhone: "+966501234567",
    customerEmail: "[email protected]",
    customerFirstName: "Ahmed",
    customerLastName: "Al-Rashid",
    orderId: "ORD-12345",
    customerId: "CUST-789"
};

createPaymentLink(orderData)
    .then(result => {
        console.log('Payment Link Created:', result.url);
        // Open in web-view
        openPaymentWebView(result.url);
    })
    .catch(error => console.error('Error:', error));

Step 2: Mobile Web-View Integration

iOS (Swift)

import UIKit
import WebKit

class PaymentViewController: UIViewController, WKNavigationDelegate {
    @IBOutlet weak var webView: WKWebView!
    
    var paymentURL: String?
    var onPaymentComplete: ((Bool, String?) -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView.navigationDelegate = self
        
        // Configure web view for payment
        setupWebView()
        
        if let urlString = paymentURL, let url = URL(string: urlString) {
            let request = URLRequest(url: url)
            webView.load(request)
        }
    }
    
    private func setupWebView() {
        // Enable JavaScript
        webView.configuration.preferences.javaScriptEnabled = true
        
        // Set user agent for mobile optimization
        webView.customUserAgent = "YourAppName/1.0 Mobile"
    }
    
    // Handle navigation changes
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationPolicy) -> Void) {
        
        guard let url = navigationAction.request.url else {
            decisionHandler(.cancel)
            return
        }
        
        // Check for payment completion redirects
        if url.absoluteString.contains("payment-success") {
            handlePaymentSuccess()
            decisionHandler(.cancel)
            return
        } else if url.absoluteString.contains("payment-failed") {
            handlePaymentFailure("Payment was cancelled or failed")
            decisionHandler(.cancel)
            return
        }
        
        decisionHandler(.allow)
    }
    
    private func handlePaymentSuccess() {
        DispatchQueue.main.async {
            self.onPaymentComplete?(true, nil)
            self.dismiss(animated: true)
        }
    }
    
    private func handlePaymentFailure(_ error: String) {
        DispatchQueue.main.async {
            self.onPaymentComplete?(false, error)
            self.dismiss(animated: true)
        }
    }
}

// Usage in your main view controller
func openPaymentWebView(paymentURL: String) {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    if let paymentVC = storyboard.instantiateViewController(withIdentifier: "PaymentViewController") as? PaymentViewController {
        
        paymentVC.paymentURL = paymentURL
        paymentVC.onPaymentComplete = { [weak self] success, error in
            if success {
                self?.showAlert(title: "Payment Successful", message: "Your order has been processed!")
                // Refresh order status
                self?.refreshOrderStatus()
            } else {
                self?.showAlert(title: "Payment Failed", message: error ?? "Unknown error occurred")
            }
        }
        
        present(paymentVC, animated: true)
    }
}

Android (Java)

import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.WebSettings;

public class PaymentActivity extends AppCompatActivity {
    private WebView webView;
    private String paymentURL;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_payment);
        
        webView = findViewById(R.id.payment_webview);
        paymentURL = getIntent().getStringExtra("payment_url");
        
        setupWebView();
        loadPaymentURL();
    }
    
    private void setupWebView() {
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webSettings.setDomStorageEnabled(true);
        webSettings.setUserAgentString("YourAppName/1.0 Mobile");
        
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // Handle payment completion
                if (url.contains("payment-success")) {
                    handlePaymentSuccess();
                    return true;
                } else if (url.contains("payment-failed")) {
                    handlePaymentFailure("Payment was cancelled or failed");
                    return true;
                }
                return false;
            }
            
            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                // Hide loading indicator
            }
        });
    }
    
    private void loadPaymentURL() {
        if (paymentURL != null) {
            webView.loadUrl(paymentURL);
        }
    }
    
    private void handlePaymentSuccess() {
        runOnUiThread(() -> {
            // Show success message
            Toast.makeText(this, "Payment successful!", Toast.LENGTH_LONG).show();
            
            // Return success result
            Intent resultIntent = new Intent();
            resultIntent.putExtra("payment_status", "success");
            setResult(RESULT_OK, resultIntent);
            finish();
        });
    }
    
    private void handlePaymentFailure(String error) {
        runOnUiThread(() -> {
            // Show error message
            Toast.makeText(this, "Payment failed: " + error, Toast.LENGTH_LONG).show();
            
            // Return failure result
            Intent resultIntent = new Intent();
            resultIntent.putExtra("payment_status", "failed");
            resultIntent.putExtra("error", error);
            setResult(RESULT_CANCELED, resultIntent);
            finish();
        });
    }
}

// Usage in your main activity
private void openPaymentWebView(String paymentURL) {
    Intent intent = new Intent(this, PaymentActivity.class);
    intent.putExtra("payment_url", paymentURL);
    startActivityForResult(intent, PAYMENT_REQUEST_CODE);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    
    if (requestCode == PAYMENT_REQUEST_CODE) {
        if (resultCode == RESULT_OK) {
            String status = data.getStringExtra("payment_status");
            if ("success".equals(status)) {
                // Handle successful payment
                showSuccessDialog();
                refreshOrderStatus();
            }
        } else {
            String error = data.getStringExtra("error");
            // Handle payment failure
            showErrorDialog(error);
        }
    }
}

React Native

import React, { useRef } from 'react';
import { WebView } from 'react-native-webview';
import { View, Alert, StyleSheet } from 'react-native';

const PaymentWebView = ({ paymentURL, onPaymentComplete, onPaymentError }) => {
  const webViewRef = useRef(null);
  
  const handleNavigationStateChange = (navState) => {
    const { url } = navState;
    
    // Check for payment completion URLs
    if (url.includes('payment-success')) {
      onPaymentComplete();
      return;
    } else if (url.includes('payment-failed') || url.includes('payment-cancelled')) {
      onPaymentError('Payment was cancelled or failed');
      return;
    }
  };
  
  const handleError = (syntheticEvent) => {
    const { nativeEvent } = syntheticEvent;
    onPaymentError(`WebView error: ${nativeEvent.description}`);
  };
  
  const injectedJavaScript = `
    // Optional: Inject custom JavaScript for enhanced mobile experience
    (function() {
      // Add mobile-specific styling or behavior
      const meta = document.createElement('meta');
      meta.name = 'viewport';
      meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
      document.getElementsByTagName('head')[0].appendChild(meta);
    })();
    true; // Required for iOS
  `;
  
  return (
    <View style={styles.container}>
      <WebView
        ref={webViewRef}
        source={{ uri: paymentURL }}
        style={styles.webView}
        onNavigationStateChange={handleNavigationStateChange}
        onError={handleError}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        startInLoadingState={true}
        userAgent="YourAppName/1.0 Mobile"
        injectedJavaScript={injectedJavaScript}
        onShouldStartLoadWithRequest={(request) => {
          // Allow all requests by default
          return true;
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  webView: {
    flex: 1,
  },
});

// Usage in your component
const CheckoutScreen = () => {
  const [paymentURL, setPaymentURL] = useState(null);
  
  const createPaymentLink = async (orderData) => {
    try {
      const response = await fetch('https://backend.sa.amwal.tech/payment_links/your_store_id/create', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'your_api_key_here',
        },
        body: JSON.stringify({
          amount: orderData.amount,
          title: orderData.title,
          description: orderData.description,
          singleUse: true,
          client_email: orderData.customerEmail,
          client_first_name: orderData.customerFirstName,
          language: 'en',
          send_sms: true,
          callback_url: 'https://yourstore.com/webhook/payment-complete',
          metadata: {
            order_id: orderData.orderId,
            platform: 'mobile_app'
          }
        })
      });
      
      const result = await response.json();
      setPaymentURL(result.url);
    } catch (error) {
      Alert.alert('Error', 'Failed to create payment link');
    }
  };
  
  const handlePaymentComplete = () => {
    Alert.alert('Success', 'Payment completed successfully!', [
      {
        text: 'OK',
        onPress: () => {
          setPaymentURL(null);
          // Navigate to success screen or refresh orders
        }
      }
    ]);
  };
  
  const handlePaymentError = (error) => {
    Alert.alert('Payment Failed', error, [
      {
        text: 'OK',
        onPress: () => setPaymentURL(null)
      }
    ]);
  };
  
  if (paymentURL) {
    return (
      <PaymentWebView
        paymentURL={paymentURL}
        onPaymentComplete={handlePaymentComplete}
        onPaymentError={handlePaymentError}
      />
    );
  }
  
  // Your regular checkout UI here
  return (
    <View>
      {/* Your checkout form */}
    </View>
  );
};

export default CheckoutScreen;

Step 3: Handle Payment Completion

Webhook Setup

Set up a webhook endpoint to receive payment notifications and verify status:

// Express.js webhook handler
app.get('/webhook/payment-complete', async (req, res) => {
    const { transaction_id, reference_number } = req.query;
    
    if (!transaction_id) {
        return res.status(400).send('Missing transaction_id');
    }
    
    try {
        // Get payment details using the transaction_id
        const paymentDetails = await getPaymentDetails(transaction_id);
        
        if (paymentDetails && paymentDetails.payment_link.status === 'Paid') {
            // Extract order information from metadata
            const metadata = paymentDetails.payment_link.metadata || {};
            
            // Update order status in database
            if (metadata.order_id) {
                await updateOrderStatus(metadata.order_id, 'paid');
            }
            
            // Send confirmation email
            if (metadata.customer_id) {
                await sendOrderConfirmation(metadata.customer_id);
            }
            
            // Update inventory
            if (metadata.order_id) {
                await updateInventory(metadata.order_id);
            }
            
            console.log(`Payment completed for transaction: ${transaction_id}`);
        }
        
        res.status(200).send('OK');
    } catch (error) {
        console.error('Webhook processing error:', error);
        res.status(500).send('Internal Server Error');
    }
});

// Function to get payment details using transaction_id
const getPaymentDetails = async (transactionId) => {
    // Note: You'll need to map transaction_id to payment_link_id
    // This might require storing the relationship when creating payment links
    // or using a different endpoint that accepts transaction_id
    
    const response = await fetch(`https://backend.sa.amwal.tech/payment_links/${paymentLinkId}/details`, {
        method: 'POST',
        headers: {
            'Authorization': 'your_api_key_here',
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-Amwal-Key': 'sandbox-amwal-xxx' // For sandbox testing
        },
        body: JSON.stringify({})
    });
    
    return await response.json();
};

Alternative: Store Transaction Mapping

Since the webhook provides transaction_id but the API needs payment_link_id, store this mapping when creating payment links:

// When creating payment link, store the mapping
const createPaymentLink = async (orderData) => {
    const response = await fetch('https://backend.sa.amwal.tech/payment_links/your_store_id/create', {
        // ... headers and body
    });
    
    const result = await response.json();
    
    if (response.ok) {
        // Store the mapping for webhook processing
        await storePaymentMapping({
            order_id: orderData.orderId,
            payment_link_id: result.payment_link_id,
            payment_url: result.url,
            created_at: new Date()
        });
        
        return result;
    }
};

// Updated webhook handler with mapping lookup
app.get('/webhook/payment-complete', async (req, res) => {
    const { transaction_id, reference_number } = req.query;
    
    try {
        // Find the payment link associated with this transaction
        // You might need to search through recent transactions
        const paymentMapping = await findPaymentByTransaction(transaction_id);
        
        if (paymentMapping) {
            const paymentDetails = await getPaymentDetailsByLinkId(paymentMapping.payment_link_id);
            
            if (paymentDetails.payment_link.status === 'Paid') {
                // Process successful payment
                await processSuccessfulPayment(paymentMapping.order_id, transaction_id);
            }
        }
        
        res.status(200).send('OK');
    } catch (error) {
        console.error('Webhook error:', error);
        res.status(500).send('Error');
    }
});

Verify Payment Status

Always verify payment status server-side using the payment link ID:

const verifyPaymentStatus = async (paymentLinkId) => {
    const response = await fetch(`https://backend.sa.amwal.tech/payment_links/${paymentLinkId}/details`, {
        method: 'POST',
        headers: {
            'Authorization': 'your_api_key_here',
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'X-Amwal-Key': 'sandbox-amwal-xxx' // For sandbox
        },
        body: JSON.stringify({})
    });
    
    const data = await response.json();
    return data.payment_link.status === 'Paid';
};

Step 4: Error Handling

Common Scenarios

const handlePaymentErrors = (error) => {
    switch (error.error_code) {
        case 'LINK_EXPIRED':
            // Create new payment link
            showAlert('Payment link expired. Creating new one...');
            createNewPaymentLink();
            break;
            
        case 'INVALID_AMOUNT':
            // Handle amount validation
            showAlert('Invalid payment amount. Please try again.');
            break;
            
        case 'NETWORK_ERROR':
            // Handle network issues
            showAlert('Network error. Please check your connection.');
            break;
            
        default:
            showAlert('Payment failed. Please try again.');
    }
};

Best Practices

1. Security

  • Always validate payments server-side
  • Implement webhook signature verification
  • Use HTTPS for all API calls
  • Store API keys securely (not in client code)

2. User Experience

  • Show loading indicators during API calls
  • Handle network timeouts gracefully
  • Provide clear error messages
  • Allow users to retry failed payments

3. Testing

  • Use sandbox environment for development
  • Test various payment scenarios
  • Test network connectivity issues
  • Verify webhook handling

4. Performance

  • Cache payment links when appropriate
  • Implement proper timeout handling
  • Optimize web-view loading