API DOCUMENTATION

PAYMENT CARD SCAN API
MobiCard ScanAPI

Use this multi-platform API to enhance your end-user's card checkout experience by letting them scan (or upload) their payment cards. Extract the card number, expiry date, brand information, EXIF data, Risk information and perform validation checks and card tokenization automatically. Code samples are provided.

With two implementation methods to select from depending on your use case.

Method 1
Use method 1 with our scan card UI template (HTML/CSS/JS WebStack) to quickly get started or method 2 below.
Method 2
Incase you need to have more control over your UI or when developing mobile applications (by securely passing the card image base64 string).

Technical Difference: Method 1 accepts the actual image file ('mobicard_scan_card_photo') as multipart form-data while Method 2 accepts the card image's base64 string ('mobicard_scan_card_photo_base64_string') within the initial request access token call.

Select a method below:

Method 1: Camera/Upload UI Overview

This method provides a complete UI solution for card scanning. The process :

  1. Generate a signed JWT token with embedded request to request for an access token
  2. Receive transaction details as well as the scan card URL endpoint
  3. Use the provided UI (scan tab + upload tab) to capture card image
  4. Submit image via AJAX to the scan card endpoint
  5. Receive parsed card data in response
Best For: Quick implementation with ready-to-use UI components. Perfect for web applications where you want a consistent card scanning interface.
Note: The value for the "status" response parameter is always either "SUCCESS" or "FAILED" for this API. Use this to determine subsequent actions.

Step 1: Generate Access Token

First, generate a JWT token and request for an access token (CURL). This is a one-time call per scan session.

On success response ('status_code' value '200'), retrieve the values for the mobicard_transaction_access_token, mobicard_token_id and mobicard_scan_card_url fields; to be used in the subsequent steps.

PHP
METHOD 1
<?php

// Mandatory claims

// You may copy paste the full sample code provided below, on this page, under the code section titled : (Sample Code : Full PHP Implementation (Method 1))

$mobicard_version = "2.0";
$mobicard_mode = "LIVE"; // production
$mobicard_merchant_id = "4";
$mobicard_api_key = "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9";
$mobicard_secret_key = "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9";

$mobicard_token_id = abs(rand(1000000,1000000000));
$mobicard_token_id = "$mobicard_token_id";

$mobicard_txn_reference = abs(rand(1000000,1000000000));
$mobicard_txn_reference = "$mobicard_txn_reference";

$mobicard_service_id = "20000"; // Scan Card service ID
$mobicard_service_type = "1"; // Use '1' for CARD SCAN METHOD 1

$mobicard_extra_data = "your_custom_data_here_will_be_returned_as_is";

// Create JWT Header
$mobicard_jwt_header = [
    "typ" => "JWT",
    "alg" => "HS256"
];
$mobicard_jwt_header = rtrim(strtr(base64_encode(json_encode($mobicard_jwt_header)), '+/', '-_'), '=');

// Create JWT Payload
$mobicard_jwt_payload = array(
    "mobicard_version" => "$mobicard_version",
    "mobicard_mode" => "$mobicard_mode",
    "mobicard_merchant_id" => "$mobicard_merchant_id",
    "mobicard_api_key" => "$mobicard_api_key",
    "mobicard_service_id" => "$mobicard_service_id",
    "mobicard_service_type" => "$mobicard_service_type",
    "mobicard_token_id" => "$mobicard_token_id",
    "mobicard_txn_reference" => "$mobicard_txn_reference",
    "mobicard_extra_data" => "$mobicard_extra_data"
);

$mobicard_jwt_payload = rtrim(strtr(base64_encode(json_encode($mobicard_jwt_payload)), '+/', '-_'), '=');

// Generate Signature
$header_payload = $mobicard_jwt_header . '.' . $mobicard_jwt_payload;
$mobicard_jwt_signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $header_payload, $mobicard_secret_key, true)), '+/', '-_'), '=');

// Create Final JWT
$mobicard_auth_jwt = "$mobicard_jwt_header.$mobicard_jwt_payload.$mobicard_jwt_signature";

// Request Access Token
$mobicard_request_access_token_url = "https://mobicardsystems.com/api/v1/card_scan";

$mobicard_curl_post_data = array('mobicard_auth_jwt' => $mobicard_auth_jwt);

$curl_mobicard = curl_init();
curl_setopt($curl_mobicard, CURLOPT_URL, $mobicard_request_access_token_url);
curl_setopt($curl_mobicard, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl_mobicard, CURLOPT_POST, true);
curl_setopt($curl_mobicard, CURLOPT_POSTFIELDS, json_encode($mobicard_curl_post_data));
curl_setopt($curl_mobicard, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl_mobicard, CURLOPT_SSL_VERIFYPEER, false);
$mobicard_curl_response = curl_exec($curl_mobicard);
curl_close($curl_mobicard);

// Parse Response
$mobicard_curl_response = json_decode($mobicard_curl_response, true);

// var_dump($mobicard_curl_response);

if($mobicard_curl_response && $mobicard_curl_response['status_code'] == "200") {
    $status_code = $mobicard_curl_response['status_code'];
    $status_message = $mobicard_curl_response['status_message'];
    $mobicard_transaction_access_token = $mobicard_curl_response['mobicard_transaction_access_token'];
    $mobicard_token_id = $mobicard_curl_response['mobicard_token_id'];
    $mobicard_txn_reference = $mobicard_curl_response['mobicard_txn_reference'];
    $mobicard_scan_card_url = $mobicard_curl_response['mobicard_scan_card_url'];
    
    // These variables are now available for the UI script
    // $mobicard_transaction_access_token, $mobicard_token_id, $mobicard_scan_card_url
} else {
    // Handle error
    var_dump($mobicard_curl_response);
    exit();
}

cURL
METHOD 1
#!/bin/bash

# Configuration
MOBICARD_VERSION="2.0"
MOBICARD_MODE="LIVE"
MOBICARD_MERCHANT_ID="4"
MOBICARD_API_KEY="YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9"
MOBICARD_SECRET_KEY="NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9"
MOBICARD_TOKEN_ID=$(shuf -i 1000000-1000000000 -n 1)
MOBICARD_TXN_REFERENCE=$(shuf -i 1000000-1000000000 -n 1)
MOBICARD_SERVICE_ID="20000"
MOBICARD_SERVICE_TYPE="1"
MOBICARD_EXTRA_DATA="your_custom_data_here_will_be_returned_as_is"

# Create JWT Header
JWT_HEADER=$(echo -n '{"typ":"JWT","alg":"HS256"}' | base64 | tr '+/' '-_' | tr -d '=')

# Create JWT Payload
PAYLOAD_JSON=$(cat << EOF
{
  "mobicard_version": "$MOBICARD_VERSION",
  "mobicard_mode": "$MOBICARD_MODE",
  "mobicard_merchant_id": "$MOBICARD_MERCHANT_ID",
  "mobicard_api_key": "$MOBICARD_API_KEY",
  "mobicard_service_id": "$MOBICARD_SERVICE_ID",
  "mobicard_service_type": "$MOBICARD_SERVICE_TYPE",
  "mobicard_token_id": "$MOBICARD_TOKEN_ID",
  "mobicard_txn_reference": "$MOBICARD_TXN_REFERENCE",
  "mobicard_extra_data": "$MOBICARD_EXTRA_DATA"
}
EOF
)

JWT_PAYLOAD=$(echo -n "$PAYLOAD_JSON" | base64 | tr '+/' '-_' | tr -d '=')

# Generate Signature
HEADER_PAYLOAD="$JWT_HEADER.$JWT_PAYLOAD"
JWT_SIGNATURE=$(echo -n "$HEADER_PAYLOAD" | openssl dgst -sha256 -hmac "$MOBICARD_SECRET_KEY" -binary | base64 | tr '+/' '-_' | tr -d '=')

# Create Final JWT
MOBICARD_AUTH_JWT="$JWT_HEADER.$JWT_PAYLOAD.$JWT_SIGNATURE"

# Request Access Token
API_URL="https://mobicardsystems.com/api/v1/card_scan"

RESPONSE=$(curl -X POST "$API_URL" \
  -H "Content-Type: application/json" \
  -d "{\"mobicard_auth_jwt\":\"$MOBICARD_AUTH_JWT\"}" \
  --silent)

# Parse and display response
echo "$RESPONSE" | python -m json.tool
Python
METHOD 1
import json
import base64
import hmac
import hashlib
import random
import requests
from urllib.parse import quote

# Mandatory claims
mobicard_version = "2.0"
mobicard_mode = "LIVE"  # production
mobicard_merchant_id = "4"
mobicard_api_key = "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9"
mobicard_secret_key = "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9"

mobicard_token_id = str(random.randint(1000000, 1000000000))
mobicard_txn_reference = str(random.randint(1000000, 1000000000))
mobicard_service_id = "20000"  # Scan Card service ID
mobicard_service_type = "1"  # Use '1' for CARD SCAN METHOD 1
mobicard_extra_data = "your_custom_data_here_will_be_returned_as_is"

# Create JWT Header
jwt_header = {
    "typ": "JWT",
    "alg": "HS256"
}
jwt_header_encoded = base64.urlsafe_b64encode(json.dumps(jwt_header).encode()).decode().rstrip('=')

# Create JWT Payload
jwt_payload = {
    "mobicard_version": mobicard_version,
    "mobicard_mode": mobicard_mode,
    "mobicard_merchant_id": mobicard_merchant_id,
    "mobicard_api_key": mobicard_api_key,
    "mobicard_service_id": mobicard_service_id,
    "mobicard_service_type": mobicard_service_type,
    "mobicard_token_id": mobicard_token_id,
    "mobicard_txn_reference": mobicard_txn_reference,
    "mobicard_extra_data": mobicard_extra_data
}

jwt_payload_encoded = base64.urlsafe_b64encode(json.dumps(jwt_payload).encode()).decode().rstrip('=')

# Generate Signature
header_payload = f"{jwt_header_encoded}.{jwt_payload_encoded}"
signature = hmac.new(
    mobicard_secret_key.encode(),
    header_payload.encode(),
    hashlib.sha256
).digest()
jwt_signature = base64.urlsafe_b64encode(signature).decode().rstrip('=')

# Create Final JWT
mobicard_auth_jwt = f"{jwt_header_encoded}.{jwt_payload_encoded}.{jwt_signature}"

# Request Access Token
url = "https://mobicardsystems.com/api/v1/card_scan"
payload = {"mobicard_auth_jwt": mobicard_auth_jwt}

try:
    response = requests.post(url, json=payload, verify=False)
    response_data = response.json()
    
    if response_data.get('status_code') == "200":
        mobicard_transaction_access_token = response_data['mobicard_transaction_access_token']
        mobicard_token_id = response_data['mobicard_token_id']
        mobicard_scan_card_url = response_data['mobicard_scan_card_url']
        
        print("Access Token Generated Successfully!")
        print(f"Transaction Access Token: {mobicard_transaction_access_token}")
        print(f"Token ID: {mobicard_token_id}")
        print(f"Scan Card URL: {mobicard_scan_card_url}")
    else:
        print(f"Error: {response_data}")
        
except Exception as e:
    print(f"Request failed: {e}")
Node JS
METHOD 1
const crypto = require('crypto');
const axios = require('axios');

// Mandatory claims
const mobicard_version = "2.0";
const mobicard_mode = "LIVE"; // production
const mobicard_merchant_id = "4";
const mobicard_api_key = "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9";
const mobicard_secret_key = "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9";

const mobicard_token_id = Math.floor(Math.random() * (1000000000 - 1000000 + 1)) + 1000000;
const mobicard_txn_reference = Math.floor(Math.random() * (1000000000 - 1000000 + 1)) + 1000000;
const mobicard_service_id = "20000"; // Scan Card service ID
const mobicard_service_type = "1"; // Use '1' for CARD SCAN METHOD 1
const mobicard_extra_data = "your_custom_data_here_will_be_returned_as_is";

// Create JWT Header
const jwtHeader = {
    "typ": "JWT",
    "alg": "HS256"
};
const encodedHeader = Buffer.from(JSON.stringify(jwtHeader)).toString('base64url');

// Create JWT Payload
const jwtPayload = {
    "mobicard_version": mobicard_version,
    "mobicard_mode": mobicard_mode,
    "mobicard_merchant_id": mobicard_merchant_id,
    "mobicard_api_key": mobicard_api_key,
    "mobicard_service_id": mobicard_service_id,
    "mobicard_service_type": mobicard_service_type,
    "mobicard_token_id": mobicard_token_id.toString(),
    "mobicard_txn_reference": mobicard_txn_reference.toString(),
    "mobicard_extra_data": mobicard_extra_data
};

const encodedPayload = Buffer.from(JSON.stringify(jwtPayload)).toString('base64url');

// Generate Signature
const headerPayload = `${encodedHeader}.${encodedPayload}`;
const signature = crypto.createHmac('sha256', mobicard_secret_key)
    .update(headerPayload)
    .digest('base64url');

// Create Final JWT
const mobicard_auth_jwt = `${encodedHeader}.${encodedPayload}.${signature}`;

// Request Access Token
async function requestAccessToken() {
    const url = "https://mobicardsystems.com/api/v1/card_scan";
    const payload = { mobicard_auth_jwt };
    
    try {
        const response = await axios.post(url, payload, {
            httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
        });
        
        const responseData = response.data;
        
        if (responseData.status_code === "200") {
            const mobicard_transaction_access_token = responseData.mobicard_transaction_access_token;
            const mobicard_token_id = responseData.mobicard_token_id;
            const mobicard_scan_card_url = responseData.mobicard_scan_card_url;
            
            console.log("Access Token Generated Successfully!");
            console.log(`Transaction Access Token: ${mobicard_transaction_access_token}`);
            console.log(`Token ID: ${mobicard_token_id}`);
            console.log(`Scan Card URL: ${mobicard_scan_card_url}`);
            
            return {
                mobicard_transaction_access_token,
                mobicard_token_id,
                mobicard_scan_card_url
            };
        } else {
            console.error("Error:", responseData);
            throw new Error(responseData.status_message);
        }
    } catch (error) {
        console.error("Request failed:", error.message);
        throw error;
    }
}

// Execute the request
requestAccessToken();
Java
METHOD 1
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import java.util.Random;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

public class MobicardTokenGenerator {
    
    public static void main(String[] args) {
        try {
            // Configuration
            String mobicardVersion = "2.0";
            String mobicardMode = "LIVE"; // production
            String mobicardMerchantId = "4";
            String mobicardApiKey = "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9";
            String mobicardSecretKey = "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9";
            
            Random random = new Random();
            String mobicardTokenId = String.valueOf(random.nextInt(900000000) + 1000000);
            String mobicardTxnReference = String.valueOf(random.nextInt(900000000) + 1000000);
            String mobicardServiceId = "20000"; // Card Services APIs
            String mobicardServiceType = "1"; // Use '1' for CARD SCAN METHOD 1
            String mobicardExtraData = "your_custom_data_here_will_be_returned_as_is";
            
            // Create JWT Header
            Map jwtHeader = new HashMap<>();
            jwtHeader.put("typ", "JWT");
            jwtHeader.put("alg", "HS256");
            
            String encodedHeader = base64UrlEncode(new Gson().toJson(jwtHeader));
            
            // Create JWT Payload
            Map jwtPayload = new HashMap<>();
            jwtPayload.put("mobicard_version", mobicardVersion);
            jwtPayload.put("mobicard_mode", mobicardMode);
            jwtPayload.put("mobicard_merchant_id", mobicardMerchantId);
            jwtPayload.put("mobicard_api_key", mobicardApiKey);
            jwtPayload.put("mobicard_service_id", mobicardServiceId);
            jwtPayload.put("mobicard_service_type", mobicardServiceType);
            jwtPayload.put("mobicard_token_id", mobicardTokenId);
            jwtPayload.put("mobicard_txn_reference", mobicardTxnReference);
            jwtPayload.put("mobicard_extra_data", mobicardExtraData);
            
            String encodedPayload = base64UrlEncode(new Gson().toJson(jwtPayload));
            
            // Generate Signature
            String headerPayload = encodedHeader + "." + encodedPayload;
            String signature = generateHMAC(headerPayload, mobicardSecretKey);
            
            // Create Final JWT
            String mobicardAuthJwt = encodedHeader + "." + encodedPayload + "." + signature;
            
            // Request Access Token
            String response = requestAccessToken(mobicardAuthJwt);
            
            // Parse response
            JsonObject jsonResponse = new Gson().fromJson(response, JsonObject.class);
            
            if (jsonResponse.get("status_code").getAsString().equals("200")) {
                String mobicardTransactionAccessToken = jsonResponse.get("mobicard_transaction_access_token").getAsString();
                String mobicardTokenIdResponse = jsonResponse.get("mobicard_token_id").getAsString();
                String mobicardScanCardUrl = jsonResponse.get("mobicard_scan_card_url").getAsString();
                
                System.out.println("Access Token Generated Successfully!");
                System.out.println("Transaction Access Token: " + mobicardTransactionAccessToken);
                System.out.println("Token ID: " + mobicardTokenIdResponse);
                System.out.println("Scan Card URL: " + mobicardScanCardUrl);
            } else {
                System.err.println("Error: " + jsonResponse.get("status_message").getAsString());
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static String base64UrlEncode(String data) {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(data.getBytes());
    }
    
    private static String generateHMAC(String data, String key) throws Exception {
        Mac sha256Hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "HmacSHA256");
        sha256Hmac.init(secretKey);
        byte[] hmacBytes = sha256Hmac.doFinal(data.getBytes());
        return base64UrlEncode(new String(hmacBytes));
    }
    
    private static String requestAccessToken(String jwt) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        Map requestBody = new HashMap<>();
        requestBody.put("mobicard_auth_jwt", jwt);
        
        String jsonBody = new Gson().toJson(requestBody);
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://mobicardsystems.com/api/v1/card_scan"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();
        
        HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
        return response.body();
    }
}

Step 2 - Option 1: HTML & JavaScript Implementation

Pass the mobicard_transaction_access_token, mobicard_token_id and mobicard_scan_card_url values retrieved from Step 1 to the JavaScript as shown below.

Look for this code section in the 'Step 2: HTML & JavaScript Implementation' Code that follows below this: Scan Card Inputs

The JavaScript code handles camera access, card image capture, and AJAX submission to the scan endpoint.

PHP: You may copy paste the full working sample code provided below, on this page, under the code section titled : (Sample Code : Full PHP Implementation (Method 1))

Step 2: HTML & JavaScript Implementation
METHOD 1
<!DOCTYPE html>
<html>
<head>

    <meta charset="utf-8">
	
    <title>Mobicard | Scan or Upload Card</title>
	
    <meta name="viewport" content="width=device-width, initial-scale=1">
	<meta name="copyright" content="MobiCard">
    <meta name="author" content="MobiCard">

    <!-- PWA -->
    <link rel="manifest" href="public/manifest.json">
    <meta name="theme-color" content="#000000">

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">

    <style>
        body { background:#000; color:#fff; }

        .nav-tabs .nav-link { border-radius:0; color:#aaa; }
        .nav-tabs .nav-link.active { background:#111; color:#fff; border-color:#333; }

        .camera-container {
            position: relative;
            width: 100%;
            height: 85vh;
            background: #000;
            overflow: hidden;
        }

        video { width:100%; height:100%; object-fit:cover; }

        .scan-frame {
            position:absolute;
            top:50%; left:50%;
            width:85%; height:55%;
            transform:translate(-50%,-50%);
            border:2px solid rgba(255,255,255,0.8);
            border-radius:12px;
            box-shadow:0 0 0 9999px rgba(0,0,0,0.4);
            pointer-events:none;
            transition: all 0.3s ease;
        }

        .scan-frame.locked {
            border-color:#0f0;
            box-shadow:0 0 20px #0f0, 0 0 0 9999px rgba(0,0,0,0.5);
        }

        .scan-frame.bad { border-color:#f00; }

        .scan-hint {
            position:absolute;
            top:20px;
            width:100%;
            text-align:center;
            font-size:14px;
            color:#0f0;
            text-shadow:0 0 10px #0f0;
            z-index:20;
        }
		
        .scan-line {
            position:absolute;
            top:0; left:0;
            width:100%; height:3px;
            background:rgba(0,255,0,0.8);
            animation: scanmove 2s linear infinite;
            z-index:5;
        }

        @keyframes  scanmove {
            0% { top:0; }
            100% { top:100%; }
        }

        .scan-loader {
            position:absolute;
            bottom:80px;
            width:100%;
            text-align:center;
            display:none;
            color:#0f0;
        }

        .floating-btn {
            position:absolute;
            bottom:20px;
            left:50%;
            transform:translateX(-50%);
            z-index:10;
        }

        .upload-box {
            border:2px dashed #444;
            padding:40px;
            text-align:center;
            margin-top:40px;
        }

        .result-label { color:#777; font-size:12px; }
    </style>
	
	
	
	<style>
		
		
		/* Uploading message styles */
		.uploading-message {
			display: none;
			position: fixed;
			top: 50%;
			left: 50%;
			transform: translate(-50%, -50%);
			background: #000000 !important;
			color: white !important;
			padding: 20px 30px;
			border-radius: 10px;
			z-index: 99999;
			font-weight: bold;
			text-align: center;
			box-shadow: 0 0 20px rgba(0,0,0,0.5);
			border: 2px solid #fff;
			font-size: 16px;
			min-width: 300px;
		}
		
		/* Overlay to block interaction while uploading */
		.uploading-overlay {
			display: none;
			position: fixed;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			background: rgba(0,0,0,0.7);
			z-index: 99998;
		}
		
		/* Lock screen styles */
		.lock-screen {
			display: none;
			position: absolute;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			background: rgba(0, 0, 0, 0.85);
			z-index: 30;
			flex-direction: column;
			justify-content: center;
			align-items: center;
			color: #fff;
			text-align: center;
		}
		
		.refresh-icon {
			font-size: 48px;
			margin-bottom: 20px;
			cursor: pointer;
			animation: pulse 2s infinite;
			color: #0f0;
		}
		
		@keyframes  pulse {
			0% { transform: scale(1); opacity: 0.7; }
			50% { transform: scale(1.1); opacity: 1; }
			100% { transform: scale(1); opacity: 0.7; }
		}
		
		.lock-message {
			font-size: 16px;
			color: #ccc;
			margin-bottom: 20px;
		}
		
		/* Mobile-specific scan frame adjustments */
		@media (max-width: 768px) {
			.scan-frame {
				width: 92% !important;
				height: 36% !important;
				border-width: 3px;
			}
		}
		
		/* Desktop-specific scan frame adjustments */
		@media (min-width: 769px) {
			.scan-frame {
				margin-top: -20px;
				width: 45% !important;
				height: 67% !important;
			}
		}
			
	</style>
	

	<style>

		/* ===================== LIGHT RESULT MODAL UI ===================== */

		#result_modal .modal-dialog {
			max-width: 420px;
		}

		#result_modal .modal-content {
			background: linear-gradient(180deg, #ffffff, #f6f8fb);
			color: #0b1220;
			border-radius: 18px;
			border: 1px solid rgba(0,0,0,0.06);
			box-shadow: 0 20px 50px rgba(0,0,0,0.2);
			overflow: hidden;
		}

		/* Header */
		#result_modal .modal-header {
			border-bottom: 1px solid rgba(0,0,0,0.08);
			background: linear-gradient(90deg, #f8fbff, #eef3f9);
			padding: 18px 20px;
			display: flex;
			align-items: center;
			justify-content: space-between;
		}

		/* Title */
		#result_modal .modal-title {
			font-weight: 700;
			letter-spacing: 0.3px;
			font-size: 18px;
			color: #0b1220;
		}

		/* Success check animation */
		#result_modal .modal-header::after {
			/* content: "✓"; */
			width: 34px;
			height: 34px;
			border-radius: 50%;
			background: linear-gradient(135deg, #22c55e, #16a34a);
			color: #fff;
			display: inline-flex;
			align-items: center;
			justify-content: center;
			font-weight: bold;
			font-size: 18px;
			box-shadow: 0 6px 18px rgba(34,197,94,0.5);
			animation: popSuccess 0.6s ease-out;
		}

		/* Success animation */
		@keyframes  popSuccess {
			0% { transform: scale(0); opacity: 0; }
			60% { transform: scale(1.2); opacity: 1; }
			100% { transform: scale(1); }
		}

		/* Body */
		#result_modal .modal-body {
			padding: 20px;
		}

		/* Card blocks */
		#result_modal .modal-body .mb-2 {
			background: #ffffff;
			border-radius: 14px;
			padding: 12px 14px;
			margin-bottom: 12px !important;
			border: 1px solid rgba(0,0,0,0.06);
			box-shadow: 0 4px 12px rgba(0,0,0,0.04);
		}

		/* Labels */
		#result_modal .result-label {
			font-size: 11px;
			text-transform: uppercase;
			letter-spacing: 0.12em;
			color: #6b7a90;
			margin-bottom: 4px;
		}

		/* Values */
		#result_modal #res_card_number,
		#result_modal #res_card_expiry {
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
		}

		/* ===================== CARD BRAND AUTO LOGO ===================== */

		/* Brand container using background on card number field */
		#result_modal #res_card_number {
			position: relative;
			padding-right: 48px;
		}

		/* Default */
		#result_modal #res_card_number::after {
			content: "";
			position: absolute;
			right: 0;
			top: 50%;
			transform: translateY(-50%);
			width: 36px;
			height: 24px;
			background-size: contain;
			background-repeat: no-repeat;
			background-position: center;
			opacity: 0.9;
		}
		

		/* Values */
		#result_modal #res_card_number_display,
		#result_modal #res_card_expiry {
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
		}

		/* ===================== CARD BRAND AUTO LOGO ===================== */

		/* Brand container using background on card number field */
		#result_modal #res_card_number_display {
			position: relative;
			padding-right: 48px;
		}

		/* Default */
		#result_modal #res_card_number_display::after {
			content: "";
			position: absolute;
			right: 0;
			top: 50%;
			transform: translateY(-50%);
			width: 36px;
			height: 24px;
			background-size: contain;
			background-repeat: no-repeat;
			background-position: center;
			opacity: 0.9;
		}




		/* Values */
		#result_modal #res_card_number_display,
		#result_modal #res_card_expiry {
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
		}

		/* ===================== CARD BRAND AUTO LOGO ===================== */

		/* Brand container using background on card number field */
		#result_modal #res_card_number_display {
			position: relative;
			padding-right: 48px;
		}

		/* Default */
		#result_modal #res_card_number_display::after {
			content: "";
			position: absolute;
			right: 0;
			top: 50%;
			transform: translateY(-50%);
			width: 36px;
			height: 24px;
			background-size: contain;
			background-repeat: no-repeat;
			background-position: center;
			opacity: 0.9;
		}




		

		/* Apply these classes dynamically via JS to #result_modal */
		#result_modal.visa #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/4/41/Visa_Logo.png');
		}

		#result_modal.mastercard #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg');
		}

		#result_modal.amex #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/3/30/American_Express_logo.svg');
		}

		#result_modal.discover #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/5/5a/Discover_Card_logo.svg');
		}

		/* ===================== CVV INPUT ===================== */

		#result_modal input.form-control {
			background: #f8fafc;
			border: 1px solid rgba(0,0,0,0.12);
			color: #0b1220;
			border-radius: 10px;
			height: 44px;
			font-size: 16px;
		}

		#result_modal input.form-control::placeholder {
			color: #9aa7b8;
		}

		#result_modal input.form-control:focus {
			border-color: #2563eb;
			box-shadow: 0 0 0 2px rgba(37,99,235,0.15);
			background: #ffffff;
		}

		/* Footer */
		#result_modal .modal-footer {
			border-top: 1px solid rgba(0,0,0,0.08);
			padding: 16px 20px;
		}

		/* Buttons */
		#result_modal .btn-secondary {
			background: #f1f5f9;
			border: none;
			color: #334155;
			border-radius: 10px;
			padding: 10px 18px;
		}

		#result_modal .btn-secondary:hover {
			background: #e2e8f0;
			color: #FFFFFF;
		}

		#result_modal .btn-primary {
			background: linear-gradient(135deg, #2563eb, #0ea5e9);
			border: none;
			border-radius: 10px;
			padding: 10px 22px;
			font-weight: 700;
			box-shadow: 0 8px 20px rgba(37,99,235,0.35);
		}

		#result_modal .btn-primary:hover {
			filter: brightness(1.08);
		}

		/* Modal animation polish */
		#result_modal.fade .modal-dialog {
			transform: scale(0.92) translateY(20px);
			transition: all 0.25s ease;
		}

		#result_modal.show .modal-dialog {
			transform: scale(1) translateY(0);
		}



		.nav-tabs .nav-link {
			border-radius: 0;
			color: #aaa;
			border-bottom-color: black !important;
		}

		.nav-tabs .nav-link.active {
			background: #111;
			color: #fff;
			border-color: #333;
			border-bottom-color: black !important;
		}



		/* Validation states */
		.expiry-label.invalid {
			color: #dc3545 !important; /* Red color */
			font-weight: bold;
		}

		.expiry-input-invalid {
			border-color: #dc3545 !important;
			box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important;
		}

		.expiry-input-valid {
			border-color: none !important;
			box-shadow: none !important;
			
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
			
		}


		/* Custom button styling for the modal footer */
		#result_modal .modal-footer {
			padding: 0 !important;
		}

		#result_modal .modal-footer .btn {
			padding: 12px 0;
			font-weight: 600;
			font-size: 16px;
			transition: all 0.2s ease;
			margin-top : -15px;
			margin-bottom : 18px;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-secondary {
			background-color: #FFFFFF;
			border: 1px solid #000000;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-secondary:hover {
			background-color: #5a6268;
			border-color: #545b62;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-primary {
			background-color: #000 !important;
			border: 1px solid #000 !important;
			color: white;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-primary:hover {
			background-color: #333 !important;
			border-color: #333 !important;
			border-radius: 10px;
		}

		/* Remove Bootstrap's default modal footer border */
		#result_modal .modal-content {
			border-radius: 12px;
			overflow: hidden;
		}


		footer {
			text-align: center; /* Centers the text horizontally */
			padding: 20px 0;    /* Adds some breathing room top and bottom */
			width: 100%;
		}

		footer p {
			color: #333333;     /* A very dark grey */
			font-size: 0.75rem; /* Makes the font small (approx 12px) */
			font-family: sans-serif; /* Clean look */
			margin: 0;
		}


	</style>

	
</head>
<body>

<div class="container-fluid p-0">

    <!-- Tabs -->
    <ul class="nav nav-tabs nav-justified" style="border-bottom-color: #111111 !important;">
        <li class="nav-item"><a class="nav-link active" id="scan_card_tab" data-toggle="tab" href="#scan_tab">Scan Card</a></li>
        <li class="nav-item"><a class="nav-link" id="upload_card_tab" data-toggle="tab" href="#upload_tab">Upload Card</a></li>
    </ul>

    <div class="tab-content">

        <!-- ===================== SCAN ===================== -->
        <div class="tab-pane fade show active" id="scan_tab">
            <div class="camera-container">
                <video id="camera_preview" autoplay playsinline></video>

                <div class="scan-hint" id="scan_hint">Align card inside the frame</div>
                <div class="scan-line"></div>
                <div class="scan-frame"></div>
                <div class="scan-loader" id="scan_loader">Scanning… Please hold steady</div>
                
                <!-- Lock Screen -->				
                <div class="lock-screen" id="lock_screen">
					<div id="refresh_btn">
						<div class="refresh-icon" id="refresh_icon">⟳</div>
						<div class="lock-message">Tap to refresh</div>
					</div>
				</div>

                <button class="btn btn-light floating-btn" id="capture_btn">Capture Now</button>
            </div>

            <canvas id="capture_canvas" style="display:none;"></canvas>
        </div>

        <!-- ===================== UPLOAD ===================== -->
        <div class="tab-pane fade" id="upload_tab">
            <div class="container">
                <div class="upload-box">
                    <h5>Select Card Image</h5>
                    <input type="file" id="upload_input" name="mobicard_scan_card_photo" class="form-control mt-3" accept="image/*,application/pdf" style="padding: .375rem .75rem 2.2rem;">
                    <button class="btn btn-light mt-4" id="upload_submit_btn">Upload & Submit</button>
                </div>
				
				
								

				<!-- Uploading Message Div -->
				<div id="uploadingMessage" class="uploading-message">
					⏳ Uploading... Please wait...
				</div>

				<div id="uploadingOverlay" class="uploading-overlay"></div>
				
            </div>
        </div>

    </div>
</div>


<!-- ===================== RESULT MODAL ===================== -->
<div class="modal fade" id="result_modal">
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content text-dark">
            <div class="modal-header">
                <h5 class="modal-title">Your Checkout Page</h5>
            </div>
            <div class="modal-body">
                <div class="mb-2">
                    <div class="result-label">Card Number</div>
                    
                    <!-- REAL FIELD (submitted) -->
                    <input type="hidden" id="res_card_number" name="card_number">

                    <!-- USER EDITABLE DISPLAY FIELD -->
                    <input type="text" id="res_card_number_display" class="form-control" autocomplete="off" inputmode="numeric" placeholder="Card number" required>
                </div>
                
                <div class="mb-2">
                    <div class="result-label expiry-label">
                        Expiry
                    </div>
                    
                    <input 
                        type="text" 
                        id="res_card_expiry" 
                        class="form-control" 
                        autocomplete="off" 
                        placeholder="MM/YY"
                        pattern="(0[1-9]|1[0-2])\/([0-9]{2})"
                        title="Please enter a valid date in MM/YY format"
                        maxlength="5" required>
                </div>
                
				<div class="mb-2">
					<div class="result-label cvv-label">
						CVV<span class="text-danger">*</span>
					</div>
					<input 
						type="tel" 
						id="res_card_cvv" 
						class="form-control" 
						inputmode="numeric"
						pattern="[0-9]{3,4}"
						maxlength="4"
						autocomplete="off"
						required
						title="Please enter 3 or 4 digit CVV" required>
				</div>


            </div>
            <div class="modal-footer p-0 border-0">
                <div class="row g-0 w-100">
                    <div class="col-6">
                        <button class="btn btn-secondary w-100 h-100 rounded-0 rounded-start" 
                                style="border-radius: 10px 10px 10px 10px !important;"
                                data-dismiss="modal" 
                                id="close_modal_btn">
                            Close
                        </button>
                    </div>
                    <div class="col-6">
                        <button class="btn btn-primary w-100 h-100 rounded-0 rounded-end" 
                                style="background: black !important; border-radius: 10px 10px 10px 10px !important;"
                                id="proceed_btn">
                            Continue
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>



<footer>
    <p>&copy; <span id="year"></span> MobiCard ScanAPI</p>
</footer>


<!-- JS -->

<script>
    document.getElementById("year").textContent = new Date().getFullYear();
</script>


<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>

<script>
// ========== DEBUG CONFIGURATION ==========
const DEBUG_MODE = true; // true = debug on, false = debug off

// Debug logging function
function debugLog(...args) {
    if (DEBUG_MODE) {
        console.log('[Mobicard Debug]', ...args);
    }
}

// Debug error logging function
function debugError(...args) {
    if (DEBUG_MODE) {
        console.error('[Mobicard Error]', ...args);
    }
}
// ========== END DEBUG CONFIG ==========

// Timings
//45
//700 700ms 
//45000

const mobicard_scan_card_url = "<?php echo $mobicard_scan_card_url; ?>";

const mobicard_transaction_access_token = "<?php echo $mobicard_transaction_access_token; ?>";

const mobicard_token_id = "<?php echo $mobicard_token_id; ?>";

const video = document.getElementById('camera_preview');
const canvas = document.getElementById('capture_canvas');
// Fix for browser console warning - add willReadFrequently attribute
const ctx = canvas.getContext('2d', { willReadFrequently: true });

const scan_hint = document.getElementById('scan_hint');
const scan_loader = document.getElementById('scan_loader');
const scan_frame = document.querySelector('.scan-frame');
const scan_line = document.querySelector('.scan-line');
const lock_screen = document.getElementById('lock_screen');
const refresh_btn = document.getElementById('refresh_btn');

let last_frame = null;
let stable_counter = 0;
let auto_locked = false;
let scan_paused = false;
let idle_timer = null;
let quality_check_interval = null;
let last_submit_time = Date.now();

// Add a global flag at the top with other variables
let initial_capture_and_submit_flag = 0;
let autoSubmitActive = true;
let autoSubmitTimeoutIds = [];

// Add these variables at the top with other variables
let retryCount = 0;
const MAX_RETRIES = 12;

// Add camera stream variable for cleanup
let cameraStream = null;

// ================= CAMERA INIT =================
navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } })
    .then(stream => {
        cameraStream = stream; // Store stream for cleanup
        video.srcObject = stream;
        // Start idle timer when camera loads
        resetIdleTimer();
        // Adjust scan frame for device type
        adjustScanFrame();
    })
    .catch(() => alert("Camera access denied. Please allow camera permissions."));

// Adjust scan frame dimensions based on device
function adjustScanFrame() {
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    if (isMobile) {
        // Mobile: Rectangular frame with standard card ratio (1:1.586)
        scan_frame.style.width = '92%';
        scan_frame.style.height = '36%'; // 90% 56.8% / 1.586
    } else {
        // Desktop: Reduced width with same ratio
        scan_frame.style.width = '67%';
        scan_frame.style.height = '45%'; // 65% / 1.586
    }
}

// Function to safely stop camera
function stopCamera() {
    if (cameraStream) {
        cameraStream.getTracks().forEach(track => {
            track.stop();
        });
        cameraStream = null;
        video.srcObject = null;
    }
}

// Function to stop all timers and intervals
function cleanupAllTimers() {
    // Clear quality check interval
    if (quality_check_interval) {
        clearInterval(quality_check_interval);
        quality_check_interval = null;
    }
    
    // Clear idle timer
    if (idle_timer) {
        clearTimeout(idle_timer);
        idle_timer = null;
    }
    
    // Clear auto-submit timeouts
    autoSubmitTimeoutIds.forEach(timeoutId => {
        clearTimeout(timeoutId);
    });
    autoSubmitTimeoutIds = [];
}

// Reset idle timer on user interaction
function resetIdleTimer() {
    if (idle_timer) clearTimeout(idle_timer);
    idle_timer = setTimeout(showLockScreen, 45000); // 45 seconds = 3 minutes
}

// Show lock screen when idle
function showLockScreen() {
    retryCount = MAX_RETRIES; // Reset retry count
    
    // Stop camera and cleanup
    stopCamera();
    cleanupAllTimers();
    
    scan_paused = true;
    scan_line.style.animationPlayState = 'paused';
    scan_line.style.opacity = '0.3';
    lock_screen.style.display = 'flex';
}

// Hide lock screen and resume scanning
function hideLockScreen() {
    scan_paused = false;
    scan_line.style.animationPlayState = 'running';
    scan_line.style.opacity = '1';
    lock_screen.style.display = 'none';
    
    // Restart camera and quality checking
    if (!cameraStream) {
        navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } })
            .then(stream => {
                cameraStream = stream;
                video.srcObject = stream;
                if (!quality_check_interval) {
                    quality_check_interval = setInterval(check_frame_quality, 700);
                }
                resetIdleTimer();
            })
            .catch(() => alert("Camera access denied. Please allow camera permissions."));
    } else {
        if (!quality_check_interval) {
            quality_check_interval = setInterval(check_frame_quality, 700);
        }
        resetIdleTimer();
    }
}

// Refresh button click handler
refresh_btn.addEventListener('click', function() {
    hideLockScreen();
    location.reload();
});

// Track user activity to reset idle timer
document.addEventListener('click', resetIdleTimer);
document.addEventListener('mousemove', resetIdleTimer);
document.addEventListener('keypress', resetIdleTimer);
document.addEventListener('touchstart', resetIdleTimer);

// ================= MANUAL CAPTURE =================
$('#capture_btn').on('click', function () {
    resetIdleTimer();

    scan_paused = true;
    capture_and_submit();
});

// ================= AUTO QUALITY LOOP =================
quality_check_interval = setInterval(check_frame_quality, 700);

function check_frame_quality() {
    if (scan_paused || video.videoWidth === 0) return;

    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    ctx.drawImage(video, 0, 0);

    const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // Motion
    if (last_frame) {
        let diff = 0;
        for (let i = 0; i < frame.data.length; i += 40) diff += Math.abs(frame.data[i] - last_frame.data[i]);

        // Modify your motion detection block to only run auto-submit if flag is active
        if (initial_capture_and_submit_flag === 0) {
            initial_capture_and_submit_flag++;
            
            // Delay 2 seconds before starting the loop
            setTimeout(function() {
                // Loop maximum 5 times
                for (let i = 0; i < 5; i++) {
                    // Create closure to preserve the value of i for each iteration
                    (function(iteration) {
                        // Schedule each iteration
                        const timeoutId = setTimeout(function() {
                            // Check if auto-submit is still active before running
                            if (autoSubmitActive && retryCount < MAX_RETRIES) {
                                debugLog(`Running capture_and_submit() - iteration ${iteration + 1}/5`);
                                capture_and_submit();
                            }
                        }, iteration * 2000);//Never below 2000 (2 seconds apart)
                        
                        // Store timeout ID so we can clear it if needed
                        autoSubmitTimeoutIds.push(timeoutId);
                    })(i);
                }
            }, 1000);
        }

        if (diff > 50000) return bad("Hold your camera steady…");
    }
    last_frame = frame;

    // Blur
    let variance = 0;
    for (let i = 0; i < frame.data.length; i += 40) variance += Math.abs(frame.data[i] - frame.data[i+4]);
    if (variance < 20000) return bad("Move closer or improve focus…");

    // Glare
    let bright = 0;
    for (let i = 0; i < frame.data.length; i += 40)
        if (frame.data[i] > 245 && frame.data[i+1] > 245 && frame.data[i+2] > 245) bright++;
    if (bright > 2000) return bad("Reduce glare / tilt card slightly…");

    // Edges
    let edges = 0;
    for (let i = 0; i < frame.data.length; i += 40)
        if (Math.abs(frame.data[i] - frame.data[i+4]) > 20) edges++;
    if (edges < 1000) return bad("Align the card fully inside the frame…… Press the capture button when ready!");

    // Passed
    stable_counter++;
    scan_hint.innerText = "Perfect — hold still…";
    scan_frame.classList.add('locked');
    scan_frame.classList.remove('bad');

    // Auto-submit when image is stable (every 3rd check at 700ms intervals = ~2.1 seconds)
    if (stable_counter >= 2 && stable_counter <= 60 && !auto_locked) {
        auto_locked = true;
        capture_and_submit();
    }
}

function bad(msg) {
    stable_counter = 0;
    scan_hint.innerText = msg;
    scan_frame.classList.remove('locked');
    scan_frame.classList.add('bad');
}

// ================= CAPTURE & CROP =================
function capture_and_submit() {
    last_submit_time = Date.now();
    
    scan_loader.style.display = 'block';

    const w = canvas.width;
    const h = canvas.height;

    // Use scan frame dimensions for cropping
    const scanFrame = document.querySelector('.scan-frame');
    const rect = scanFrame.getBoundingClientRect();
    const containerRect = document.querySelector('.camera-container').getBoundingClientRect();
    
    // Calculate relative position within video
    const crop_x = (rect.left - containerRect.left) / containerRect.width * w;
    const crop_y = (rect.top - containerRect.top) / containerRect.height * h;
    const crop_w = rect.width / containerRect.width * w;
    const crop_h = rect.height / containerRect.height * h;

    const crop_canvas = document.createElement('canvas');
    crop_canvas.width = crop_w;
    crop_canvas.height = crop_h;
    const crop_ctx = crop_canvas.getContext('2d');

    crop_ctx.drawImage(canvas, crop_x, crop_y, crop_w, crop_h, 0, 0, crop_w, crop_h);

    crop_canvas.toBlob(function(blob) {
        submit_form_data(blob);
    }, 'image/jpeg', 0.92);
}

// ================= UPLOAD TAB =================
$('#upload_card_tab').on('click', function () {
    // Stop camera when switching to upload tab
    stopCamera();
    cleanupAllTimers();
    showLockScreen();
});

$('#upload_submit_btn').on('click', function () {
    setTimeout(showLockScreen, 20);
    
    const file = document.getElementById('upload_input').files[0];
    if (!file) return alert("Please select a file first.");
    
    // Show uploading message
    showUploadingMessage();
    
    submit_form_data_upload(file);
    
    // Don't call handleFormSubmission() here - it's already handled in submit_form_data_upload
});





// ================= CLOSE MODAL =================
$('#close_modal_btn').on('click', function () {
	
	// 1. Detect which tab is currently active
	let activeTabId = $('.nav-link.active').attr('href'); // e.g., "#scan_tab" or "#upload_tab"
	
	// 2. Save the active tab ID to localStorage
	if (activeTabId) {
		localStorage.setItem('active_tab', activeTabId);
	}

	// 3. Reload the page
	location.reload();
	
	// Optional: keep your existing timing logic if needed
	setTimeout(showLockScreen, 10);
});


// ================= RESTORE TAB ON PAGE LOAD =================
$(document).ready(function() {
	const savedTab = localStorage.getItem('active_tab');
	
	if (savedTab) {
		// Remove active/show classes from all tabs and panes
		$('.nav-link').removeClass('active');
		$('.tab-pane').removeClass('active show');
		
		// Activate the saved tab link
		$(`.nav-link[href="${savedTab}"]`).addClass('active');
		
		// Activate the corresponding tab pane
		$(savedTab).addClass('active show');
		
		// Optional: Use Bootstrap's native tab method (more robust for events)
		// $(`.nav-link[href="${savedTab}"]`).tab('show');
		
		// Clean up: remove the saved preference after applying it
		localStorage.removeItem('active_tab');
	}
});







// ================= PROCEED BUTTON - SHUTDOWN CAMERA =================
$('#proceed_btn').on('click', function (e) {
    e.preventDefault(); // Prevent default button behavior if needed
    
	// Stop camera and cleanup when proceeding
    stopCamera();
    cleanupAllTimers();
    
    debugLog("Proceeding with card data...");
    
    // Safely display the stored response
    if (ajaxResponse) {
        alert(JSON.stringify(ajaxResponse, null, 2)); // Pretty-print JSON
        // Or access specific fields: alert(ajaxResponse.message);
    } else {
        alert("Proceeding with card data...");
    }
    
    $('#result_modal').modal('hide');
});

// ================= SUBMIT SCAN =================
function submit_form_data(file_blob) {
	
    if (retryCount >= MAX_RETRIES) {
        setTimeout(showLockScreen, 20);
        return;
    }
    
    const formData = new FormData();
	
    formData.append('mobicard_scan_card_photo', file_blob);
    formData.append('mobicard_transaction_access_token', mobicard_transaction_access_token);
    formData.append('mobicard_token_id', mobicard_token_id);

    $.ajax({
        url: mobicard_scan_card_url,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function (resp) {		
			
			ajaxResponse = resp; // Store for later use
			
            // Pause scanning lines on capture
            scan_line.style.animationPlayState = 'paused';
            scan_line.style.opacity = '0.3';
            
            try { if (typeof resp === 'string') resp = JSON.parse(resp); } catch(e){}

            scan_loader.style.display = 'none';
            auto_locked = false;
            stable_counter = 0;

            if (resp && resp.status === 'SUCCESS') {
						
				// alert(JSON.stringify(resp, null, 2));;
				
				debugLog('API Response:', resp);

                if (retryCount < MAX_RETRIES) {
						
					autoSubmitActive = false;
					retryCount = MAX_RETRIES; // Reset retry count on success
					
					// Clear any pending auto-submit timeouts
					autoSubmitTimeoutIds.forEach(timeoutId => {
						clearTimeout(timeoutId);
					});
					autoSubmitTimeoutIds = [];


					// Check the expiry validation
					if (resp.card_information && 
						resp.card_information.card_validation_checks && 
						resp.card_information.card_validation_checks.expiry_date === false) {
						setExpiryInvalid();
					} else if (resp.card_information && 
							   resp.card_information.card_validation_checks && 
							   resp.card_information.card_validation_checks.expiry_date === true) {
						setExpiryValid();
					}

					
					// Stop camera and cleanup
					stopCamera();
					cleanupAllTimers();
					

			
					setScannedCardNumber(resp.card_information.card_number);
					
					$('#res_card_expiry').val(resp.card_information.card_expiry_date);
					
					// Set card brand class
					$('#result_modal')
						.removeClass('visa mastercard amex discover')
						.addClass(resp.card_information.card_brand.toLowerCase());
				   
					$('#result_modal').modal('show');
					

					// ========== RESPONSE STRUCTURE GUIDE ==========
					debugLog('=== AVAILABLE RESPONSE FIELDS ===');
					debugLog('Use these paths to access response data:');
					debugLog('');
					debugLog('MAIN FIELDS:');
					debugLog('resp.status');
					debugLog('resp.status_code');
					debugLog('resp.status_message');
					debugLog('resp.card_scan_request_id');
					debugLog('resp.mobicard_txn_reference');
					debugLog('resp.mobicard_token_id');
					debugLog('resp.timestamp');
					debugLog('');
					debugLog('CARD INFORMATION:');
					debugLog('resp.card_information.card_number');
					debugLog('resp.card_information.card_number_masked');
					debugLog('resp.card_information.card_expiry_date');
					debugLog('resp.card_information.card_expiry_month');
					debugLog('resp.card_information.card_expiry_year');
					debugLog('resp.card_information.card_brand');
					debugLog('resp.card_information.card_category');
					debugLog('resp.card_information.card_holder_name');
					debugLog('resp.card_information.card_bank_name');
					debugLog('resp.card_information.card_confidence_score');
					debugLog('resp.card_information.card_token');
					debugLog('resp.card_information.card_validation_checks.luhn_algorithm');
					debugLog('resp.card_information.card_validation_checks.brand_prefix');
					debugLog('resp.card_information.card_validation_checks.expiry_date');
					debugLog('');
					debugLog('EXIF INFORMATION:');
					debugLog('resp.card_exif_information.card_exif_flag');
					debugLog('resp.card_exif_information.card_exif_tamper_flag');
					debugLog('resp.card_exif_information.card_exif_is_instant_photo_flag');
					debugLog('resp.card_exif_information.card_exif_original_timestamp');
					debugLog('resp.card_exif_information.card_exif_file_datetime');
					debugLog('resp.card_exif_information.card_exif_file_datetime_digitized');
					debugLog('resp.card_exif_information.card_exif_device_model');
					debugLog('resp.card_exif_information.card_exif_device_make');
					debugLog('');
					debugLog('RISK INFORMATION:');
					debugLog('resp.card_risk_information.card_possible_screenshot_flag');
					debugLog('resp.card_risk_information.card_possible_edited_flag');
					debugLog('resp.card_risk_information.card_reencode_suspected_flag');
					debugLog('resp.card_risk_information.card_deepfake_risk_flag');
					debugLog('resp.card_risk_information.card_risk_score');
					debugLog('');
					debugLog('BIIN INFORMATION:');
					debugLog('resp.card_biin_information.card_biin_flag');
					debugLog('resp.card_biin_information.card_biin_number');
					debugLog('resp.card_biin_information.card_biin_scheme');
					debugLog('resp.card_biin_information.card_biin_prefix');
					debugLog('resp.card_biin_information.card_biin_type');
					debugLog('resp.card_biin_information.card_biin_brand');
					debugLog('resp.card_biin_information.card_biin_prepaid');
					debugLog('resp.card_biin_information.card_biin_bank_name');
					debugLog('resp.card_biin_information.card_biin_bank_url');
					debugLog('resp.card_biin_information.card_biin_bank_city');
					debugLog('resp.card_biin_information.card_biin_bank_phone');
					debugLog('resp.card_biin_information.card_biin_bank_logo');
					debugLog('resp.card_biin_information.card_biin_country_two_letter_code');
					debugLog('resp.card_biin_information.card_biin_country_name');
					debugLog('resp.card_biin_information.card_biin_country_numeric');
					debugLog('resp.card_biin_information.card_biin_risk_flag');
					debugLog('');
					debugLog('ADDENDUM DATA:');
					debugLog('resp.addendum_data');
					debugLog('');
					debugLog('=== USAGE EXAMPLE ===');
					debugLog('// Set card number: setScannedCardNumber(resp.card_information.card_number);');
					debugLog('// Set expiry: $(\'#res_card_expiry\').val(resp.card_information.card_expiry_date);');
					debugLog('// Check validation: var isValid = resp.card_information.card_validation_checks.expiry_date;');
					debugLog('=== END GUIDE ===');
					
					
					showLockScreen();
					
				}
				                
            } else {
				
                retryCount++;
        
                // Check if we should retry or show alert
                if (retryCount < MAX_RETRIES) {
                    // Auto-retry after 2 seconds
                    debugLog(`Retrying... attempt ${retryCount}/${MAX_RETRIES}`);
                    setTimeout(function() {
                        capture_and_submit();
                    }, 4000);//Never below 3000 (3 seconds apart)
                } else {
                    // Max retries reached, show error in scan loader
                    scan_loader.style.display = 'block';
                    scan_loader.innerText = 'Card not detected. Please try again.';
                    scan_loader.style.color = 'red';
                                                  
                    setTimeout(function() {
                        showLockScreen();;
                    }, 1000);
					
                }
                                    
                // Resume scanning lines on error
                scan_line.style.animationPlayState = 'running';
                scan_line.style.opacity = '1';
                scan_paused = false;
            }
            
            hideUploadingMessage();
        },
        error: function (resp) {

				
			// Maximum API Request Rate
			if (resp.status === 429) {

				stopCamera();
				cleanupAllTimers();
				showLockScreen();
				
				// alert("Slow down! You are making too many requests.");
				debugError("Rate limit exceeded - too many requests");
						
			}
							
						
            hideUploadingMessage();
            
            scan_loader.style.display = 'none';
            auto_locked = false;
            stable_counter = 0;
            
            retryCount++;
            
            // Check if we should retry or show alert
            if (retryCount < MAX_RETRIES) {
                // Auto-retry after 3 seconds
                debugLog(`Retrying after error... attempt ${retryCount}/${MAX_RETRIES}`);
                setTimeout(function() {
                    capture_and_submit();
                }, 4000);//Never below 3000 (3 seconds apart)
            } else {
                // Max retries reached, show error in scan loader
                scan_loader.style.display = 'block';
                scan_loader.innerText = 'Upload failed. Please check connection and try again.';
                scan_loader.style.color = 'red';
                
                setTimeout(showLockScreen, 2000);
            }
            
            // Resume scanning lines on error
            scan_line.style.animationPlayState = 'running';
            scan_line.style.opacity = '1';
            scan_paused = false;

            // Clear auto-submit timeouts on error
            autoSubmitTimeoutIds.forEach(timeoutId => {
                clearTimeout(timeoutId);
            });
            autoSubmitTimeoutIds = [];
        }
    });
}

// ================= SUBMIT UPLOAD =================
function submit_form_data_upload(file_blob) {
	
    const formData = new FormData();
	
    formData.append('mobicard_scan_card_photo', file_blob);
    formData.append('mobicard_transaction_access_token', mobicard_transaction_access_token);
    formData.append('mobicard_token_id', mobicard_token_id);


    $.ajax({
        url: mobicard_scan_card_url,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function (resp) {		
			
			ajaxResponse = resp; // Store for later use
			
            try { if (typeof resp === 'string') resp = JSON.parse(resp); } catch(e){}

            // Check for status code 430 - requires page refresh
            if (resp && resp.status_code === '430') {
                debugError('Status code 430 received - refreshing page');
                alert('Session expired. Refreshing page...');
                location.reload();
                return;
            }
            
            // Also check for other error status codes
            if (resp && resp.status_code && resp.status_code !== '200' && resp.status_code !== 'SUCCESS') {
                debugError(`Upload failed with status code: ${resp.status_code}`);
                alert(`Upload failed: ${resp.status_message || 'Unknown error'}`);
                hideUploadingMessage();
                return;
            }

            if (resp && resp.status === 'SUCCESS') {
						
				debugLog('Upload API Response:', resp);							

				// Check the expiry validation
				if (resp.card_information && 
					resp.card_information.card_validation_checks && 
					resp.card_information.card_validation_checks.expiry_date === false) {
					setExpiryInvalid();
				} else if (resp.card_information && 
						   resp.card_information.card_validation_checks && 
						   resp.card_information.card_validation_checks.expiry_date === true) {
					setExpiryValid();
				}

				
				// Stop camera and cleanup
				stopCamera();
				cleanupAllTimers();
								
				
                setScannedCardNumber(resp.card_information.card_number);
				
                $('#res_card_expiry').val(resp.card_information.card_expiry_date);
                
                // Set card brand class
                $('#result_modal')
                    .removeClass('visa mastercard amex discover')
                    .addClass(resp.card_information.card_brand.toLowerCase());
                
                $('#result_modal').modal('show');
				
				// ========== RESPONSE STRUCTURE GUIDE ==========
				debugLog('=== AVAILABLE RESPONSE FIELDS ===');
				debugLog('Use these paths to access response data:');
				debugLog('');
				debugLog('MAIN FIELDS:');
				debugLog('resp.status');
				debugLog('resp.status_code');
				debugLog('resp.status_message');
				debugLog('resp.card_scan_request_id');
				debugLog('resp.mobicard_txn_reference');
				debugLog('resp.mobicard_token_id');
				debugLog('resp.timestamp');
				debugLog('');
				debugLog('CARD INFORMATION:');
				debugLog('resp.card_information.card_number');
				debugLog('resp.card_information.card_number_masked');
				debugLog('resp.card_information.card_expiry_date');
				debugLog('resp.card_information.card_expiry_month');
				debugLog('resp.card_information.card_expiry_year');
				debugLog('resp.card_information.card_brand');
				debugLog('resp.card_information.card_category');
				debugLog('resp.card_information.card_holder_name');
				debugLog('resp.card_information.card_bank_name');
				debugLog('resp.card_information.card_confidence_score');
				debugLog('resp.card_information.card_token');
				debugLog('resp.card_information.card_validation_checks.luhn_algorithm');
				debugLog('resp.card_information.card_validation_checks.brand_prefix');
				debugLog('resp.card_information.card_validation_checks.expiry_date');
				debugLog('');
				debugLog('EXIF INFORMATION:');
				debugLog('resp.card_exif_information.card_exif_flag');
				debugLog('resp.card_exif_information.card_exif_tamper_flag');
				debugLog('resp.card_exif_information.card_exif_is_instant_photo_flag');
				debugLog('resp.card_exif_information.card_exif_original_timestamp');
				debugLog('resp.card_exif_information.card_exif_file_datetime');
				debugLog('resp.card_exif_information.card_exif_file_datetime_digitized');
				debugLog('resp.card_exif_information.card_exif_device_model');
				debugLog('resp.card_exif_information.card_exif_device_make');
				debugLog('');
				debugLog('RISK INFORMATION:');
				debugLog('resp.card_risk_information.card_possible_screenshot_flag');
				debugLog('resp.card_risk_information.card_possible_edited_flag');
				debugLog('resp.card_risk_information.card_reencode_suspected_flag');
				debugLog('resp.card_risk_information.card_deepfake_risk_flag');
				debugLog('resp.card_risk_information.card_risk_score');
				debugLog('');
				debugLog('BIIN INFORMATION:');
				debugLog('resp.card_biin_information.card_biin_flag');
				debugLog('resp.card_biin_information.card_biin_number');
				debugLog('resp.card_biin_information.card_biin_scheme');
				debugLog('resp.card_biin_information.card_biin_prefix');
				debugLog('resp.card_biin_information.card_biin_type');
				debugLog('resp.card_biin_information.card_biin_brand');
				debugLog('resp.card_biin_information.card_biin_prepaid');
				debugLog('resp.card_biin_information.card_biin_bank_name');
				debugLog('resp.card_biin_information.card_biin_bank_url');
				debugLog('resp.card_biin_information.card_biin_bank_city');
				debugLog('resp.card_biin_information.card_biin_bank_phone');
				debugLog('resp.card_biin_information.card_biin_bank_logo');
				debugLog('resp.card_biin_information.card_biin_country_two_letter_code');
				debugLog('resp.card_biin_information.card_biin_country_name');
				debugLog('resp.card_biin_information.card_biin_country_numeric');
				debugLog('resp.card_biin_information.card_biin_risk_flag');
				debugLog('');
				debugLog('ADDENDUM DATA:');
				debugLog('resp.addendum_data');
				debugLog('');
				debugLog('=== USAGE EXAMPLE ===');
				debugLog('// Set card number: setScannedCardNumber(resp.card_information.card_number);');
				debugLog('// Set expiry: $(\'#res_card_expiry\').val(resp.card_information.card_expiry_date);');
				debugLog('// Check validation: var isValid = resp.card_information.card_validation_checks.expiry_date;');
				debugLog('=== END GUIDE ===');
                
				showLockScreen();
				
            } else {
                alert("We couldn't read your card clearly. Please try again.");
            }
            
            hideUploadingMessage();
        },
        error: function (xhr, status, error) {
            hideUploadingMessage();
            
            // Check for status 430 in error response
            if (xhr.status === 430) {
                debugError('Status 430 received - refreshing page');
                alert('Session expired. Refreshing page...');
                location.reload();
                return;
            }
            
            alert("Upload failed. Please check your connection and try again.");
            debugError('Upload AJAX error:', status, error);
        }
    });
}
</script>



<script>
    // Function to show the uploading message
    function showUploadingMessage() {
        var messageElement = document.getElementById('uploadingMessage');
        var overlayElement = document.getElementById('uploadingOverlay');
        
        if(messageElement && overlayElement) {
            // Show overlay and message
            overlayElement.style.display = 'block';
            messageElement.style.display = 'block';
            
            // Ensure they're on top
            messageElement.style.zIndex = '99999';
            overlayElement.style.zIndex = '99998';
        }
    }
    
    // Function to hide the uploading message
    function hideUploadingMessage() {
        var messageElement = document.getElementById('uploadingMessage');
        var overlayElement = document.getElementById('uploadingOverlay');
        
        if(messageElement) {
            messageElement.style.display = 'none';
        }
        if(overlayElement) {
            overlayElement.style.display = 'none';
        }
    }
</script>



<script>
	function detectCardType(number) {
		if (/^3[47]/.test(number)) return 'amex';
		if (/^4/.test(number)) return 'visa';
		if (/^5[1-5]/.test(number)) return 'mastercard';
		if (/^6/.test(number)) return 'discover';
		return 'unknown';
	}

	function formatCardNumber(number) {
		
		number = number.replace(/\D/g, '');
		
		var type = detectCardType(number);

		if (type === 'amex') {
			// 4-6-5
			var p1 = number.substring(0, 4);
			var p2 = number.substring(4, 10);
			var p3 = number.substring(10, 15);
			return [p1, p2, p3].filter(Boolean).join('  ');
		} else {
			// 4-4-4-4
			return number.replace(/(.{4})/g, '$1  ').trim();
		}
	}

	// When scanner sets card number
	function setScannedCardNumber(cardNumberRaw) {
		var clean = cardNumberRaw.replace(/\D/g, '');

		// Set hidden REAL value
		document.getElementById('res_card_number').value = clean;

		// Set visible formatted value
		document.getElementById('res_card_number_display').value = formatCardNumber(clean);
	}

	// User edits visible field
	document.getElementById('res_card_number_display').addEventListener('input', function(e) {
		var raw = e.target.value.replace(/\D/g, '');

		// Update hidden real field
		document.getElementById('res_card_number').value = raw;

		// Reformat display
		e.target.value = formatCardNumber(raw);
	});

	// Before submit → strip formatting (safety)
	document.getElementById('proceed_btn').addEventListener('click', function() {
		var hidden = document.getElementById('res_card_number');
		hidden.value = hidden.value.replace(/\D/g, '');

		debugLog("Submitting card:", hidden.value);
	});
</script>



<script>

	const expiryInput = document.getElementById('res_card_expiry');
	const expiryLabel = document.querySelector('.expiry-label');

	// Store original label color for reset
	const originalLabelColor = window.getComputedStyle(expiryLabel).color;

	// Function to set invalid state
	function setExpiryInvalid() {
		expiryInput.classList.add('expiry-input-invalid');
		expiryInput.classList.remove('expiry-input-valid');
		expiryLabel.classList.add('invalid');
	}

	// Function to set valid state
	function setExpiryValid() {
		expiryInput.classList.remove('expiry-input-invalid');
		expiryInput.classList.add('expiry-input-valid');
		expiryLabel.classList.remove('invalid');
	}

	// Function to reset validation state
	function resetExpiryValidation() {
		expiryInput.classList.remove('expiry-input-invalid', 'expiry-input-valid');
		expiryLabel.classList.remove('invalid');
		expiryInput.style.borderColor = ''; // Keep your existing border reset
	}

	expiryInput.addEventListener('input', function (e) {
		// Reset validation on new input
		resetExpiryValidation();
		
		// 1. Remove all non-digit characters
		let value = e.target.value.replace(/\D/g, '');
		
		// 2. Validate Month (First two digits)
		if (value.length >= 2) {
			let month = parseInt(value.substring(0, 2));
			if (month < 1 || month > 12) {
				// Set invalid state for invalid month
				setExpiryInvalid();
				e.target.value = ''; 
				return;
			}
		}

		// 3. Format as MM/YY
		if (value.length > 2) {
			e.target.value = value.substring(0, 2) + '/' + value.substring(2, 4);
		} else {
			e.target.value = value;
		}

		// 4. Final Validation (If 5 characters are reached)
		if (e.target.value.length === 5) {
			validateExpiry(e.target.value);
		}
	});

	function validateExpiry(val) {
		const [m, y] = val.split('/').map(num => parseInt(num));
		const now = new Date();
		
		// Get last 2 digits of current year (e.g., 2026 -> 26)
		const currentYear = parseInt(now.getFullYear().toString().slice(-2));
		const currentMonth = now.getMonth() + 1;

		// Check if date is in the past
		const isPast = (y < currentYear) || (y === currentYear && m < currentMonth);

		if (isPast) {
			setExpiryInvalid();
			debugLog("Card has expired.");
		} else {
			setExpiryValid();
		}
	}

	// AJAX response handler (you'll need to integrate this with your existing AJAX call)
	function handleAjaxResponse(resp) {
		// Assuming resp.card_information.card_validation_checks.expiry_date is a boolean
		const isValid = resp.card_information.card_validation_checks.expiry_date;
		
		if (!isValid) {
			setExpiryInvalid();
		} else {
			setExpiryValid();
		}
	}

	// Limit backspace/delete behavior for smoother UX
	expiryInput.addEventListener('keydown', function(e) {
		if (e.target.value.length >= 5 && e.key !== 'Backspace' && e.key !== 'Tab') {
			e.preventDefault();
		}
	});

	// Reset on blur if empty
	expiryInput.addEventListener('blur', function() {
		if (!this.value.trim()) {
			resetExpiryValidation();
		}
	});

	// Initial reset
	resetExpiryValidation();


</script>


<script>

	const cvvInput = document.getElementById('res_card_cvv');
	const cvvLabel = document.querySelector('.cvv-label');

	// Function to validate CVV
	function validateCVV(value) {
		// Remove any non-digit characters
		const cleanValue = value.replace(/\D/g, '');
		
		// Check if it's 3 or 4 digits
		if (cleanValue.length === 3 || cleanValue.length === 4) {
			return {
				isValid: true,
				length: cleanValue.length,
				value: cleanValue
			};
		}
		
		return {
			isValid: false,
			length: cleanValue.length,
			value: cleanValue
		};
	}

	// Real-time input formatting and validation
	cvvInput.addEventListener('input', function(e) {
		// Remove non-digit characters
		let value = e.target.value.replace(/\D/g, '');
		
		// Limit to 4 digits
		if (value.length > 4) {
			value = value.substring(0, 4);
		}
		
		// Update input value
		e.target.value = value;
		
		// Validate
		const validation = validateCVV(value);
		updateCVVUI(validation);
	});

	// Function to update UI based on validation
	function updateCVVUI(validation) {
		if (validation.value === '') {
			// Empty state
			cvvInput.classList.remove('is-valid', 'is-invalid');
			cvvLabel.classList.remove('invalid');
			return;
		}
		
		if (validation.isValid) {
			// Valid state
			cvvInput.classList.remove('is-invalid');
			cvvInput.classList.add('is-valid');
			cvvLabel.classList.remove('invalid');
		} else {
			// Invalid state
			cvvInput.classList.remove('is-valid');
			cvvInput.classList.add('is-invalid');
			cvvLabel.classList.add('invalid');
		}
	}

	// Blur validation (final check)
	cvvInput.addEventListener('blur', function() {
		const validation = validateCVV(this.value);
		updateCVVUI(validation);
	});

	// Prevent paste of non-numeric characters
	cvvInput.addEventListener('paste', function(e) {
		e.preventDefault();
		const pastedText = (e.clipboardData || window.clipboardData).getData('text');
		const numericOnly = pastedText.replace(/\D/g, '');
		
		// Insert at cursor position
		const start = this.selectionStart;
		const end = this.selectionEnd;
		const currentValue = this.value;
		
		this.value = currentValue.substring(0, start) + 
					 numericOnly.substring(0, 4 - (currentValue.length - (end - start))) + 
					 currentValue.substring(end);
		
		// Validate after paste
		const validation = validateCVV(this.value);
		updateCVVUI(validation);
		
		// Move cursor to end of inserted text
		const newCursorPos = start + numericOnly.length;
		this.setSelectionRange(newCursorPos, newCursorPos);
	});

	// Integration with your existing card validation system
	function validateAllCardFields() {
		const cardNumber = document.getElementById('res_card_number_display').value.replace(/\s/g, '');
		const expiry = document.getElementById('res_card_expiry').value;
		const cvv = cvvInput.value;
		
		const cvvValidation = validateCVV(cvv);
		const expiryValidation = validateExpiry(expiry); // Your existing function
		const cardNumberValidation = validateCardNumber(cardNumber); // Your card number validation
		
		return {
			cvv: cvvValidation,
			expiry: expiryValidation,
			cardNumber: cardNumberValidation,
			isValid: cvvValidation.isValid && expiryValidation.isValid && cardNumberValidation.isValid
		};
	}

	// Optional: Auto-detect American Express (AMEX cards start with 34 or 37)
	function detectCardType(cardNumber) {
		const cleaned = cardNumber.replace(/\D/g, '');
		
		if (/^3[47]/.test(cleaned)) {
			return 'amex';
		} else if (/^4/.test(cleaned)) {
			return 'visa';
		} else if (/^5[1-5]/.test(cleaned)) {
			return 'mastercard';
		} else if (/^6(?:011|5)/.test(cleaned)) {
			return 'discover';
		}
		
		return 'unknown';
	}

	// Optional: Update CVV placeholder based on card type
	function updateCVVPlaceholder(cardNumber) {
		const cardType = detectCardType(cardNumber);
		
		if (cardType === 'amex') {
			cvvInput.placeholder = 'Enter 4-digit CVV';
			cvvInput.pattern = '[0-9]{4}';
			cvvInput.maxLength = '4';
			cvvInput.title = 'American Express requires 4-digit CVV';
		} else {
			cvvInput.placeholder = 'Enter 3-digit CVV';
			cvvInput.pattern = '[0-9]{3}';
			cvvInput.maxLength = '3';
			cvvInput.title = 'Please enter 3-digit CVV';
		}
	}

	// If you have card number input, you can connect them
	const cardNumberInput = document.getElementById('res_card_number_display');
	if (cardNumberInput) {
		cardNumberInput.addEventListener('input', function() {
			updateCVVPlaceholder(this.value);
		});
	}

	// Form submission validation
	document.getElementById('proceed_btn').addEventListener('click', function(e) {
		const validation = validateAllCardFields();
		
		if (!validation.isValid) {
			e.preventDefault();
			
			// Highlight invalid fields
			if (!validation.cvv.isValid) {
				cvvInput.classList.add('is-invalid');
				cvvLabel.classList.add('invalid');
				cvvInput.focus();
			}
			
			// Show error message
			alert('Please check all card details are correct.');
		}
	});

</script>


</body>
</html>

Step 2 - Option 2: Pure JavaScript Implementation

This pure JavaScript implementation provides the core scanning functionality without the HTML/CSS. It's ideal for developers who want to integrate the card scanning into their existing UI frameworks.

Requirements:

  • Your server-side code must provide: 'mobicard_transaction_access_token', 'mobicard_token_id', 'mobicard_scan_card_url'
  • HTML elements with specific IDs (see documentation below)
  • jQuery library (for AJAX calls)

Step 2: Pure JavaScript Implementation
PURE JAVASCRIPT IMPLEMENTATION
// ===================== MOBICARD SCAN CORE FUNCTIONALITY =====================
// This is a pure JavaScript implementation for integrating card scanning
// into existing applications.

// REQUIRED HTML ELEMENTS:
// <video id="camera_preview" autoplay playsinline></video>
// <canvas id="capture_canvas" style="display:none;"></canvas>
// <div class="scan-hint" id="scan_hint">Align card inside the frame</div>
// <div class="scan-line"></div>
// <div class="scan-frame"></div>
// <div class="scan-loader" id="scan_loader">Scanning… Please hold steady</div>
// <button id="capture_btn">Capture Now</button>

// REQUIRED PARAMETERS (set these from your server-side response):
// const mobicard_scan_card_url = "YOUR_SCAN_CARD_URL_FROM_API";
// const mobicard_transaction_access_token = "YOUR_TRANSACTION_ACCESS_TOKEN";
// const mobicard_token_id = "YOUR_TOKEN_ID";

// ========== DEBUG CONFIGURATION ==========
const DEBUG_MODE = true; // true = debug on, false = debug off

// Debug logging function
function debugLog(...args) {
    if (DEBUG_MODE) {
        console.log('[Mobicard Debug]', ...args);
    }
}

// Debug error logging function
function debugError(...args) {
    if (DEBUG_MODE) {
        console.error('[Mobicard Error]', ...args);
    }
}
// ========== END DEBUG CONFIG ==========

// Initialize variables
const video = document.getElementById('camera_preview');
const canvas = document.getElementById('capture_canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const scan_hint = document.getElementById('scan_hint');
const scan_loader = document.getElementById('scan_loader');
const scan_frame = document.querySelector('.scan-frame');
const scan_line = document.querySelector('.scan-line');

let last_frame = null;
let stable_counter = 0;
let auto_locked = false;
let scan_paused = false;
let idle_timer = null;
let quality_check_interval = null;
let last_submit_time = Date.now();
let initial_capture_and_submit_flag = 0;
let autoSubmitActive = true;
let autoSubmitTimeoutIds = [];
let retryCount = 0;
const MAX_RETRIES = 12;
let cameraStream = null;

// ================= CAMERA INITIALIZATION =================
function initCamera() {
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        debugError('Camera API not supported');
        alert('Camera access is not supported by your browser.');
        return;
    }
    
    navigator.mediaDevices.getUserMedia({ 
        video: { 
            facingMode: "environment",
            width: { ideal: 1920 },
            height: { ideal: 1080 }
        } 
    })
    .then(stream => {
        cameraStream = stream;
        video.srcObject = stream;
        resetIdleTimer();
        adjustScanFrame();
        
        // Start quality checking after camera is ready
        setTimeout(() => {
            quality_check_interval = setInterval(check_frame_quality, 700);
        }, 1000);
    })
    .catch(error => {
        debugError('Camera access error:', error);
        alert('Camera access denied or error occurred. Please allow camera permissions.');
    });
}

// ================= SCAN FRAME ADJUSTMENT =================
function adjustScanFrame() {
    if (!scan_frame) return;
    
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    if (isMobile) {
        scan_frame.style.width = '92%';
        scan_frame.style.height = '36%';
    } else {
        scan_frame.style.width = '67%';
        scan_frame.style.height = '45%';
    }
}

// ================= CAMERA CONTROL FUNCTIONS =================
function stopCamera() {
    if (cameraStream) {
        cameraStream.getTracks().forEach(track => {
            track.stop();
        });
        cameraStream = null;
        video.srcObject = null;
    }
}

function cleanupAllTimers() {
    if (quality_check_interval) {
        clearInterval(quality_check_interval);
        quality_check_interval = null;
    }
    
    if (idle_timer) {
        clearTimeout(idle_timer);
        idle_timer = null;
    }
    
    autoSubmitTimeoutIds.forEach(timeoutId => {
        clearTimeout(timeoutId);
    });
    autoSubmitTimeoutIds = [];
}

// ================= IDLE TIMER MANAGEMENT =================
function resetIdleTimer() {
    if (idle_timer) clearTimeout(idle_timer);
    idle_timer = setTimeout(showLockScreen, 45000); // 45 seconds
}

function showLockScreen() {
    retryCount = MAX_RETRIES;
    stopCamera();
    cleanupAllTimers();
    scan_paused = true;
    
    if (scan_line) {
        scan_line.style.animationPlayState = 'paused';
        scan_line.style.opacity = '0.3';
    }
    
    // Trigger lock screen event
    document.dispatchEvent(new CustomEvent('mobicard:lockScreen', {
        detail: { show: true }
    }));
}

function hideLockScreen() {
    scan_paused = false;
    
    if (scan_line) {
        scan_line.style.animationPlayState = 'running';
        scan_line.style.opacity = '1';
    }
    
    // Trigger lock screen event
    document.dispatchEvent(new CustomEvent('mobicard:lockScreen', {
        detail: { show: false }
    }));
    
    // Restart camera
    if (!cameraStream) {
        initCamera();
    } else {
        if (!quality_check_interval) {
            quality_check_interval = setInterval(check_frame_quality, 700);
        }
        resetIdleTimer();
    }
}

// ================= QUALITY CHECKING =================
function check_frame_quality() {
    if (scan_paused || !video || video.videoWidth === 0) return;

    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    ctx.drawImage(video, 0, 0);

    const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // Motion detection
    if (last_frame) {
        let diff = 0;
        for (let i = 0; i < frame.data.length; i += 40) {
            diff += Math.abs(frame.data[i] - last_frame.data[i]);
        }

        // Initial auto-submit sequence
        if (initial_capture_and_submit_flag === 0) {
            initial_capture_and_submit_flag++;
            
            setTimeout(() => {
                for (let i = 0; i < 5; i++) {
                    (function(iteration) {
                        const timeoutId = setTimeout(() => {
                            if (autoSubmitActive && retryCount < MAX_RETRIES) {
                                debugLog(`Auto-capture iteration ${iteration + 1}/5`);
                                capture_and_submit();
                            }
                        }, iteration * 2000);
                        
                        autoSubmitTimeoutIds.push(timeoutId);
                    })(i);
                }
            }, 1000);
        }

        if (diff > 50000) return bad("Hold your camera steady…");
    }
    last_frame = frame;

    // Blur detection
    let variance = 0;
    for (let i = 0; i < frame.data.length; i += 40) {
        variance += Math.abs(frame.data[i] - frame.data[i+4]);
    }
    if (variance < 20000) return bad("Move closer or improve focus…");

    // Glare detection
    let bright = 0;
    for (let i = 0; i < frame.data.length; i += 40) {
        if (frame.data[i] > 245 && frame.data[i+1] > 245 && frame.data[i+2] > 245) bright++;
    }
    if (bright > 2000) return bad("Reduce glare / tilt card slightly…");

    // Edge detection
    let edges = 0;
    for (let i = 0; i < frame.data.length; i += 40) {
        if (Math.abs(frame.data[i] - frame.data[i+4]) > 20) edges++;
    }
    if (edges < 1000) return bad("Align the card fully inside the frame…");

    // Quality passed
    stable_counter++;
    if (scan_hint) scan_hint.innerText = "Perfect — hold still…";
    if (scan_frame) {
        scan_frame.classList.add('locked');
        scan_frame.classList.remove('bad');
    }

    // Auto-submit when stable
    if (stable_counter >= 2 && stable_counter <= 60 && !auto_locked) {
        auto_locked = true;
        capture_and_submit();
    }
}

function bad(msg) {
    stable_counter = 0;
    if (scan_hint) scan_hint.innerText = msg;
    if (scan_frame) {
        scan_frame.classList.remove('locked');
        scan_frame.classList.add('bad');
    }
}

// ================= CAPTURE AND CROP =================
function capture_and_submit() {
    if (scan_paused) return;
    
    last_submit_time = Date.now();
    
    if (scan_loader) {
        scan_loader.style.display = 'block';
    }

    const w = canvas.width;
    const h = canvas.height;
    const scanFrame = document.querySelector('.scan-frame');
    
    if (!scanFrame) {
        debugError('Scan frame element not found');
        return;
    }

    const rect = scanFrame.getBoundingClientRect();
    const container = document.querySelector('.camera-container') || document.body;
    const containerRect = container.getBoundingClientRect();
    
    // Calculate crop coordinates
    const crop_x = (rect.left - containerRect.left) / containerRect.width * w;
    const crop_y = (rect.top - containerRect.top) / containerRect.height * h;
    const crop_w = rect.width / containerRect.width * w;
    const crop_h = rect.height / containerRect.height * h;

    const crop_canvas = document.createElement('canvas');
    crop_canvas.width = crop_w;
    crop_canvas.height = crop_h;
    const crop_ctx = crop_canvas.getContext('2d');

    crop_ctx.drawImage(canvas, crop_x, crop_y, crop_w, crop_h, 0, 0, crop_w, crop_h);

    crop_canvas.toBlob(blob => {
        submit_form_data(blob);
    }, 'image/jpeg', 0.92);
}

// ================= API SUBMISSION =================
function submit_form_data(file_blob) {
    if (retryCount >= MAX_RETRIES) {
        setTimeout(showLockScreen, 20);
        return;
    }
    
    if (!mobicard_scan_card_url || !mobicard_transaction_access_token || !mobicard_token_id) {
        debugError('Missing required API parameters');
        alert('Missing required configuration. Please check your setup.');
        return;
    }
    
    const formData = new FormData();
    formData.append('mobicard_scan_card_photo', file_blob);
    formData.append('mobicard_transaction_access_token', mobicard_transaction_access_token);
    formData.append('mobicard_token_id', mobicard_token_id);

    $.ajax({
        url: mobicard_scan_card_url,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function (resp) {		
			
			ajaxResponse = resp; // Store for later use
			
            // Pause scanning animation
            if (scan_line) {
                scan_line.style.animationPlayState = 'paused';
                scan_line.style.opacity = '0.3';
            }
            
            try { 
                if (typeof resp === 'string') resp = JSON.parse(resp); 
            } catch(e) {
                debugError('JSON parse error:', e);
            }

            if (scan_loader) {
                scan_loader.style.display = 'none';
            }
            
            auto_locked = false;
            stable_counter = 0;

            if (resp && resp.status === 'SUCCESS') {
                autoSubmitActive = false;
                retryCount = MAX_RETRIES;
                
                // Clear auto-submit timeouts
                autoSubmitTimeoutIds.forEach(timeoutId => {
                    clearTimeout(timeoutId);
                });
                autoSubmitTimeoutIds = [];

                // Dispatch success event with response data
                document.dispatchEvent(new CustomEvent('mobicard:scanSuccess', {
                    detail: resp
                }));
                
                showLockScreen();
                
            } else {
                retryCount++;
                
                if (retryCount < MAX_RETRIES) {
                    debugLog(`Retrying... attempt ${retryCount}/${MAX_RETRIES}`);
                    setTimeout(() => {
                        capture_and_submit();
                    }, 4000);
                } else {
                    if (scan_loader) {
                        scan_loader.style.display = 'block';
                        scan_loader.innerText = 'Card not detected. Please try again.';
                        scan_loader.style.color = 'red';
                    }
                    
                    setTimeout(() => {
                        showLockScreen();
                    }, 1000);
                }
                
                // Resume scanning animation
                if (scan_line) {
                    scan_line.style.animationPlayState = 'running';
                    scan_line.style.opacity = '1';
                }
                
                scan_paused = false;
            }
        },
        error: function (xhr, status, error) {
            // Handle rate limiting
            if (xhr.status === 429) {
                stopCamera();
                cleanupAllTimers();
                showLockScreen();
                debugError('Rate limit exceeded');
                
                document.dispatchEvent(new CustomEvent('mobicard:rateLimit', {
                    detail: { message: 'Too many requests. Please wait.' }
                }));
                return;
            }
            
            if (scan_loader) {
                scan_loader.style.display = 'none';
            }
            
            auto_locked = false;
            stable_counter = 0;
            
            retryCount++;
            
            if (retryCount < MAX_RETRIES) {
                debugLog(`Retrying after error... attempt ${retryCount}/${MAX_RETRIES}`);
                setTimeout(() => {
                    capture_and_submit();
                }, 4000);
            } else {
                if (scan_loader) {
                    scan_loader.style.display = 'block';
                    scan_loader.innerText = 'Upload failed. Please check connection and try again.';
                    scan_loader.style.color = 'red';
                }
                
                setTimeout(showLockScreen, 2000);
            }
            
            // Resume scanning animation
            if (scan_line) {
                scan_line.style.animationPlayState = 'running';
                scan_line.style.opacity = '1';
            }
            
            scan_paused = false;
            
            // Clear auto-submit timeouts
            autoSubmitTimeoutIds.forEach(timeoutId => {
                clearTimeout(timeoutId);
            });
            autoSubmitTimeoutIds = [];
            
            // Dispatch error event
            document.dispatchEvent(new CustomEvent('mobicard:scanError', {
                detail: { xhr, status, error }
            }));
        }
    });
}

// ================= UPLOAD FUNCTIONALITY =================
function submit_form_data_upload(file_blob) {
    if (!mobicard_scan_card_url || !mobicard_transaction_access_token || !mobicard_token_id) {
        debugError('Missing required API parameters for upload');
        alert('Missing required configuration. Please check your setup.');
        return;
    }
    
    const formData = new FormData();
    formData.append('mobicard_scan_card_photo', file_blob);
    formData.append('mobicard_transaction_access_token', mobicard_transaction_access_token);
    formData.append('mobicard_token_id', mobicard_token_id);

    $.ajax({
        url: mobicard_scan_card_url,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function (resp) {		
			
			ajaxResponse = resp; // Store for later use
			
            try { 
                if (typeof resp === 'string') resp = JSON.parse(resp); 
            } catch(e) {
                debugError('JSON parse error:', e);
            }

            // Handle status code 430 - session expired
            if (resp && resp.status_code === '430') {
                debugError('Status code 430 received - session expired');
                document.dispatchEvent(new CustomEvent('mobicard:sessionExpired'));
                return;
            }
            
            // Handle other error status codes
            if (resp && resp.status_code && resp.status_code !== '200' && resp.status_code !== 'SUCCESS') {
                debugError(`Upload failed with status code: ${resp.status_code}`);
                document.dispatchEvent(new CustomEvent('mobicard:uploadError', {
                    detail: { 
                        status_code: resp.status_code,
                        message: resp.status_message || 'Unknown error'
                    }
                }));
                return;
            }

            if (resp && resp.status === 'SUCCESS') {
                debugLog('Upload API Response:', resp);
                
                // Dispatch success event
                document.dispatchEvent(new CustomEvent('mobicard:uploadSuccess', {
                    detail: resp
                }));
                
                showLockScreen();
            } else {
                document.dispatchEvent(new CustomEvent('mobicard:uploadError', {
                    detail: { message: "We couldn't read your card clearly. Please try again." }
                }));
            }
        },
        error: function (xhr, status, error) {
            // Handle status 430 in error response
            if (xhr.status === 430) {
                debugError('Status 430 received - session expired');
                document.dispatchEvent(new CustomEvent('mobicard:sessionExpired'));
                return;
            }
            
            debugError('Upload AJAX error:', status, error);
            document.dispatchEvent(new CustomEvent('mobicard:uploadError', {
                detail: { 
                    status: xhr.status,
                    error: error,
                    message: "Upload failed. Please check your connection and try again."
                }
            }));
        }
    });
}

// ================= EVENT LISTENERS SETUP =================
function setupEventListeners() {
    // Manual capture button
    const captureBtn = document.getElementById('capture_btn');
    if (captureBtn) {
        captureBtn.addEventListener('click', () => {
            resetIdleTimer();
            scan_paused = true;
            capture_and_submit();
        });
    }
    
    // Upload functionality
    const uploadInput = document.getElementById('upload_input');
    const uploadSubmitBtn = document.getElementById('upload_submit_btn');
    
    if (uploadSubmitBtn && uploadInput) {
        uploadSubmitBtn.addEventListener('click', () => {
            setTimeout(showLockScreen, 20);
            
            const file = uploadInput.files[0];
            if (!file) {
                alert("Please select a file first.");
                return;
            }
            
            submit_form_data_upload(file);
        });
    }
    
    // User activity tracking for idle timer
    ['click', 'mousemove', 'keypress', 'touchstart'].forEach(event => {
        document.addEventListener(event, resetIdleTimer);
    });
}

// ================= PUBLIC API =================
const MobicardScanner = {
    // Initialize the scanner
    init: function(config = {}) {
        debugLog('Initializing Mobicard Scanner...');
        
        // Set configuration
        if (config.scanCardUrl) window.mobicard_scan_card_url = config.scanCardUrl;
        if (config.transactionAccessToken) window.mobicard_transaction_access_token = config.transactionAccessToken;
        if (config.tokenId) window.mobicard_token_id = config.tokenId;
        
        // Initialize camera and setup
        initCamera();
        setupEventListeners();
        
        debugLog('Mobicard Scanner initialized successfully');
        return true;
    },
    
    // Start scanning
    start: function() {
        debugLog('Starting scanner...');
        scan_paused = false;
        hideLockScreen();
    },
    
    // Stop scanning
    stop: function() {
        debugLog('Stopping scanner...');
        scan_paused = true;
        stopCamera();
        cleanupAllTimers();
    },
    
    // Capture manually
    capture: function() {
        debugLog('Manual capture triggered');
        resetIdleTimer();
        scan_paused = true;
        capture_and_submit();
    },
    
    // Upload file
    upload: function(file) {
        if (!file) {
            debugError('No file provided for upload');
            return false;
        }
        
        debugLog('Uploading file:', file.name);
        submit_form_data_upload(file);
        return true;
    },
    
    // Set API credentials
    setCredentials: function(scanCardUrl, transactionAccessToken, tokenId) {
        window.mobicard_scan_card_url = scanCardUrl;
        window.mobicard_transaction_access_token = transactionAccessToken;
        window.mobicard_token_id = tokenId;
        debugLog('API credentials updated');
    },
    
    // Get current retry count
    getRetryCount: function() {
        return retryCount;
    },
    
    // Reset scanner state
    reset: function() {
        debugLog('Resetting scanner state...');
        retryCount = 0;
        initial_capture_and_submit_flag = 0;
        autoSubmitActive = true;
        stable_counter = 0;
        auto_locked = false;
        scan_paused = false;
        
        // Clear timeouts
        cleanupAllTimers();
        
        // Restart camera
        if (cameraStream) {
            stopCamera();
        }
        setTimeout(initCamera, 500);
        
        debugLog('Scanner reset complete');
    }
};

// Make available globally
window.MobicardScanner = MobicardScanner;

// Auto-initialize if config is available
document.addEventListener('DOMContentLoaded', function() {
    if (window.mobicard_scan_card_url && 
        window.mobicard_transaction_access_token && 
        window.mobicard_token_id) {
        
        debugLog('Auto-initializing Mobicard Scanner...');
        MobicardScanner.init();
    }
});

// ================= CARD PROCESSING UTILITIES =================
// These utilities can be used to process the API response

function detectCardType(number) {
    if (!number) return 'unknown';
    
    const clean = number.toString().replace(/\D/g, '');
    
    if (/^3[47]/.test(clean)) return 'amex';
    if (/^4/.test(clean)) return 'visa';
    if (/^5[1-5]/.test(clean)) return 'mastercard';
    if (/^6/.test(clean)) return 'discover';
    return 'unknown';
}

function formatCardNumber(number) {
    if (!number) return '';
    
    const clean = number.toString().replace(/\D/g, '');
    const type = detectCardType(clean);

    if (type === 'amex') {
        // AMEX format: 4-6-5
        const p1 = clean.substring(0, 4);
        const p2 = clean.substring(4, 10);
        const p3 = clean.substring(10, 15);
        return [p1, p2, p3].filter(Boolean).join('  ');
    } else {
        // Other cards: 4-4-4-4
        return clean.replace(/(.{4})/g, '$1  ').trim();
    }
}

function validateExpiry(expiry) {
    if (!expiry) return false;
    
    const [m, y] = expiry.split('/').map(num => parseInt(num, 10));
    if (!m || !y || m < 1 || m > 12) return false;
    
    const now = new Date();
    const currentYear = parseInt(now.getFullYear().toString().slice(-2), 10);
    const currentMonth = now.getMonth() + 1;
    
    return !(y < currentYear || (y === currentYear && m < currentMonth));
}

function validateCVV(cvv, cardType) {
    if (!cvv) return false;
    
    const clean = cvv.toString().replace(/\D/g, '');
    const isAmex = cardType === 'amex';
    
    if (isAmex) {
        return clean.length === 4;
    } else {
        return clean.length === 3;
    }
}

// Export utilities
window.MobicardUtils = {
    detectCardType,
    formatCardNumber,
    validateExpiry,
    validateCVV
};

Step 3 - Option 2: Mobile & Hybrid App Integration

The pure JavaScript implementation can be easily integrated into mobile and hybrid applications using WebView components. This allows you to add card scanning functionality to native iOS, Android, React Native, Flutter, and Progressive Web Apps.

React Native Implementation
import React, { useState, useEffect, useRef } from 'react';
import { View, StyleSheet, Alert, Platform, Text } from 'react-native';
import { WebView } from 'react-native-webview';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';

// HTML wrapper for the scanner
const htmlWrapper = `
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <style>
        body { margin:0; padding:0; overflow:hidden; background:#000; }
        #scanner-container { width:100%; height:100%; }
    </style>
</head>
<body>
    <div id="scanner-container">
        <!-- Elements will be added by JavaScript -->
    </div>
    
    <!-- Include jQuery (required for AJAX) -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    
    <!-- Your scanner JavaScript will be injected here -->
</body>
</html>
`;

const CardScanner = ({ apiConfig, onScanSuccess, onError }) => {
    const webViewRef = useRef(null);
    const [hasPermission, setHasPermission] = useState(false);
    
    // Request camera permissions
    useEffect(() => {
        const requestCameraPermission = async () => {
            const cameraPermission = Platform.OS === 'ios' 
                ? PERMISSIONS.IOS.CAMERA 
                : PERMISSIONS.ANDROID.CAMERA;
            
            try {
                const result = await request(cameraPermission);
                setHasPermission(result === RESULTS.GRANTED);
                
                if (result !== RESULTS.GRANTED) {
                    Alert.alert(
                        'Permission Required',
                        'Camera access is required for card scanning',
                        [{ text: 'OK' }]
                    );
                }
            } catch (error) {
                console.error('Permission error:', error);
                onError?.(error);
            }
        };
        
        requestCameraPermission();
    }, []);
    
    // JavaScript to inject into WebView
    const injectedJavaScript = `
        // Set API configuration from React Native
        window.mobicard_scan_card_url = '${apiConfig.scanCardUrl}';
        window.mobicard_transaction_access_token = '${apiConfig.transactionAccessToken}';
        window.mobicard_token_id = '${apiConfig.tokenId}';
        
        // Initialize scanner when page loads
        document.addEventListener('DOMContentLoaded', function() {
            if (window.MobicardScanner) {
                window.MobicardScanner.init();
                
                // Listen for scanner events and post to React Native
                document.addEventListener('mobicard:scanSuccess', function(event) {
                    window.ReactNativeWebView.postMessage(
                        JSON.stringify({
                            type: 'SCAN_SUCCESS',
                            data: event.detail
                        })
                    );
                });
                
                document.addEventListener('mobicard:scanError', function(event) {
                    window.ReactNativeWebView.postMessage(
                        JSON.stringify({
                            type: 'SCAN_ERROR',
                            data: event.detail
                        })
                    );
                });
                
                document.addEventListener('mobicard:sessionExpired', function() {
                    window.ReactNativeWebView.postMessage(
                        JSON.stringify({
                            type: 'SESSION_EXPIRED'
                        })
                    );
                });
            }
        });
        
        // Add scan frame styling
        const style = document.createElement('style');
        style.textContent = \`
            .camera-container {
                position: relative;
                width: 100%;
                height: 100vh;
                background: #000;
                overflow: hidden;
            }
            
            video {
                width: 100%;
                height: 100%;
                object-fit: cover;
            }
            
            .scan-frame {
                position: absolute;
                top: 50%;
                left: 50%;
                width: 85%;
                height: 55%;
                transform: translate(-50%, -50%);
                border: 2px solid rgba(255,255,255,0.8);
                border-radius: 12px;
                box-shadow: 0 0 0 9999px rgba(0,0,0,0.4);
                pointer-events: none;
            }
            
            .scan-hint {
                position: absolute;
                top: 20px;
                width: 100%;
                text-align: center;
                font-size: 14px;
                color: #0f0;
                text-shadow: 0 0 10px #0f0;
                z-index: 20;
            }
            
            .floating-btn {
                position: absolute;
                bottom: 20px;
                left: 50%;
                transform: translateX(-50%);
                z-index: 10;
                background: white;
                color: black;
                border: none;
                padding: 12px 24px;
                border-radius: 25px;
                font-weight: bold;
            }
        \`;
        document.head.appendChild(style);
        
        // Add required HTML elements
        const container = document.getElementById('scanner-container');
        container.innerHTML = \`
            <div class="camera-container">
                <video id="camera_preview" autoplay playsinline></video>
                <div class="scan-hint" id="scan_hint">Align card inside the frame</div>
                <div class="scan-frame"></div>
                <div class="scan-loader" id="scan_loader">Scanning...</div>
                <button class="floating-btn" id="capture_btn">Capture Now</button>
            </div>
            <canvas id="capture_canvas" style="display:none;"></canvas>
        \`;
        
        true; // Required for iOS
    `;
    
    // Handle messages from WebView
    const handleMessage = (event) => {
        try {
            const message = JSON.parse(event.nativeEvent.data);
            
            switch (message.type) {
                case 'SCAN_SUCCESS':
                    onScanSuccess?.(message.data);
                    break;
                case 'SCAN_ERROR':
                    onError?.(new Error(message.data.error || 'Scan failed'));
                    break;
                case 'SESSION_EXPIRED':
                    Alert.alert(
                        'Session Expired',
                        'Your session has expired. Please try again.',
                        [{ text: 'OK' }]
                    );
                    break;
            }
        } catch (error) {
            console.error('Error parsing WebView message:', error);
        }
    };
    
    if (!hasPermission) {
        return (
            <View style={styles.container}>
                <Text>Waiting for camera permission...</Text>
            </View>
        );
    }
    
    return (
        <View style={styles.container}>
            <WebView
                ref={webViewRef}
                source={{{ html: htmlWrapper }}}
                injectedJavaScript={injectedJavaScript}
                onMessage={handleMessage}
                style={styles.webview}
                javaScriptEnabled={true}
                domStorageEnabled={true}
                startInLoadingState={true}
                mixedContentMode="always"
                onError={(error) => {
                    console.error('WebView error:', error);
                    onError?.(error);
                }}
                onHttpError={(error) => {
                    console.error('WebView HTTP error:', error);
                    onError?.(error);
                }}
            />
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#000',
    },
    webview: {
        flex: 1,
        backgroundColor: '#000',
    },
});

export default CardScanner;

// Usage Example:
// <CardScanner
//     apiConfig=<?php echo e(//         scanCardUrl: 'https://api.mobicard.com/v1/scan',
//         transactionAccessToken: 'your_token_here',
//         tokenId: 'your_token_id'
//); ?>

//     onScanSuccess={(result) => console.log('Scan result:', result)}
//     onError={(error) => console.error('Error:', error)}
// />
Flutter Implementation
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:permission_handler/permission_handler.dart';

class CardScannerScreen extends StatefulWidget {
  final String scanCardUrl;
  final String transactionAccessToken;
  final String tokenId;
  final Function(Map<String, dynamic>) onScanSuccess;
  final Function(String) onError;

  CardScannerScreen({
    required this.scanCardUrl,
    required this.transactionAccessToken,
    required this.tokenId,
    required this.onScanSuccess,
    required this.onError,
  });

  @override
  _CardScannerScreenState createState() => _CardScannerScreenState();
}

class _CardScannerScreenState extends State<CardScannerScreen> {
  late WebViewController _controller;
  bool _hasCameraPermission = false;

  // HTML template for scanner
  String get _htmlContent => '''
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Card Scanner</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; color: #fff; font-family: sans-serif; }
        
        .scanner-container {
            position: relative;
            width: 100vw;
            height: 100vh;
            overflow: hidden;
        }
        
        #cameraPreview {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        
        .scan-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
        }
        
        .scan-frame {
            position: absolute;
            top: 50%;
            left: 50%;
            width: 85%;
            height: 55%;
            transform: translate(-50%, -50%);
            border: 3px solid rgba(0, 255, 0, 0.8);
            border-radius: 15px;
            box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);
        }
        
        .scan-hint {
            position: absolute;
            top: 30px;
            width: 100%;
            text-align: center;
            font-size: 16px;
            color: #0f0;
            text-shadow: 0 0 10px #0f0;
        }
        
        .capture-btn {
            position: absolute;
            bottom: 40px;
            left: 50%;
            transform: translateX(-50%);
            background: #fff;
            color: #000;
            border: none;
            padding: 15px 30px;
            border-radius: 30px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            z-index: 100;
        }
        
        .loader {
            position: absolute;
            bottom: 100px;
            width: 100%;
            text-align: center;
            color: #0f0;
            display: none;
        }
    </style>
</head>
<body>
    <div class="scanner-container">
        <video id="cameraPreview" autoplay playsinline></video>
        <div class="scan-overlay">
            <div class="scan-frame"></div>
            <div class="scan-hint" id="scanHint">Align card in frame</div>
            <div class="loader" id="scanLoader">Processing...</div>
        </div>
        <button class="capture-btn" id="captureBtn">Capture Card</button>
    </div>
    
    <canvas id="captureCanvas" style="display:none;"></canvas>
    
    <!-- jQuery for AJAX -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    
    <script>
        // API Configuration
        const MOBICARD_CONFIG = {
            scanCardUrl: '${widget.scanCardUrl}',
            transactionAccessToken: '${widget.transactionAccessToken}',
            tokenId: '${widget.tokenId}'
        };
        
        // Initialize scanner
        function initScanner() {
            if (window.MobicardScanner) {
                window.MobicardScanner.init(MOBICARD_CONFIG);
                
                // Set up event listeners
                document.addEventListener('mobicard:scanSuccess', function(event) {
                    // Send result to Flutter
                    if (window.flutter_inappwebview) {
                        window.flutter_inappwebview.callHandler('scanSuccess', event.detail);
                    }
                });
                
                document.addEventListener('mobicard:scanError', function(event) {
                    if (window.flutter_inappwebview) {
                        window.flutter_inappwebview.callHandler('scanError', event.detail);
                    }
                });
            }
        }
        
        // Initialize when page loads
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initScanner);
        } else {
            initScanner();
        }
    </script>
</body>
</html>
''';

  @override
  void initState() {
    super.initState();
    _requestCameraPermission();
  }

  Future<void> _requestCameraPermission() async {
    final status = await Permission.camera.request();
    setState(() {
      _hasCameraPermission = status.isGranted;
    });
    
    if (!_hasCameraPermission) {
      widget.onError('Camera permission denied');
    }
  }

  @override
  Widget build(BuildContext context) {
    if (!_hasCameraPermission) {
      return Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.camera_alt, size: 64, color: Colors.grey),
              SizedBox(height: 20),
              Text('Camera permission required',
                  style: TextStyle(fontSize: 18)),
              SizedBox(height: 10),
              ElevatedButton(
                onPressed: _requestCameraPermission,
                child: Text('Grant Permission'),
              ),
            ],
          ),
        ),
      );
    }

    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: WebView(
          initialUrl: 'about:blank',
          javascriptMode: JavascriptMode.unrestricted,
          onWebViewCreated: (WebViewController webViewController) {
            _controller = webViewController;
            _controller.loadUrl(
                Uri.dataFromString(_htmlContent, mimeType: 'text/html')
                    .toString());
          },
          javascriptChannels: <JavascriptChannel>{
            _scanSuccessChannel(),
            _scanErrorChannel(),
          },
          onPageFinished: (String url) {
            // Inject additional JavaScript if needed
            _controller.runJavascript('''
                console.log('Scanner loaded successfully');
            ''');
          },
        ),
      ),
    );
  }

  JavascriptChannel _scanSuccessChannel() {
    return JavascriptChannel(
      name: 'scanSuccess',
      onMessageReceived: (JavascriptMessage message) {
        try {
          final result = jsonDecode(message.message);
          widget.onScanSuccess(result);
        } catch (e) {
          widget.onError('Failed to parse scan result: $e');
        }
      },
    );
  }

  JavascriptChannel _scanErrorChannel() {
    return JavascriptChannel(
      name: 'scanError',
      onMessageReceived: (JavascriptMessage message) {
        widget.onError(message.message);
      },
    );
  }
}

// Usage Example:
// Navigator.push(
//   context,
//   MaterialPageRoute(
//     builder: (context) => CardScannerScreen(
//       scanCardUrl: 'https://api.mobicard.com/v1/scan',
//       transactionAccessToken: 'your_token',
//       tokenId: 'your_token_id',
//       onScanSuccess: (result) {
//         print('Card scanned: \$result');
//         Navigator.pop(context);
//       },
//       onError: (error) {
//         print('Error: \$error');
//         ScaffoldMessenger.of(context).showSnackBar(
//           SnackBar(content: Text('Scan failed: \$error')),
//         );
//       },
//     ),
//   ),
// );
Ionic Implementation
// card-scanner.service.ts
import { Injectable } from '@angular/core';
import { Plugins, CameraResultType, CameraSource } from '@capacitor/core';
import { Platform } from '@ionic/angular';

const { Camera, Permissions } = Plugins;

export interface ScanResult {
  status: string;
  card_information: {
    card_number: string;
    card_expiry_date: string;
    card_brand: string;
    // ... other card fields
  };
}

@Injectable({
  providedIn: 'root'
})
export class CardScannerService {
  private scannerInitialized = false;

  constructor(private platform: Platform) {}

  async initializeScanner(config: {
    scanCardUrl: string;
    transactionAccessToken: string;
    tokenId: string;
  }): Promise<boolean> {
    // Request camera permissions
    const cameraPermission = await Permissions.request({ name: 'camera' });
    
    if (cameraPermission.state !== 'granted') {
      throw new Error('Camera permission denied');
    }

    // Load scanner script
    await this.loadScannerScript();

    // Initialize scanner
    if ((window as any).MobicardScanner) {
      (window as any).MobicardScanner.init(config);
      this.scannerInitialized = true;
      
      // Set up event listeners
      this.setupEventListeners();
      
      return true;
    }
    
    return false;
  }

  private loadScannerScript(): Promise<void> {
    return new Promise((resolve, reject) => {
      // Check if already loaded
      if ((window as any).MobicardScanner) {
        resolve();
        return;
      }

      // Load jQuery first
      this.loadScript('https://code.jquery.com/jquery-3.7.1.min.js')
        .then(() => {
          // Load Mobicard scanner
          return this.loadScript('assets/js/mobicard-scanner.min.js');
        })
        .then(() => resolve())
        .catch(reject);
    });
  }

  private loadScript(src: string): Promise<void> {
    return new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = src;
      script.onload = () => resolve();
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  private setupEventListeners(): void {
    document.addEventListener('mobicard:scanSuccess', (event: any) => {
      this.onScanSuccess.emit(event.detail);
    });

    document.addEventListener('mobicard:scanError', (event: any) => {
      this.onScanError.emit(event.detail);
    });

    document.addEventListener('mobicard:sessionExpired', () => {
      this.onSessionExpired.emit();
    });
  }

  // Start camera scanning
  startCameraScan(): void {
    if (!this.scannerInitialized) {
      throw new Error('Scanner not initialized');
    }
    
    if ((window as any).MobicardScanner) {
      (window as any).MobicardScanner.start();
    }
  }

  // Stop camera scanning
  stopCameraScan(): void {
    if ((window as any).MobicardScanner) {
      (window as any).MobicardScanner.stop();
    }
  }

  // Upload image from gallery
  async uploadFromGallery(): Promise<ScanResult> {
    try {
      const image = await Camera.getPhoto({
        quality: 90,
        allowEditing: false,
        resultType: CameraResultType.DataUrl,
        source: CameraSource.Photos
      });

      if (image.dataUrl) {
        // Convert data URL to blob
        const blob = this.dataURLtoBlob(image.dataUrl);
        
        // Upload via scanner
        if ((window as any).MobicardScanner) {
          return new Promise((resolve, reject) => {
            const successHandler = (result: ScanResult) => {
              document.removeEventListener('mobicard:uploadSuccess', successHandler);
              document.removeEventListener('mobicard:uploadError', errorHandler);
              resolve(result);
            };

            const errorHandler = (error: any) => {
              document.removeEventListener('mobicard:uploadSuccess', successHandler);
              document.removeEventListener('mobicard:uploadError', errorHandler);
              reject(error);
            };

            document.addEventListener('mobicard:uploadSuccess', successHandler);
            document.addEventListener('mobicard:uploadError', errorHandler);

            (window as any).MobicardScanner.upload(blob);
          });
        }
      }
      
      throw new Error('Failed to get image');
    } catch (error) {
      throw error;
    }
  }

  private dataURLtoBlob(dataurl: string): Blob {
    const arr = dataurl.split(',');
    const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg';
    const bstr = atob(arr[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    
    return new Blob([u8arr], { type: mime });
  }

  // Event emitters
  onScanSuccess = new EventEmitter<ScanResult>();
  onScanError = new EventEmitter<any>();
  onSessionExpired = new EventEmitter<void>();

  ngOnDestroy(): void {
    this.stopCameraScan();
  }
}

// card-scanner.component.ts
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { CardScannerService, ScanResult } from './card-scanner.service';

@Component({
  selector: 'app-card-scanner',
  template: `
    <div class="scanner-container">
      <div #scannerElement class="scanner-element"></div>
      
      <div class="scanner-controls" *ngIf="showControls">
        <ion-button (click)="startScan()" expand="block" color="primary">
          <ion-icon name="camera"></ion-icon>
          Start Scan
        </ion-button>
        
        <ion-button (click)="uploadImage()" expand="block" color="secondary">
          <ion-icon name="image"></ion-icon>
          Upload Image
        </ion-button>
        
        <ion-button (click)="stopScan()" expand="block" color="danger">
          <ion-icon name="stop"></ion-icon>
          Stop
        </ion-button>
      </div>
      
      
      <div class="scanner-status" *ngIf="statusMessage">
        {{statusMessage}}
      </div>
	  
	  
	  
    </div>
  `,
  styles: [`
    .scanner-container {
      position: relative;
      width: 100%;
      height: 100%;
      background: #000;
      overflow: hidden;
    }
    
    .scanner-element {
      width: 100%;
      height: 100%;
    }
    
    .scanner-controls {
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      padding: 0 20px;
      z-index: 100;
    }
    
    .scanner-status {
      position: absolute;
      top: 20px;
      left: 0;
      right: 0;
      text-align: center;
      color: #0f0;
      font-size: 14px;
      z-index: 100;
    }
  `]
})
export class CardScannerComponent implements OnInit, OnDestroy {
  @Input() apiConfig: {
    scanCardUrl: string;
    transactionAccessToken: string;
    tokenId: string;
  };
  
  @Input() showControls = true;
  @Output() scanComplete = new EventEmitter<ScanResult>();
  @Output() scanError = new EventEmitter<any>();
  
  statusMessage = '';
  isScanning = false;

  constructor(private scannerService: CardScannerService) {}

  async ngOnInit(): Promise<void> {
    try {
      await this.scannerService.initializeScanner(this.apiConfig);
      
      // Subscribe to events
      this.scannerService.onScanSuccess.subscribe(result => {
        this.isScanning = false;
        this.statusMessage = 'Scan successful!';
        this.scanComplete.emit(result);
      });
      
      this.scannerService.onScanError.subscribe(error => {
        this.isScanning = false;
        this.statusMessage = 'Scan failed';
        this.scanError.emit(error);
      });
      
      this.scannerService.onSessionExpired.subscribe(() => {
        this.statusMessage = 'Session expired. Please try again.';
      });
      
    } catch (error) {
      this.scanError.emit(error);
    }
  }

  startScan(): void {
    this.isScanning = true;
    this.statusMessage = 'Align card in frame...';
    this.scannerService.startCameraScan();
  }

  stopScan(): void {
    this.isScanning = false;
    this.statusMessage = '';
    this.scannerService.stopCameraScan();
  }

  async uploadImage(): Promise<void> {
    try {
      this.statusMessage = 'Processing image...';
      const result = await this.scannerService.uploadFromGallery();
      this.scanComplete.emit(result);
    } catch (error) {
      this.scanError.emit(error);
    } finally {
      this.statusMessage = '';
    }
  }

  ngOnDestroy(): void {
    this.stopScan();
  }
}

// Usage in app.module.ts
// import { CardScannerComponent } from './components/card-scanner/card-scanner.component';
// 
// @NgModule({
//   declarations: [CardScannerComponent],
//   exports: [CardScannerComponent]
// })

// Usage in page:
// <app-card-scanner
//   [apiConfig]="{
//     scanCardUrl: 'https://api.mobicard.com/v1/scan',
//     transactionAccessToken: 'your_token',
//     tokenId: 'your_token_id'
//   }"
//   (scanComplete)="handleScanComplete($event)"
//   (scanError)="handleScanError($event)"
// >
// </app-card-scanner>
PWA Implementation
// service-worker.js
const CACHE_NAME = 'mobicard-scanner-v1';
const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/styles.css',
  '/mobicard-scanner.js',
  'https://code.jquery.com/jquery-3.7.1.min.js',
  'https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css',
  'https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js'
];

// Install service worker
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(ASSETS_TO_CACHE))
      .then(() => self.skipWaiting())
  );
});

// Activate service worker
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});

// Fetch strategy: cache first, then network
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        if (response) {
          return response;
        }
        
        return fetch(event.request).then(response => {
          // Don't cache API calls
          if (event.request.url.includes('/api/')) {
            return response;
          }
          
          // Cache static assets
          if (response.status === 200) {
            const responseToCache = response.clone();
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });
          }
          
          return response;
        });
      })
  );
});

// Background sync for offline scans
self.addEventListener('sync', event => {
  if (event.tag === 'sync-scans') {
    event.waitUntil(syncScans());
  }
});

async function syncScans() {
  const db = await openScansDatabase();
  const pendingScans = await db.getAll('pending');
  
  for (const scan of pendingScans) {
    try {
      const response = await fetch(scan.url, {
        method: 'POST',
        headers: scan.headers,
        body: scan.body
      });
      
      if (response.ok) {
        await db.delete('pending', scan.id);
      }
    } catch (error) {
      console.error('Sync failed:', error);
    }
  }
}

// manifest.json
{
  "name": "Mobicard Scanner",
  "short_name": "Card Scanner",
  "description": "Scan credit cards using your camera",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#000000",
  "orientation": "portrait",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/scanner-1.png",
      "sizes": "1080x1920",
      "type": "image/png",
      "form_factor": "narrow"
    },
    {
      "src": "/screenshots/scanner-2.png",
      "sizes": "1920x1080",
      "type": "image/png",
      "form_factor": "wide"
    }
  ],
  "categories": ["finance", "productivity"],
  "shortcuts": [
    {
      "name": "Scan Card",
      "short_name": "Scan",
      "description": "Open camera to scan a card",
      "url": "/scan"
    },
    {
      "name": "Upload Image",
      "short_name": "Upload",
      "description": "Upload card image from gallery",
      "url": "/upload"
    }
  ]
}

// PWA Scanner Component
class PWACardScanner extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.init();
  }

  async init() {
    // Check if PWA is installed
    if (window.matchMedia('(display-mode: standalone)').matches) {
      this.isPWA = true;
    }

    // Check network status
    this.networkStatus = navigator.onLine ? 'online' : 'offline';
    window.addEventListener('online', () => this.handleNetworkChange('online'));
    window.addEventListener('offline', () => this.handleNetworkChange('offline'));

    // Request install prompt
    this.deferredPrompt = null;
    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault();
      this.deferredPrompt = e;
      this.showInstallPrompt();
    });

    // Initialize scanner
    await this.loadScanner();
  }

  async loadScanner() {
    // Load scanner script if not already loaded
    if (!window.MobicardScanner) {
      await this.loadScript('https://code.jquery.com/jquery-3.7.1.min.js');
      await this.loadScript('/js/mobicard-scanner.js');
    }

    // Initialize with config
    const config = {
      scanCardUrl: this.getAttribute('scan-card-url'),
      transactionAccessToken: this.getAttribute('access-token'),
      tokenId: this.getAttribute('token-id')
    };

    window.MobicardScanner.init(config);

    // Set up offline handling
    if ('serviceWorker' in navigator && 'SyncManager' in window) {
      navigator.serviceWorker.ready.then(registration => {
        this.syncRegistration = registration;
      });
    }
  }

  handleNetworkChange(status) {
    this.networkStatus = status;
    
    if (status === 'online') {
      // Sync pending scans
      if (this.syncRegistration) {
        this.syncRegistration.sync.register('sync-scans');
      }
    }
  }

  showInstallPrompt() {
    if (this.deferredPrompt && !this.isPWA) {
      const installButton = document.createElement('button');
      installButton.className = 'install-prompt';
      installButton.innerHTML = '📱 Install App';
      installButton.onclick = () => this.installPWA();
      
      this.shadowRoot.appendChild(installButton);
    }
  }

  async installPWA() {
    if (this.deferredPrompt) {
      this.deferredPrompt.prompt();
      const { outcome } = await this.deferredPrompt.userChoice;
      
      if (outcome === 'accepted') {
        console.log('PWA installed');
      }
      
      this.deferredPrompt = null;
    }
  }

  async saveForOffline(scanData) {
    if (!navigator.onLine) {
      const db = await this.openDatabase();
      await db.add('pending', {
        url: scanData.url,
        headers: scanData.headers,
        body: scanData.body,
        timestamp: Date.now()
      });

      // Register for sync when back online
      if (this.syncRegistration) {
        this.syncRegistration.sync.register('sync-scans');
      }

      return true;
    }
    return false;
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          position: relative;
        }
        
        .scanner-wrapper {
          position: relative;
          width: 100%;
          height: 100vh;
          background: #000;
        }
        
        .offline-indicator {
          position: absolute;
          top: 10px;
          right: 10px;
          background: #ff6b6b;
          color: white;
          padding: 5px 10px;
          border-radius: 15px;
          font-size: 12px;
          z-index: 1000;
        }
        
        .install-prompt {
          position: absolute;
          top: 10px;
          left: 10px;
          background: #4CAF50;
          color: white;
          border: none;
          padding: 10px 15px;
          border-radius: 20px;
          cursor: pointer;
          z-index: 1000;
          box-shadow: 0 2px 5px rgba(0,0,0,0.2);
        }
        
        .pwa-features {
          position: absolute;
          bottom: 20px;
          left: 0;
          right: 0;
          text-align: center;
          color: white;
          font-size: 12px;
          opacity: 0.7;
        }
      </style>
      
      <div class="scanner-wrapper">
        ${this.networkStatus === 'offline' ? 
          '<div class="offline-indicator">Offline Mode</div>' : ''}
        
        <!-- Scanner will be injected here -->
        <div id="pwa-scanner-container"></div>
        
        <div class="pwa-features">
          Works offline • Install as app • No downloads required
        </div>
      </div>
    `;
  }

  connectedCallback() {
    this.render();
  }
}

// Register the custom element
customElements.define('pwa-card-scanner', PWACardScanner);

// Usage in HTML:
// <pwa-card-scanner
//   scan-card-url="https://api.mobicard.com/v1/scan"
//   access-token="your_token"
//   token-id="your_token_id"
// >
// </pwa-card-scanner>

// Register service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('ServiceWorker registered:', registration);
      })
      .catch(error => {
        console.error('ServiceWorker registration failed:', error);
      });
  });
}
Key Benefits for Mobile Integration:
  • Cross-Platform: Same JavaScript code works on iOS, Android, and web
  • Native Performance: Camera access via device-native APIs
  • Offline Support: PWA capabilities for offline scanning
  • App Store Ready: Can be packaged for Apple App Store and Google Play
  • Automatic Updates: Web-based deployment allows instant updates
  • Small App Size: No heavy native SDKs required

Sample Code : Full PHP Implementation (Method 1)

You may copy-paste the code below as-is but remember to store your 'api_key' and 'secret_key' in your .env file.

Full PHP, HTML & JavaScript Implementation
METHOD 1
<?php

// Mandatory claims
$mobicard_version = "2.0";
$mobicard_mode = "LIVE"; // production
$mobicard_merchant_id = "4";
$mobicard_api_key = "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9";
$mobicard_secret_key = "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9";

$mobicard_token_id = abs(rand(1000000,1000000000));
$mobicard_token_id = "$mobicard_token_id";

$mobicard_txn_reference = abs(rand(1000000,1000000000));
$mobicard_txn_reference = "$mobicard_txn_reference";

$mobicard_service_id = "20000"; // Scan Card service ID
$mobicard_service_type = "1"; // Use '1' for CARD SCAN METHOD 1

$mobicard_extra_data = "your_custom_data_here_will_be_returned_as_is";

// Create JWT Header
$mobicard_jwt_header = [
    "typ" => "JWT",
    "alg" => "HS256"
];
$mobicard_jwt_header = rtrim(strtr(base64_encode(json_encode($mobicard_jwt_header)), '+/', '-_'), '=');

// Create JWT Payload
$mobicard_jwt_payload = array(
    "mobicard_version" => "$mobicard_version",
    "mobicard_mode" => "$mobicard_mode",
    "mobicard_merchant_id" => "$mobicard_merchant_id",
    "mobicard_api_key" => "$mobicard_api_key",
    "mobicard_service_id" => "$mobicard_service_id",
    "mobicard_service_type" => "$mobicard_service_type",
    "mobicard_token_id" => "$mobicard_token_id",
    "mobicard_txn_reference" => "$mobicard_txn_reference",
    "mobicard_extra_data" => "$mobicard_extra_data"
);

$mobicard_jwt_payload = rtrim(strtr(base64_encode(json_encode($mobicard_jwt_payload)), '+/', '-_'), '=');

// Generate Signature
$header_payload = $mobicard_jwt_header . '.' . $mobicard_jwt_payload;
$mobicard_jwt_signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $header_payload, $mobicard_secret_key, true)), '+/', '-_'), '=');

// Create Final JWT
$mobicard_auth_jwt = "$mobicard_jwt_header.$mobicard_jwt_payload.$mobicard_jwt_signature";

// Request Access Token
$mobicard_request_access_token_url = "https://mobicardsystems.com/api/v1/card_scan";

$mobicard_curl_post_data = array('mobicard_auth_jwt' => $mobicard_auth_jwt);

$curl_mobicard = curl_init();
curl_setopt($curl_mobicard, CURLOPT_URL, $mobicard_request_access_token_url);
curl_setopt($curl_mobicard, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl_mobicard, CURLOPT_POST, true);
curl_setopt($curl_mobicard, CURLOPT_POSTFIELDS, json_encode($mobicard_curl_post_data));
curl_setopt($curl_mobicard, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl_mobicard, CURLOPT_SSL_VERIFYPEER, false);
$mobicard_curl_response = curl_exec($curl_mobicard);
curl_close($curl_mobicard);

// Parse Response
$mobicard_curl_response = json_decode($mobicard_curl_response, true);

if($mobicard_curl_response && $mobicard_curl_response['status_code'] == "200") {
    $status_code = $mobicard_curl_response['status_code'];
    $status_message = $mobicard_curl_response['status_message'];
    $mobicard_transaction_access_token = $mobicard_curl_response['mobicard_transaction_access_token'];
    $mobicard_token_id = $mobicard_curl_response['mobicard_token_id'];
    $mobicard_txn_reference = $mobicard_curl_response['mobicard_txn_reference'];
    $mobicard_scan_card_url = $mobicard_curl_response['mobicard_scan_card_url'];
    
    // These variables are now available for the UI script
    // $mobicard_transaction_access_token, $mobicard_token_id, $mobicard_scan_card_url
} else {
    // Handle error
    var_dump($mobicard_curl_response);
    exit();
}

?><!DOCTYPE html>
<html>
<head>

    <meta charset="utf-8">
	
    <title>Mobicard | Scan or Upload Card</title>
	
    <meta name="viewport" content="width=device-width, initial-scale=1">
	<meta name="copyright" content="MobiCard">
    <meta name="author" content="MobiCard">

    <!-- PWA -->
    <link rel="manifest" href="public/manifest.json">
    <meta name="theme-color" content="#000000">

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">

    <style>
        body { background:#000; color:#fff; }

        .nav-tabs .nav-link { border-radius:0; color:#aaa; }
        .nav-tabs .nav-link.active { background:#111; color:#fff; border-color:#333; }

        .camera-container {
            position: relative;
            width: 100%;
            height: 85vh;
            background: #000;
            overflow: hidden;
        }

        video { width:100%; height:100%; object-fit:cover; }

        .scan-frame {
            position:absolute;
            top:50%; left:50%;
            width:85%; height:55%;
            transform:translate(-50%,-50%);
            border:2px solid rgba(255,255,255,0.8);
            border-radius:12px;
            box-shadow:0 0 0 9999px rgba(0,0,0,0.4);
            pointer-events:none;
            transition: all 0.3s ease;
        }

        .scan-frame.locked {
            border-color:#0f0;
            box-shadow:0 0 20px #0f0, 0 0 0 9999px rgba(0,0,0,0.5);
        }

        .scan-frame.bad { border-color:#f00; }

        .scan-hint {
            position:absolute;
            top:20px;
            width:100%;
            text-align:center;
            font-size:14px;
            color:#0f0;
            text-shadow:0 0 10px #0f0;
            z-index:20;
        }
		
        .scan-line {
            position:absolute;
            top:0; left:0;
            width:100%; height:3px;
            background:rgba(0,255,0,0.8);
            animation: scanmove 2s linear infinite;
            z-index:5;
        }

        @keyframes  scanmove {
            0% { top:0; }
            100% { top:100%; }
        }

        .scan-loader {
            position:absolute;
            bottom:80px;
            width:100%;
            text-align:center;
            display:none;
            color:#0f0;
        }

        .floating-btn {
            position:absolute;
            bottom:20px;
            left:50%;
            transform:translateX(-50%);
            z-index:10;
        }

        .upload-box {
            border:2px dashed #444;
            padding:40px;
            text-align:center;
            margin-top:40px;
        }

        .result-label { color:#777; font-size:12px; }
    </style>
	
	
	
	<style>
		
		
		/* Uploading message styles */
		.uploading-message {
			display: none;
			position: fixed;
			top: 50%;
			left: 50%;
			transform: translate(-50%, -50%);
			background: #000000 !important;
			color: white !important;
			padding: 20px 30px;
			border-radius: 10px;
			z-index: 99999;
			font-weight: bold;
			text-align: center;
			box-shadow: 0 0 20px rgba(0,0,0,0.5);
			border: 2px solid #fff;
			font-size: 16px;
			min-width: 300px;
		}
		
		/* Overlay to block interaction while uploading */
		.uploading-overlay {
			display: none;
			position: fixed;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			background: rgba(0,0,0,0.7);
			z-index: 99998;
		}
		
		/* Lock screen styles */
		.lock-screen {
			display: none;
			position: absolute;
			top: 0;
			left: 0;
			width: 100%;
			height: 100%;
			background: rgba(0, 0, 0, 0.85);
			z-index: 30;
			flex-direction: column;
			justify-content: center;
			align-items: center;
			color: #fff;
			text-align: center;
		}
		
		.refresh-icon {
			font-size: 48px;
			margin-bottom: 20px;
			cursor: pointer;
			animation: pulse 2s infinite;
			color: #0f0;
		}
		
		@keyframes  pulse {
			0% { transform: scale(1); opacity: 0.7; }
			50% { transform: scale(1.1); opacity: 1; }
			100% { transform: scale(1); opacity: 0.7; }
		}
		
		.lock-message {
			font-size: 16px;
			color: #ccc;
			margin-bottom: 20px;
		}
		
		/* Mobile-specific scan frame adjustments */
		@media (max-width: 768px) {
			.scan-frame {
				width: 92% !important;
				height: 36% !important;
				border-width: 3px;
			}
		}
		
		/* Desktop-specific scan frame adjustments */
		@media (min-width: 769px) {
			.scan-frame {
				margin-top: -20px;
				width: 45% !important;
				height: 67% !important;
			}
		}
			
	</style>
	

	<style>

		/* ===================== LIGHT RESULT MODAL UI ===================== */

		#result_modal .modal-dialog {
			max-width: 420px;
		}

		#result_modal .modal-content {
			background: linear-gradient(180deg, #ffffff, #f6f8fb);
			color: #0b1220;
			border-radius: 18px;
			border: 1px solid rgba(0,0,0,0.06);
			box-shadow: 0 20px 50px rgba(0,0,0,0.2);
			overflow: hidden;
		}

		/* Header */
		#result_modal .modal-header {
			border-bottom: 1px solid rgba(0,0,0,0.08);
			background: linear-gradient(90deg, #f8fbff, #eef3f9);
			padding: 18px 20px;
			display: flex;
			align-items: center;
			justify-content: space-between;
		}

		/* Title */
		#result_modal .modal-title {
			font-weight: 700;
			letter-spacing: 0.3px;
			font-size: 18px;
			color: #0b1220;
		}

		/* Success check animation */
		#result_modal .modal-header::after {
			/* content: "✓"; */
			width: 34px;
			height: 34px;
			border-radius: 50%;
			background: linear-gradient(135deg, #22c55e, #16a34a);
			color: #fff;
			display: inline-flex;
			align-items: center;
			justify-content: center;
			font-weight: bold;
			font-size: 18px;
			box-shadow: 0 6px 18px rgba(34,197,94,0.5);
			animation: popSuccess 0.6s ease-out;
		}

		/* Success animation */
		@keyframes  popSuccess {
			0% { transform: scale(0); opacity: 0; }
			60% { transform: scale(1.2); opacity: 1; }
			100% { transform: scale(1); }
		}

		/* Body */
		#result_modal .modal-body {
			padding: 20px;
		}

		/* Card blocks */
		#result_modal .modal-body .mb-2 {
			background: #ffffff;
			border-radius: 14px;
			padding: 12px 14px;
			margin-bottom: 12px !important;
			border: 1px solid rgba(0,0,0,0.06);
			box-shadow: 0 4px 12px rgba(0,0,0,0.04);
		}

		/* Labels */
		#result_modal .result-label {
			font-size: 11px;
			text-transform: uppercase;
			letter-spacing: 0.12em;
			color: #6b7a90;
			margin-bottom: 4px;
		}

		/* Values */
		#result_modal #res_card_number,
		#result_modal #res_card_expiry {
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
		}

		/* ===================== CARD BRAND AUTO LOGO ===================== */

		/* Brand container using background on card number field */
		#result_modal #res_card_number {
			position: relative;
			padding-right: 48px;
		}

		/* Default */
		#result_modal #res_card_number::after {
			content: "";
			position: absolute;
			right: 0;
			top: 50%;
			transform: translateY(-50%);
			width: 36px;
			height: 24px;
			background-size: contain;
			background-repeat: no-repeat;
			background-position: center;
			opacity: 0.9;
		}
		

		/* Values */
		#result_modal #res_card_number_display,
		#result_modal #res_card_expiry {
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
		}

		/* ===================== CARD BRAND AUTO LOGO ===================== */

		/* Brand container using background on card number field */
		#result_modal #res_card_number_display {
			position: relative;
			padding-right: 48px;
		}

		/* Default */
		#result_modal #res_card_number_display::after {
			content: "";
			position: absolute;
			right: 0;
			top: 50%;
			transform: translateY(-50%);
			width: 36px;
			height: 24px;
			background-size: contain;
			background-repeat: no-repeat;
			background-position: center;
			opacity: 0.9;
		}




		/* Values */
		#result_modal #res_card_number_display,
		#result_modal #res_card_expiry {
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
		}

		/* ===================== CARD BRAND AUTO LOGO ===================== */

		/* Brand container using background on card number field */
		#result_modal #res_card_number_display {
			position: relative;
			padding-right: 48px;
		}

		/* Default */
		#result_modal #res_card_number_display::after {
			content: "";
			position: absolute;
			right: 0;
			top: 50%;
			transform: translateY(-50%);
			width: 36px;
			height: 24px;
			background-size: contain;
			background-repeat: no-repeat;
			background-position: center;
			opacity: 0.9;
		}




		

		/* Apply these classes dynamically via JS to #result_modal */
		#result_modal.visa #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/4/41/Visa_Logo.png');
		}

		#result_modal.mastercard #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg');
		}

		#result_modal.amex #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/3/30/American_Express_logo.svg');
		}

		#result_modal.discover #res_card_number::after {
			background-image: url('https://upload.wikimedia.org/wikipedia/commons/5/5a/Discover_Card_logo.svg');
		}

		/* ===================== CVV INPUT ===================== */

		#result_modal input.form-control {
			background: #f8fafc;
			border: 1px solid rgba(0,0,0,0.12);
			color: #0b1220;
			border-radius: 10px;
			height: 44px;
			font-size: 16px;
		}

		#result_modal input.form-control::placeholder {
			color: #9aa7b8;
		}

		#result_modal input.form-control:focus {
			border-color: #2563eb;
			box-shadow: 0 0 0 2px rgba(37,99,235,0.15);
			background: #ffffff;
		}

		/* Footer */
		#result_modal .modal-footer {
			border-top: 1px solid rgba(0,0,0,0.08);
			padding: 16px 20px;
		}

		/* Buttons */
		#result_modal .btn-secondary {
			background: #f1f5f9;
			border: none;
			color: #334155;
			border-radius: 10px;
			padding: 10px 18px;
		}

		#result_modal .btn-secondary:hover {
			background: #e2e8f0;
			color: #FFFFFF;
		}

		#result_modal .btn-primary {
			background: linear-gradient(135deg, #2563eb, #0ea5e9);
			border: none;
			border-radius: 10px;
			padding: 10px 22px;
			font-weight: 700;
			box-shadow: 0 8px 20px rgba(37,99,235,0.35);
		}

		#result_modal .btn-primary:hover {
			filter: brightness(1.08);
		}

		/* Modal animation polish */
		#result_modal.fade .modal-dialog {
			transform: scale(0.92) translateY(20px);
			transition: all 0.25s ease;
		}

		#result_modal.show .modal-dialog {
			transform: scale(1) translateY(0);
		}



		.nav-tabs .nav-link {
			border-radius: 0;
			color: #aaa;
			border-bottom-color: black !important;
		}

		.nav-tabs .nav-link.active {
			background: #111;
			color: #fff;
			border-color: #333;
			border-bottom-color: black !important;
		}



		/* Validation states */
		.expiry-label.invalid {
			color: #dc3545 !important; /* Red color */
			font-weight: bold;
		}

		.expiry-input-invalid {
			border-color: #dc3545 !important;
			box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important;
		}

		.expiry-input-valid {
			border-color: none !important;
			box-shadow: none !important;
			
			font-size: 18px;
			font-weight: 700;
			letter-spacing: 1px;
			color: #0b1220;
			
		}


		/* Custom button styling for the modal footer */
		#result_modal .modal-footer {
			padding: 0 !important;
		}

		#result_modal .modal-footer .btn {
			padding: 12px 0;
			font-weight: 600;
			font-size: 16px;
			transition: all 0.2s ease;
			margin-top : -15px;
			margin-bottom : 18px;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-secondary {
			background-color: #FFFFFF;
			border: 1px solid #000000;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-secondary:hover {
			background-color: #5a6268;
			border-color: #545b62;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-primary {
			background-color: #000 !important;
			border: 1px solid #000 !important;
			color: white;
			border-radius: 10px;
		}

		#result_modal .modal-footer .btn-primary:hover {
			background-color: #333 !important;
			border-color: #333 !important;
			border-radius: 10px;
		}

		/* Remove Bootstrap's default modal footer border */
		#result_modal .modal-content {
			border-radius: 12px;
			overflow: hidden;
		}


		footer {
			text-align: center; /* Centers the text horizontally */
			padding: 20px 0;    /* Adds some breathing room top and bottom */
			width: 100%;
		}

		footer p {
			color: #333333;     /* A very dark grey */
			font-size: 0.75rem; /* Makes the font small (approx 12px) */
			font-family: sans-serif; /* Clean look */
			margin: 0;
		}


	</style>

	
</head>
<body>

<div class="container-fluid p-0">

    <!-- Tabs -->
    <ul class="nav nav-tabs nav-justified" style="border-bottom-color: #111111 !important;">
        <li class="nav-item"><a class="nav-link active" id="scan_card_tab" data-toggle="tab" href="#scan_tab">Scan Card</a></li>
        <li class="nav-item"><a class="nav-link" id="upload_card_tab" data-toggle="tab" href="#upload_tab">Upload Card</a></li>
    </ul>

    <div class="tab-content">

        <!-- ===================== SCAN ===================== -->
        <div class="tab-pane fade show active" id="scan_tab">
            <div class="camera-container">
                <video id="camera_preview" autoplay playsinline></video>

                <div class="scan-hint" id="scan_hint">Align card inside the frame</div>
                <div class="scan-line"></div>
                <div class="scan-frame"></div>
                <div class="scan-loader" id="scan_loader">Scanning… Please hold steady</div>
                
                <!-- Lock Screen -->				
                <div class="lock-screen" id="lock_screen">
					<div id="refresh_btn">
						<div class="refresh-icon" id="refresh_icon">⟳</div>
						<div class="lock-message">Tap to refresh</div>
					</div>
				</div>

                <button class="btn btn-light floating-btn" id="capture_btn">Capture Now</button>
            </div>

            <canvas id="capture_canvas" style="display:none;"></canvas>
        </div>

        <!-- ===================== UPLOAD ===================== -->
        <div class="tab-pane fade" id="upload_tab">
            <div class="container">
                <div class="upload-box">
                    <h5>Select Card Image</h5>
                    <input type="file" id="upload_input" name="mobicard_scan_card_photo" class="form-control mt-3" accept="image/*,application/pdf" style="padding: .375rem .75rem 2.2rem;">
                    <button class="btn btn-light mt-4" id="upload_submit_btn">Upload & Submit</button>
                </div>
				
				
								

				<!-- Uploading Message Div -->
				<div id="uploadingMessage" class="uploading-message">
					⏳ Uploading... Please wait...
				</div>

				<div id="uploadingOverlay" class="uploading-overlay"></div>
				
            </div>
        </div>

    </div>
</div>


<!-- ===================== RESULT MODAL ===================== -->
<div class="modal fade" id="result_modal">
    <div class="modal-dialog modal-dialog-centered">
        <div class="modal-content text-dark">
            <div class="modal-header">
                <h5 class="modal-title">Your Checkout Page</h5>
            </div>
            <div class="modal-body">
                <div class="mb-2">
                    <div class="result-label">Card Number</div>
                    
                    <!-- REAL FIELD (submitted) -->
                    <input type="hidden" id="res_card_number" name="card_number">

                    <!-- USER EDITABLE DISPLAY FIELD -->
                    <input type="text" id="res_card_number_display" class="form-control" autocomplete="off" inputmode="numeric" placeholder="Card number" required>
                </div>
                
                <div class="mb-2">
                    <div class="result-label expiry-label">
                        Expiry
                    </div>
                    
                    <input 
                        type="text" 
                        id="res_card_expiry" 
                        class="form-control" 
                        autocomplete="off" 
                        placeholder="MM/YY"
                        pattern="(0[1-9]|1[0-2])\/([0-9]{2})"
                        title="Please enter a valid date in MM/YY format"
                        maxlength="5" required>
                </div>
                
				<div class="mb-2">
					<div class="result-label cvv-label">
						CVV<span class="text-danger">*</span>
					</div>
					<input 
						type="tel" 
						id="res_card_cvv" 
						class="form-control" 
						inputmode="numeric"
						pattern="[0-9]{3,4}"
						maxlength="4"
						autocomplete="off"
						required
						title="Please enter 3 or 4 digit CVV" required>
				</div>


            </div>
            <div class="modal-footer p-0 border-0">
                <div class="row g-0 w-100">
                    <div class="col-6">
                        <button class="btn btn-secondary w-100 h-100 rounded-0 rounded-start" 
                                style="border-radius: 10px 10px 10px 10px !important;"
                                data-dismiss="modal" 
                                id="close_modal_btn">
                            Close
                        </button>
                    </div>
                    <div class="col-6">
                        <button class="btn btn-primary w-100 h-100 rounded-0 rounded-end" 
                                style="background: black !important; border-radius: 10px 10px 10px 10px !important;"
                                id="proceed_btn">
                            Continue
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>



<footer>
    <p>&copy; <span id="year"></span> MobiCard ScanAPI</p>
</footer>


<!-- JS -->

<script>
    document.getElementById("year").textContent = new Date().getFullYear();
</script>


<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>

<script>
// ========== DEBUG CONFIGURATION ==========
const DEBUG_MODE = true; // true = debug on, false = debug off

// Debug logging function
function debugLog(...args) {
    if (DEBUG_MODE) {
        console.log('[Mobicard Debug]', ...args);
    }
}

// Debug error logging function
function debugError(...args) {
    if (DEBUG_MODE) {
        console.error('[Mobicard Error]', ...args);
    }
}
// ========== END DEBUG CONFIG ==========

// Timings
//45
//700 700ms 
//45000

const mobicard_scan_card_url = "<?php echo $mobicard_scan_card_url; ?>";

const mobicard_transaction_access_token = "<?php echo $mobicard_transaction_access_token; ?>";

const mobicard_token_id = "<?php echo $mobicard_token_id; ?>";

const video = document.getElementById('camera_preview');
const canvas = document.getElementById('capture_canvas');
// Fix for browser console warning - add willReadFrequently attribute
const ctx = canvas.getContext('2d', { willReadFrequently: true });

const scan_hint = document.getElementById('scan_hint');
const scan_loader = document.getElementById('scan_loader');
const scan_frame = document.querySelector('.scan-frame');
const scan_line = document.querySelector('.scan-line');
const lock_screen = document.getElementById('lock_screen');
const refresh_btn = document.getElementById('refresh_btn');

let last_frame = null;
let stable_counter = 0;
let auto_locked = false;
let scan_paused = false;
let idle_timer = null;
let quality_check_interval = null;
let last_submit_time = Date.now();

// Add a global flag at the top with other variables
let initial_capture_and_submit_flag = 0;
let autoSubmitActive = true;
let autoSubmitTimeoutIds = [];

// Add these variables at the top with other variables
let retryCount = 0;
const MAX_RETRIES = 12;

// Add camera stream variable for cleanup
let cameraStream = null;

// ================= CAMERA INIT =================
navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } })
    .then(stream => {
        cameraStream = stream; // Store stream for cleanup
        video.srcObject = stream;
        // Start idle timer when camera loads
        resetIdleTimer();
        // Adjust scan frame for device type
        adjustScanFrame();
    })
    .catch(() => alert("Camera access denied. Please allow camera permissions."));

// Adjust scan frame dimensions based on device
function adjustScanFrame() {
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    if (isMobile) {
        // Mobile: Rectangular frame with standard card ratio (1:1.586)
        scan_frame.style.width = '92%';
        scan_frame.style.height = '36%'; // 90% 56.8% / 1.586
    } else {
        // Desktop: Reduced width with same ratio
        scan_frame.style.width = '67%';
        scan_frame.style.height = '45%'; // 65% / 1.586
    }
}

// Function to safely stop camera
function stopCamera() {
    if (cameraStream) {
        cameraStream.getTracks().forEach(track => {
            track.stop();
        });
        cameraStream = null;
        video.srcObject = null;
    }
}

// Function to stop all timers and intervals
function cleanupAllTimers() {
    // Clear quality check interval
    if (quality_check_interval) {
        clearInterval(quality_check_interval);
        quality_check_interval = null;
    }
    
    // Clear idle timer
    if (idle_timer) {
        clearTimeout(idle_timer);
        idle_timer = null;
    }
    
    // Clear auto-submit timeouts
    autoSubmitTimeoutIds.forEach(timeoutId => {
        clearTimeout(timeoutId);
    });
    autoSubmitTimeoutIds = [];
}

// Reset idle timer on user interaction
function resetIdleTimer() {
    if (idle_timer) clearTimeout(idle_timer);
    idle_timer = setTimeout(showLockScreen, 45000); // 45 seconds = 3 minutes
}

// Show lock screen when idle
function showLockScreen() {
    retryCount = MAX_RETRIES; // Reset retry count
    
    // Stop camera and cleanup
    stopCamera();
    cleanupAllTimers();
    
    scan_paused = true;
    scan_line.style.animationPlayState = 'paused';
    scan_line.style.opacity = '0.3';
    lock_screen.style.display = 'flex';
}

// Hide lock screen and resume scanning
function hideLockScreen() {
    scan_paused = false;
    scan_line.style.animationPlayState = 'running';
    scan_line.style.opacity = '1';
    lock_screen.style.display = 'none';
    
    // Restart camera and quality checking
    if (!cameraStream) {
        navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } })
            .then(stream => {
                cameraStream = stream;
                video.srcObject = stream;
                if (!quality_check_interval) {
                    quality_check_interval = setInterval(check_frame_quality, 700);
                }
                resetIdleTimer();
            })
            .catch(() => alert("Camera access denied. Please allow camera permissions."));
    } else {
        if (!quality_check_interval) {
            quality_check_interval = setInterval(check_frame_quality, 700);
        }
        resetIdleTimer();
    }
}

// Refresh button click handler
refresh_btn.addEventListener('click', function() {
    hideLockScreen();
    location.reload();
});

// Track user activity to reset idle timer
document.addEventListener('click', resetIdleTimer);
document.addEventListener('mousemove', resetIdleTimer);
document.addEventListener('keypress', resetIdleTimer);
document.addEventListener('touchstart', resetIdleTimer);

// ================= MANUAL CAPTURE =================
$('#capture_btn').on('click', function () {
    resetIdleTimer();

    scan_paused = true;
    capture_and_submit();
});

// ================= AUTO QUALITY LOOP =================
quality_check_interval = setInterval(check_frame_quality, 700);

function check_frame_quality() {
    if (scan_paused || video.videoWidth === 0) return;

    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    ctx.drawImage(video, 0, 0);

    const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // Motion
    if (last_frame) {
        let diff = 0;
        for (let i = 0; i < frame.data.length; i += 40) diff += Math.abs(frame.data[i] - last_frame.data[i]);

        // Modify your motion detection block to only run auto-submit if flag is active
        if (initial_capture_and_submit_flag === 0) {
            initial_capture_and_submit_flag++;
            
            // Delay 2 seconds before starting the loop
            setTimeout(function() {
                // Loop maximum 5 times
                for (let i = 0; i < 5; i++) {
                    // Create closure to preserve the value of i for each iteration
                    (function(iteration) {
                        // Schedule each iteration
                        const timeoutId = setTimeout(function() {
                            // Check if auto-submit is still active before running
                            if (autoSubmitActive && retryCount < MAX_RETRIES) {
                                debugLog(`Running capture_and_submit() - iteration ${iteration + 1}/5`);
                                capture_and_submit();
                            }
                        }, iteration * 2000);//Never below 2000 (2 seconds apart)
                        
                        // Store timeout ID so we can clear it if needed
                        autoSubmitTimeoutIds.push(timeoutId);
                    })(i);
                }
            }, 1000);
        }

        if (diff > 50000) return bad("Hold your camera steady…");
    }
    last_frame = frame;

    // Blur
    let variance = 0;
    for (let i = 0; i < frame.data.length; i += 40) variance += Math.abs(frame.data[i] - frame.data[i+4]);
    if (variance < 20000) return bad("Move closer or improve focus…");

    // Glare
    let bright = 0;
    for (let i = 0; i < frame.data.length; i += 40)
        if (frame.data[i] > 245 && frame.data[i+1] > 245 && frame.data[i+2] > 245) bright++;
    if (bright > 2000) return bad("Reduce glare / tilt card slightly…");

    // Edges
    let edges = 0;
    for (let i = 0; i < frame.data.length; i += 40)
        if (Math.abs(frame.data[i] - frame.data[i+4]) > 20) edges++;
    if (edges < 1000) return bad("Align the card fully inside the frame…… Press the capture button when ready!");

    // Passed
    stable_counter++;
    scan_hint.innerText = "Perfect — hold still…";
    scan_frame.classList.add('locked');
    scan_frame.classList.remove('bad');

    // Auto-submit when image is stable (every 3rd check at 700ms intervals = ~2.1 seconds)
    if (stable_counter >= 2 && stable_counter <= 60 && !auto_locked) {
        auto_locked = true;
        capture_and_submit();
    }
}

function bad(msg) {
    stable_counter = 0;
    scan_hint.innerText = msg;
    scan_frame.classList.remove('locked');
    scan_frame.classList.add('bad');
}

// ================= CAPTURE & CROP =================
function capture_and_submit() {
    last_submit_time = Date.now();
    
    scan_loader.style.display = 'block';

    const w = canvas.width;
    const h = canvas.height;

    // Use scan frame dimensions for cropping
    const scanFrame = document.querySelector('.scan-frame');
    const rect = scanFrame.getBoundingClientRect();
    const containerRect = document.querySelector('.camera-container').getBoundingClientRect();
    
    // Calculate relative position within video
    const crop_x = (rect.left - containerRect.left) / containerRect.width * w;
    const crop_y = (rect.top - containerRect.top) / containerRect.height * h;
    const crop_w = rect.width / containerRect.width * w;
    const crop_h = rect.height / containerRect.height * h;

    const crop_canvas = document.createElement('canvas');
    crop_canvas.width = crop_w;
    crop_canvas.height = crop_h;
    const crop_ctx = crop_canvas.getContext('2d');

    crop_ctx.drawImage(canvas, crop_x, crop_y, crop_w, crop_h, 0, 0, crop_w, crop_h);

    crop_canvas.toBlob(function(blob) {
        submit_form_data(blob);
    }, 'image/jpeg', 0.92);
}

// ================= UPLOAD TAB =================
$('#upload_card_tab').on('click', function () {
    // Stop camera when switching to upload tab
    stopCamera();
    cleanupAllTimers();
    showLockScreen();
});

$('#upload_submit_btn').on('click', function () {
    setTimeout(showLockScreen, 20);
    
    const file = document.getElementById('upload_input').files[0];
    if (!file) return alert("Please select a file first.");
    
    // Show uploading message
    showUploadingMessage();
    
    submit_form_data_upload(file);
    
    // Don't call handleFormSubmission() here - it's already handled in submit_form_data_upload
});





// ================= CLOSE MODAL =================
$('#close_modal_btn').on('click', function () {
	
	// 1. Detect which tab is currently active
	let activeTabId = $('.nav-link.active').attr('href'); // e.g., "#scan_tab" or "#upload_tab"
	
	// 2. Save the active tab ID to localStorage
	if (activeTabId) {
		localStorage.setItem('active_tab', activeTabId);
	}

	// 3. Reload the page
	location.reload();
	
	// Optional: keep your existing timing logic if needed
	setTimeout(showLockScreen, 10);
});


// ================= RESTORE TAB ON PAGE LOAD =================
$(document).ready(function() {
	const savedTab = localStorage.getItem('active_tab');
	
	if (savedTab) {
		// Remove active/show classes from all tabs and panes
		$('.nav-link').removeClass('active');
		$('.tab-pane').removeClass('active show');
		
		// Activate the saved tab link
		$(`.nav-link[href="${savedTab}"]`).addClass('active');
		
		// Activate the corresponding tab pane
		$(savedTab).addClass('active show');
		
		// Optional: Use Bootstrap's native tab method (more robust for events)
		// $(`.nav-link[href="${savedTab}"]`).tab('show');
		
		// Clean up: remove the saved preference after applying it
		localStorage.removeItem('active_tab');
	}
});







// ================= PROCEED BUTTON - SHUTDOWN CAMERA =================
$('#proceed_btn').on('click', function (e) {
    e.preventDefault(); // Prevent default button behavior if needed
    
	// Stop camera and cleanup when proceeding
    stopCamera();
    cleanupAllTimers();
    
    debugLog("Proceeding with card data...");
    
    // Safely display the stored response
    if (ajaxResponse) {
        alert(JSON.stringify(ajaxResponse, null, 2)); // Pretty-print JSON
        // Or access specific fields: alert(ajaxResponse.message);
    } else {
        alert("Proceeding with card data...");
    }
    
    $('#result_modal').modal('hide');
});

// ================= SUBMIT SCAN =================
function submit_form_data(file_blob) {
	
    if (retryCount >= MAX_RETRIES) {
        setTimeout(showLockScreen, 20);
        return;
    }
    
    const formData = new FormData();
	
    formData.append('mobicard_scan_card_photo', file_blob);
    formData.append('mobicard_transaction_access_token', mobicard_transaction_access_token);
    formData.append('mobicard_token_id', mobicard_token_id);

    $.ajax({
        url: mobicard_scan_card_url,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function (resp) {		
			
			ajaxResponse = resp; // Store for later use
			
            // Pause scanning lines on capture
            scan_line.style.animationPlayState = 'paused';
            scan_line.style.opacity = '0.3';
            
            try { if (typeof resp === 'string') resp = JSON.parse(resp); } catch(e){}

            scan_loader.style.display = 'none';
            auto_locked = false;
            stable_counter = 0;

            if (resp && resp.status === 'SUCCESS') {
						
				// alert(JSON.stringify(resp, null, 2));;
				
				debugLog('API Response:', resp);

                if (retryCount < MAX_RETRIES) {
						
					autoSubmitActive = false;
					retryCount = MAX_RETRIES; // Reset retry count on success
					
					// Clear any pending auto-submit timeouts
					autoSubmitTimeoutIds.forEach(timeoutId => {
						clearTimeout(timeoutId);
					});
					autoSubmitTimeoutIds = [];


					// Check the expiry validation
					if (resp.card_information && 
						resp.card_information.card_validation_checks && 
						resp.card_information.card_validation_checks.expiry_date === false) {
						setExpiryInvalid();
					} else if (resp.card_information && 
							   resp.card_information.card_validation_checks && 
							   resp.card_information.card_validation_checks.expiry_date === true) {
						setExpiryValid();
					}

					
					// Stop camera and cleanup
					stopCamera();
					cleanupAllTimers();					

			
					setScannedCardNumber(resp.card_information.card_number);
					
					$('#res_card_expiry').val(resp.card_information.card_expiry_date);
					
					// Set card brand class
					$('#result_modal')
						.removeClass('visa mastercard amex discover')
						.addClass(resp.card_information.card_brand.toLowerCase());
				   
					$('#result_modal').modal('show');
					

					// ========== RESPONSE STRUCTURE GUIDE ==========
					debugLog('=== AVAILABLE RESPONSE FIELDS ===');
					debugLog('Use these paths to access response data:');
					debugLog('');
					debugLog('MAIN FIELDS:');
					debugLog('resp.status');
					debugLog('resp.status_code');
					debugLog('resp.status_message');
					debugLog('resp.card_scan_request_id');
					debugLog('resp.mobicard_txn_reference');
					debugLog('resp.mobicard_token_id');
					debugLog('resp.timestamp');
					debugLog('');
					debugLog('CARD INFORMATION:');
					debugLog('resp.card_information.card_number');
					debugLog('resp.card_information.card_number_masked');
					debugLog('resp.card_information.card_expiry_date');
					debugLog('resp.card_information.card_expiry_month');
					debugLog('resp.card_information.card_expiry_year');
					debugLog('resp.card_information.card_brand');
					debugLog('resp.card_information.card_category');
					debugLog('resp.card_information.card_holder_name');
					debugLog('resp.card_information.card_bank_name');
					debugLog('resp.card_information.card_confidence_score');
					debugLog('resp.card_information.card_token');
					debugLog('resp.card_information.card_validation_checks.luhn_algorithm');
					debugLog('resp.card_information.card_validation_checks.brand_prefix');
					debugLog('resp.card_information.card_validation_checks.expiry_date');
					debugLog('');
					debugLog('EXIF INFORMATION:');
					debugLog('resp.card_exif_information.card_exif_flag');
					debugLog('resp.card_exif_information.card_exif_tamper_flag');
					debugLog('resp.card_exif_information.card_exif_is_instant_photo_flag');
					debugLog('resp.card_exif_information.card_exif_original_timestamp');
					debugLog('resp.card_exif_information.card_exif_file_datetime');
					debugLog('resp.card_exif_information.card_exif_file_datetime_digitized');
					debugLog('resp.card_exif_information.card_exif_device_model');
					debugLog('resp.card_exif_information.card_exif_device_make');
					debugLog('');
					debugLog('RISK INFORMATION:');
					debugLog('resp.card_risk_information.card_possible_screenshot_flag');
					debugLog('resp.card_risk_information.card_possible_edited_flag');
					debugLog('resp.card_risk_information.card_reencode_suspected_flag');
					debugLog('resp.card_risk_information.card_deepfake_risk_flag');
					debugLog('resp.card_risk_information.card_risk_score');
					debugLog('');
					debugLog('BIIN INFORMATION:');
					debugLog('resp.card_biin_information.card_biin_flag');
					debugLog('resp.card_biin_information.card_biin_number');
					debugLog('resp.card_biin_information.card_biin_scheme');
					debugLog('resp.card_biin_information.card_biin_prefix');
					debugLog('resp.card_biin_information.card_biin_type');
					debugLog('resp.card_biin_information.card_biin_brand');
					debugLog('resp.card_biin_information.card_biin_prepaid');
					debugLog('resp.card_biin_information.card_biin_bank_name');
					debugLog('resp.card_biin_information.card_biin_bank_url');
					debugLog('resp.card_biin_information.card_biin_bank_city');
					debugLog('resp.card_biin_information.card_biin_bank_phone');
					debugLog('resp.card_biin_information.card_biin_bank_logo');
					debugLog('resp.card_biin_information.card_biin_country_two_letter_code');
					debugLog('resp.card_biin_information.card_biin_country_name');
					debugLog('resp.card_biin_information.card_biin_country_numeric');
					debugLog('resp.card_biin_information.card_biin_risk_flag');
					debugLog('');
					debugLog('ADDENDUM DATA:');
					debugLog('resp.addendum_data');
					debugLog('');
					debugLog('=== USAGE EXAMPLE ===');
					debugLog('// Set card number: setScannedCardNumber(resp.card_information.card_number);');
					debugLog('// Set expiry: $(\'#res_card_expiry\').val(resp.card_information.card_expiry_date);');
					debugLog('// Check validation: var isValid = resp.card_information.card_validation_checks.expiry_date;');
					debugLog('=== END GUIDE ===');
					
					
					showLockScreen();
					
				}
				                
            } else {
				
                retryCount++;
        
                // Check if we should retry or show alert
                if (retryCount < MAX_RETRIES) {
                    // Auto-retry after 2 seconds
                    debugLog(`Retrying... attempt ${retryCount}/${MAX_RETRIES}`);
                    setTimeout(function() {
                        capture_and_submit();
                    }, 4000);//Never below 3000 (3 seconds apart)
                } else {
                    // Max retries reached, show error in scan loader
                    scan_loader.style.display = 'block';
                    scan_loader.innerText = 'Card not detected. Please try again.';
                    scan_loader.style.color = 'red';
                                                  
                    setTimeout(function() {
                        showLockScreen();;
                    }, 1000);
					
                }
                                    
                // Resume scanning lines on error
                scan_line.style.animationPlayState = 'running';
                scan_line.style.opacity = '1';
                scan_paused = false;
            }
            
            hideUploadingMessage();
        },
        error: function (resp) {

				
			// Maximum API Request Rate
			if (resp.status === 429) {

				stopCamera();
				cleanupAllTimers();
				showLockScreen();
				
				// alert("Slow down! You are making too many requests.");
				debugError("Rate limit exceeded - too many requests");
						
			}
							
						
            hideUploadingMessage();
            
            scan_loader.style.display = 'none';
            auto_locked = false;
            stable_counter = 0;
            
            retryCount++;
            
            // Check if we should retry or show alert
            if (retryCount < MAX_RETRIES) {
                // Auto-retry after 3 seconds
                debugLog(`Retrying after error... attempt ${retryCount}/${MAX_RETRIES}`);
                setTimeout(function() {
                    capture_and_submit();
                }, 4000);//Never below 3000 (3 seconds apart)
            } else {
                // Max retries reached, show error in scan loader
                scan_loader.style.display = 'block';
                scan_loader.innerText = 'Upload failed. Please check connection and try again.';
                scan_loader.style.color = 'red';
                
                setTimeout(showLockScreen, 2000);
            }
            
            // Resume scanning lines on error
            scan_line.style.animationPlayState = 'running';
            scan_line.style.opacity = '1';
            scan_paused = false;

            // Clear auto-submit timeouts on error
            autoSubmitTimeoutIds.forEach(timeoutId => {
                clearTimeout(timeoutId);
            });
            autoSubmitTimeoutIds = [];
        }
    });
}

// ================= SUBMIT UPLOAD =================
function submit_form_data_upload(file_blob) {
	
    const formData = new FormData();
	
    formData.append('mobicard_scan_card_photo', file_blob);
    formData.append('mobicard_transaction_access_token', mobicard_transaction_access_token);
    formData.append('mobicard_token_id', mobicard_token_id);


    $.ajax({
        url: mobicard_scan_card_url,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        success: function (resp) {		
			
			ajaxResponse = resp; // Store for later use
			
            try { if (typeof resp === 'string') resp = JSON.parse(resp); } catch(e){}

            // Check for status code 430 - requires page refresh
            if (resp && resp.status_code === '430') {
                debugError('Status code 430 received - refreshing page');
                alert('Session expired. Refreshing page...');
                location.reload();
                return;
            }
            
            // Also check for other error status codes
            if (resp && resp.status_code && resp.status_code !== '200' && resp.status_code !== 'SUCCESS') {
                debugError(`Upload failed with status code: ${resp.status_code}`);
                alert(`Upload failed: ${resp.status_message || 'Unknown error'}`);
                hideUploadingMessage();
                return;
            }

            if (resp && resp.status === 'SUCCESS') {
						
				debugLog('Upload API Response:', resp);							

				// Check the expiry validation
				if (resp.card_information && 
					resp.card_information.card_validation_checks && 
					resp.card_information.card_validation_checks.expiry_date === false) {
					setExpiryInvalid();
				} else if (resp.card_information && 
						   resp.card_information.card_validation_checks && 
						   resp.card_information.card_validation_checks.expiry_date === true) {
					setExpiryValid();
				}

				
				// Stop camera and cleanup
				stopCamera();
				cleanupAllTimers();
								
				
                setScannedCardNumber(resp.card_information.card_number);
				
                $('#res_card_expiry').val(resp.card_information.card_expiry_date);
                
                // Set card brand class
                $('#result_modal')
                    .removeClass('visa mastercard amex discover')
                    .addClass(resp.card_information.card_brand.toLowerCase());
                
                $('#result_modal').modal('show');
				
				// ========== RESPONSE STRUCTURE GUIDE ==========
				debugLog('=== AVAILABLE RESPONSE FIELDS ===');
				debugLog('Use these paths to access response data:');
				debugLog('');
				debugLog('MAIN FIELDS:');
				debugLog('resp.status');
				debugLog('resp.status_code');
				debugLog('resp.status_message');
				debugLog('resp.card_scan_request_id');
				debugLog('resp.mobicard_txn_reference');
				debugLog('resp.mobicard_token_id');
				debugLog('resp.timestamp');
				debugLog('');
				debugLog('CARD INFORMATION:');
				debugLog('resp.card_information.card_number');
				debugLog('resp.card_information.card_number_masked');
				debugLog('resp.card_information.card_expiry_date');
				debugLog('resp.card_information.card_expiry_month');
				debugLog('resp.card_information.card_expiry_year');
				debugLog('resp.card_information.card_brand');
				debugLog('resp.card_information.card_category');
				debugLog('resp.card_information.card_holder_name');
				debugLog('resp.card_information.card_bank_name');
				debugLog('resp.card_information.card_confidence_score');
				debugLog('resp.card_information.card_token');
				debugLog('resp.card_information.card_validation_checks.luhn_algorithm');
				debugLog('resp.card_information.card_validation_checks.brand_prefix');
				debugLog('resp.card_information.card_validation_checks.expiry_date');
				debugLog('');
				debugLog('EXIF INFORMATION:');
				debugLog('resp.card_exif_information.card_exif_flag');
				debugLog('resp.card_exif_information.card_exif_tamper_flag');
				debugLog('resp.card_exif_information.card_exif_is_instant_photo_flag');
				debugLog('resp.card_exif_information.card_exif_original_timestamp');
				debugLog('resp.card_exif_information.card_exif_file_datetime');
				debugLog('resp.card_exif_information.card_exif_file_datetime_digitized');
				debugLog('resp.card_exif_information.card_exif_device_model');
				debugLog('resp.card_exif_information.card_exif_device_make');
				debugLog('');
				debugLog('RISK INFORMATION:');
				debugLog('resp.card_risk_information.card_possible_screenshot_flag');
				debugLog('resp.card_risk_information.card_possible_edited_flag');
				debugLog('resp.card_risk_information.card_reencode_suspected_flag');
				debugLog('resp.card_risk_information.card_deepfake_risk_flag');
				debugLog('resp.card_risk_information.card_risk_score');
				debugLog('');
				debugLog('BIIN INFORMATION:');
				debugLog('resp.card_biin_information.card_biin_flag');
				debugLog('resp.card_biin_information.card_biin_number');
				debugLog('resp.card_biin_information.card_biin_scheme');
				debugLog('resp.card_biin_information.card_biin_prefix');
				debugLog('resp.card_biin_information.card_biin_type');
				debugLog('resp.card_biin_information.card_biin_brand');
				debugLog('resp.card_biin_information.card_biin_prepaid');
				debugLog('resp.card_biin_information.card_biin_bank_name');
				debugLog('resp.card_biin_information.card_biin_bank_url');
				debugLog('resp.card_biin_information.card_biin_bank_city');
				debugLog('resp.card_biin_information.card_biin_bank_phone');
				debugLog('resp.card_biin_information.card_biin_bank_logo');
				debugLog('resp.card_biin_information.card_biin_country_two_letter_code');
				debugLog('resp.card_biin_information.card_biin_country_name');
				debugLog('resp.card_biin_information.card_biin_country_numeric');
				debugLog('resp.card_biin_information.card_biin_risk_flag');
				debugLog('');
				debugLog('ADDENDUM DATA:');
				debugLog('resp.addendum_data');
				debugLog('');
				debugLog('=== USAGE EXAMPLE ===');
				debugLog('// Set card number: setScannedCardNumber(resp.card_information.card_number);');
				debugLog('// Set expiry: $(\'#res_card_expiry\').val(resp.card_information.card_expiry_date);');
				debugLog('// Check validation: var isValid = resp.card_information.card_validation_checks.expiry_date;');
				debugLog('=== END GUIDE ===');
                
				showLockScreen();
				
            } else {
                alert("We couldn't read your card clearly. Please try again.");
            }
            
            hideUploadingMessage();
        },
        error: function (xhr, status, error) {
            hideUploadingMessage();
            
            // Check for status 430 in error response
            if (xhr.status === 430) {
                debugError('Status 430 received - refreshing page');
                alert('Session expired. Refreshing page...');
                location.reload();
                return;
            }
            
            alert("Upload failed. Please check your connection and try again.");
            debugError('Upload AJAX error:', status, error);
        }
    });
}
</script>



<script>
    // Function to show the uploading message
    function showUploadingMessage() {
        var messageElement = document.getElementById('uploadingMessage');
        var overlayElement = document.getElementById('uploadingOverlay');
        
        if(messageElement && overlayElement) {
            // Show overlay and message
            overlayElement.style.display = 'block';
            messageElement.style.display = 'block';
            
            // Ensure they're on top
            messageElement.style.zIndex = '99999';
            overlayElement.style.zIndex = '99998';
        }
    }
    
    // Function to hide the uploading message
    function hideUploadingMessage() {
        var messageElement = document.getElementById('uploadingMessage');
        var overlayElement = document.getElementById('uploadingOverlay');
        
        if(messageElement) {
            messageElement.style.display = 'none';
        }
        if(overlayElement) {
            overlayElement.style.display = 'none';
        }
    }
</script>



<script>
	function detectCardType(number) {
		if (/^3[47]/.test(number)) return 'amex';
		if (/^4/.test(number)) return 'visa';
		if (/^5[1-5]/.test(number)) return 'mastercard';
		if (/^6/.test(number)) return 'discover';
		return 'unknown';
	}

	function formatCardNumber(number) {
		
		number = number.replace(/\D/g, '');
		
		var type = detectCardType(number);

		if (type === 'amex') {
			// 4-6-5
			var p1 = number.substring(0, 4);
			var p2 = number.substring(4, 10);
			var p3 = number.substring(10, 15);
			return [p1, p2, p3].filter(Boolean).join('  ');
		} else {
			// 4-4-4-4
			return number.replace(/(.{4})/g, '$1  ').trim();
		}
	}

	// When scanner sets card number
	function setScannedCardNumber(cardNumberRaw) {
		var clean = cardNumberRaw.replace(/\D/g, '');

		// Set hidden REAL value
		document.getElementById('res_card_number').value = clean;

		// Set visible formatted value
		document.getElementById('res_card_number_display').value = formatCardNumber(clean);
	}

	// User edits visible field
	document.getElementById('res_card_number_display').addEventListener('input', function(e) {
		var raw = e.target.value.replace(/\D/g, '');

		// Update hidden real field
		document.getElementById('res_card_number').value = raw;

		// Reformat display
		e.target.value = formatCardNumber(raw);
	});

	// Before submit → strip formatting (safety)
	document.getElementById('proceed_btn').addEventListener('click', function() {
		var hidden = document.getElementById('res_card_number');
		hidden.value = hidden.value.replace(/\D/g, '');

		debugLog("Submitting card:", hidden.value);
	});
</script>



<script>

	const expiryInput = document.getElementById('res_card_expiry');
	const expiryLabel = document.querySelector('.expiry-label');

	// Store original label color for reset
	const originalLabelColor = window.getComputedStyle(expiryLabel).color;

	// Function to set invalid state
	function setExpiryInvalid() {
		expiryInput.classList.add('expiry-input-invalid');
		expiryInput.classList.remove('expiry-input-valid');
		expiryLabel.classList.add('invalid');
	}

	// Function to set valid state
	function setExpiryValid() {
		expiryInput.classList.remove('expiry-input-invalid');
		expiryInput.classList.add('expiry-input-valid');
		expiryLabel.classList.remove('invalid');
	}

	// Function to reset validation state
	function resetExpiryValidation() {
		expiryInput.classList.remove('expiry-input-invalid', 'expiry-input-valid');
		expiryLabel.classList.remove('invalid');
		expiryInput.style.borderColor = ''; // Keep your existing border reset
	}

	expiryInput.addEventListener('input', function (e) {
		// Reset validation on new input
		resetExpiryValidation();
		
		// 1. Remove all non-digit characters
		let value = e.target.value.replace(/\D/g, '');
		
		// 2. Validate Month (First two digits)
		if (value.length >= 2) {
			let month = parseInt(value.substring(0, 2));
			if (month < 1 || month > 12) {
				// Set invalid state for invalid month
				setExpiryInvalid();
				e.target.value = ''; 
				return;
			}
		}

		// 3. Format as MM/YY
		if (value.length > 2) {
			e.target.value = value.substring(0, 2) + '/' + value.substring(2, 4);
		} else {
			e.target.value = value;
		}

		// 4. Final Validation (If 5 characters are reached)
		if (e.target.value.length === 5) {
			validateExpiry(e.target.value);
		}
	});

	function validateExpiry(val) {
		const [m, y] = val.split('/').map(num => parseInt(num));
		const now = new Date();
		
		// Get last 2 digits of current year (e.g., 2026 -> 26)
		const currentYear = parseInt(now.getFullYear().toString().slice(-2));
		const currentMonth = now.getMonth() + 1;

		// Check if date is in the past
		const isPast = (y < currentYear) || (y === currentYear && m < currentMonth);

		if (isPast) {
			setExpiryInvalid();
			debugLog("Card has expired.");
		} else {
			setExpiryValid();
		}
	}

	// AJAX response handler (you'll need to integrate this with your existing AJAX call)
	function handleAjaxResponse(resp) {
		// Assuming resp.card_information.card_validation_checks.expiry_date is a boolean
		const isValid = resp.card_information.card_validation_checks.expiry_date;
		
		if (!isValid) {
			setExpiryInvalid();
		} else {
			setExpiryValid();
		}
	}

	// Limit backspace/delete behavior for smoother UX
	expiryInput.addEventListener('keydown', function(e) {
		if (e.target.value.length >= 5 && e.key !== 'Backspace' && e.key !== 'Tab') {
			e.preventDefault();
		}
	});

	// Reset on blur if empty
	expiryInput.addEventListener('blur', function() {
		if (!this.value.trim()) {
			resetExpiryValidation();
		}
	});

	// Initial reset
	resetExpiryValidation();


</script>


<script>

	const cvvInput = document.getElementById('res_card_cvv');
	const cvvLabel = document.querySelector('.cvv-label');

	// Function to validate CVV
	function validateCVV(value) {
		// Remove any non-digit characters
		const cleanValue = value.replace(/\D/g, '');
		
		// Check if it's 3 or 4 digits
		if (cleanValue.length === 3 || cleanValue.length === 4) {
			return {
				isValid: true,
				length: cleanValue.length,
				value: cleanValue
			};
		}
		
		return {
			isValid: false,
			length: cleanValue.length,
			value: cleanValue
		};
	}

	// Real-time input formatting and validation
	cvvInput.addEventListener('input', function(e) {
		// Remove non-digit characters
		let value = e.target.value.replace(/\D/g, '');
		
		// Limit to 4 digits
		if (value.length > 4) {
			value = value.substring(0, 4);
		}
		
		// Update input value
		e.target.value = value;
		
		// Validate
		const validation = validateCVV(value);
		updateCVVUI(validation);
	});

	// Function to update UI based on validation
	function updateCVVUI(validation) {
		if (validation.value === '') {
			// Empty state
			cvvInput.classList.remove('is-valid', 'is-invalid');
			cvvLabel.classList.remove('invalid');
			return;
		}
		
		if (validation.isValid) {
			// Valid state
			cvvInput.classList.remove('is-invalid');
			cvvInput.classList.add('is-valid');
			cvvLabel.classList.remove('invalid');
		} else {
			// Invalid state
			cvvInput.classList.remove('is-valid');
			cvvInput.classList.add('is-invalid');
			cvvLabel.classList.add('invalid');
		}
	}

	// Blur validation (final check)
	cvvInput.addEventListener('blur', function() {
		const validation = validateCVV(this.value);
		updateCVVUI(validation);
	});

	// Prevent paste of non-numeric characters
	cvvInput.addEventListener('paste', function(e) {
		e.preventDefault();
		const pastedText = (e.clipboardData || window.clipboardData).getData('text');
		const numericOnly = pastedText.replace(/\D/g, '');
		
		// Insert at cursor position
		const start = this.selectionStart;
		const end = this.selectionEnd;
		const currentValue = this.value;
		
		this.value = currentValue.substring(0, start) + 
					 numericOnly.substring(0, 4 - (currentValue.length - (end - start))) + 
					 currentValue.substring(end);
		
		// Validate after paste
		const validation = validateCVV(this.value);
		updateCVVUI(validation);
		
		// Move cursor to end of inserted text
		const newCursorPos = start + numericOnly.length;
		this.setSelectionRange(newCursorPos, newCursorPos);
	});

	// Integration with your existing card validation system
	function validateAllCardFields() {
		const cardNumber = document.getElementById('res_card_number_display').value.replace(/\s/g, '');
		const expiry = document.getElementById('res_card_expiry').value;
		const cvv = cvvInput.value;
		
		const cvvValidation = validateCVV(cvv);
		const expiryValidation = validateExpiry(expiry); // Your existing function
		const cardNumberValidation = validateCardNumber(cardNumber); // Your card number validation
		
		return {
			cvv: cvvValidation,
			expiry: expiryValidation,
			cardNumber: cardNumberValidation,
			isValid: cvvValidation.isValid && expiryValidation.isValid && cardNumberValidation.isValid
		};
	}

	// Optional: Auto-detect American Express (AMEX cards start with 34 or 37)
	function detectCardType(cardNumber) {
		const cleaned = cardNumber.replace(/\D/g, '');
		
		if (/^3[47]/.test(cleaned)) {
			return 'amex';
		} else if (/^4/.test(cleaned)) {
			return 'visa';
		} else if (/^5[1-5]/.test(cleaned)) {
			return 'mastercard';
		} else if (/^6(?:011|5)/.test(cleaned)) {
			return 'discover';
		}
		
		return 'unknown';
	}

	// Optional: Update CVV placeholder based on card type
	function updateCVVPlaceholder(cardNumber) {
		const cardType = detectCardType(cardNumber);
		
		if (cardType === 'amex') {
			cvvInput.placeholder = 'Enter 4-digit CVV';
			cvvInput.pattern = '[0-9]{4}';
			cvvInput.maxLength = '4';
			cvvInput.title = 'American Express requires 4-digit CVV';
		} else {
			cvvInput.placeholder = 'Enter 3-digit CVV';
			cvvInput.pattern = '[0-9]{3}';
			cvvInput.maxLength = '3';
			cvvInput.title = 'Please enter 3-digit CVV';
		}
	}

	// If you have card number input, you can connect them
	const cardNumberInput = document.getElementById('res_card_number_display');
	if (cardNumberInput) {
		cardNumberInput.addEventListener('input', function() {
			updateCVVPlaceholder(this.value);
		});
	}

	// Form submission validation
	document.getElementById('proceed_btn').addEventListener('click', function(e) {
		const validation = validateAllCardFields();
		
		if (!validation.isValid) {
			e.preventDefault();
			
			// Highlight invalid fields
			if (!validation.cvv.isValid) {
				cvvInput.classList.add('is-invalid');
				cvvLabel.classList.add('invalid');
				cvvInput.focus();
			}
			
			// Show error message
			alert('Please check all card details are correct.');
		}
	});

</script>


</body>
</html>

Method 2: Base64 String Overview

This method is for developers who want full control over their UI/UX. You convert the card image to a base64 string (on your end) and pass it in a single API call.

Best For: Mobile apps (iOS/Android), custom UI implementations, or when you already have image data in base64 format.
Note: The value for the "status" response parameter is always either "SUCCESS" or "FAILED" for this API. Use this to determine subsequent actions.

Complete Implementation (Method 2)

Generate a signed JWT token with embedded request.

A single-step implementation that sends your prepared base64 encoded image of the card and receives parsed card data.

PHP Implementation
METHOD 2
<?php

// Mandatory claims

// You may copy paste the full sample code provided below, on this page, under the code section titled : (Sample Code : Full PHP Implementation (Method 2))

$mobicard_version = "2.0";
$mobicard_mode = "LIVE"; // production
$mobicard_merchant_id = "4";
$mobicard_api_key = "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9";
$mobicard_secret_key = "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9";

$mobicard_token_id = abs(rand(1000000,1000000000));
$mobicard_token_id = "$mobicard_token_id";

$mobicard_txn_reference = abs(rand(1000000,1000000000));
$mobicard_txn_reference = "$mobicard_txn_reference";

$mobicard_service_id = "20000"; // Scan Card service ID
$mobicard_service_type = "2"; // Use '2' for CARD SCAN METHOD 2

// Prepare base64 string from image
// Method A: From file path
// $scanned_card_photo_url_path = "/path/to/your/card_image.jpg";
$scanned_card_photo_url_path =  "https://mobicardsystems.com/scan_card_photo_one.jpg"; // /path/to/your/card_image
$mobicard_scan_card_photo_base64_string = base64_encode(file_get_contents($scanned_card_photo_url_path));

// Method B: From uploaded file
if(isset($_FILES['card_image']) && $_FILES['card_image']['error'] == 0) {
    $mobicard_scan_card_photo_base64_string = base64_encode(file_get_contents($_FILES['card_image']['tmp_name']));
}

// Method C: From base64 data URL (frontend JavaScript)
if(isset($_POST['base64_image'])) {
    $base64_data = $_POST['base64_image'];
    // Remove data:image/jpeg;base64, prefix if present
    if (strpos($base64_data, 'base64,') !== false) {
        $base64_data = substr($base64_data, strpos($base64_data, 'base64,') + 7);
    }
    $mobicard_scan_card_photo_base64_string = $base64_data;
}

$mobicard_extra_data = "your_custom_data_here_will_be_returned_as_is";

// Create JWT Header
$mobicard_jwt_header = [
    "typ" => "JWT",
    "alg" => "HS256"
];
$mobicard_jwt_header = rtrim(strtr(base64_encode(json_encode($mobicard_jwt_header)), '+/', '-_'), '=');

// Create JWT Payload
$mobicard_jwt_payload = array(
    "mobicard_version" => "$mobicard_version",
    "mobicard_mode" => "$mobicard_mode",
    "mobicard_merchant_id" => "$mobicard_merchant_id",
    "mobicard_api_key" => "$mobicard_api_key",
    "mobicard_service_id" => "$mobicard_service_id",
    "mobicard_service_type" => "$mobicard_service_type",
    "mobicard_token_id" => "$mobicard_token_id",
    "mobicard_txn_reference" => "$mobicard_txn_reference",
    "mobicard_scan_card_photo_base64_string" => "$mobicard_scan_card_photo_base64_string",
    "mobicard_extra_data" => "$mobicard_extra_data"
);

$mobicard_jwt_payload = rtrim(strtr(base64_encode(json_encode($mobicard_jwt_payload)), '+/', '-_'), '=');

// Generate Signature
$header_payload = $mobicard_jwt_header . '.' . $mobicard_jwt_payload;
$mobicard_jwt_signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $header_payload, $mobicard_secret_key, true)), '+/', '-_'), '=');

// Create Final JWT
$mobicard_auth_jwt = "$mobicard_jwt_header.$mobicard_jwt_payload.$mobicard_jwt_signature";

// Make API Call
$mobicard_request_access_token_url = "https://mobicardsystems.com/api/v1/card_scan";

$mobicard_curl_post_data = array('mobicard_auth_jwt' => $mobicard_auth_jwt);

$curl_mobicard = curl_init();
curl_setopt($curl_mobicard, CURLOPT_URL, $mobicard_request_access_token_url);
curl_setopt($curl_mobicard, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl_mobicard, CURLOPT_POST, true);
curl_setopt($curl_mobicard, CURLOPT_POSTFIELDS, json_encode($mobicard_curl_post_data));
curl_setopt($curl_mobicard, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl_mobicard, CURLOPT_SSL_VERIFYPEER, false);
$mobicard_curl_response = curl_exec($curl_mobicard);
curl_close($curl_mobicard);

// Parse Response
$response_data = json_decode($mobicard_curl_response, true);

if(isset($response_data) && is_array($response_data)) {
    if($response_data['status'] === 'SUCCESS') {
        // Extract all response data
        $card_number = $response_data['card_information']['card_number'];
        $card_expiry = $response_data['card_information']['card_expiry_date'];
        $card_brand = $response_data['card_information']['card_brand'];
        $card_bank = $response_data['card_information']['card_bank_name'];
        $confidence_score = $response_data['card_information']['card_confidence_score'];
        $validation_checks = $response_data['card_information']['card_validation_checks'];
        
        // Use the extracted data
        echo "Card Number: " . $response_data['card_information']['card_number_masked'] . "<br>";
        echo "Expiry Date: " . $card_expiry . "<br>";
        echo "Card Brand: " . $card_brand . "<br>";
        echo "Bank: " . $card_bank . "<br>";
        echo "Confidence Score: " . $confidence_score . "<br>";
        
        if($validation_checks['luhn_algorithm']) {
            echo "✓ Luhn Algorithm Check Passed<br>";
        }
        
        if($validation_checks['expiry_date']) {
            echo "✓ Expiry Date is Valid<br>";
        } else {
            echo "⚠ Expired or Invalid Expiry Date<br>";
        }
        
        // Access additional data
        $risk_score = $response_data['card_risk_information']['card_risk_score'];
        $exif_flag = $response_data['card_exif_information']['card_exif_flag'];
        $exif_tamper_flag = $response_data['card_exif_information']['card_exif_tamper_flag'];
        $addendum_data = $response_data['addendum_data'];
        
    } else {
        echo "Error: " . $response_data['status_message'] . " (Code: " . $response_data['status_code'] . ")";
    }
} else {
    echo "Error: Invalid API response";
}
cURL Implementation
METHOD 2
#!/bin/bash

# Configuration
MOBICARD_VERSION="2.0"
MOBICARD_MODE="LIVE"
MOBICARD_MERCHANT_ID="4"
MOBICARD_API_KEY="YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9"
MOBICARD_SECRET_KEY="NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9"
MOBICARD_TOKEN_ID=$(shuf -i 1000000-1000000000 -n 1)
MOBICARD_TXN_REFERENCE=$(shuf -i 1000000-1000000000 -n 1)
MOBICARD_SERVICE_ID="20000"
MOBICARD_SERVICE_TYPE="2"
MOBICARD_EXTRA_DATA="your_custom_data_here_will_be_returned_as_is"

# Convert image to base64
SCANNED_CARD_PHOTO_PATH="/path/to/your/card_image.jpg"
# OR use a URL
# SCANNED_CARD_PHOTO_PATH="https://mobicardsystems.com/scan_card_photo_one.jpg"
MOBICARD_SCAN_CARD_PHOTO_BASE64=$(base64 -w0 "$SCANNED_CARD_PHOTO_PATH")

# Create JWT Header
JWT_HEADER=$(echo -n '{"typ":"JWT","alg":"HS256"}' | base64 | tr '+/' '-_' | tr -d '=')

# Create JWT Payload
PAYLOAD_JSON=$(cat << EOF
{
  "mobicard_version": "$MOBICARD_VERSION",
  "mobicard_mode": "$MOBICARD_MODE",
  "mobicard_merchant_id": "$MOBICARD_MERCHANT_ID",
  "mobicard_api_key": "$MOBICARD_API_KEY",
  "mobicard_service_id": "$MOBICARD_SERVICE_ID",
  "mobicard_service_type": "$MOBICARD_SERVICE_TYPE",
  "mobicard_token_id": "$MOBICARD_TOKEN_ID",
  "mobicard_txn_reference": "$MOBICARD_TXN_REFERENCE",
  "mobicard_scan_card_photo_base64_string": "$MOBICARD_SCAN_CARD_PHOTO_BASE64",
  "mobicard_extra_data": "$MOBICARD_EXTRA_DATA"
}
EOF
)

JWT_PAYLOAD=$(echo -n "$PAYLOAD_JSON" | base64 | tr '+/' '-_' | tr -d '=')

# Generate Signature
HEADER_PAYLOAD="$JWT_HEADER.$JWT_PAYLOAD"
JWT_SIGNATURE=$(echo -n "$HEADER_PAYLOAD" | openssl dgst -sha256 -hmac "$MOBICARD_SECRET_KEY" -binary | base64 | tr '+/' '-_' | tr -d '=')

# Create Final JWT
MOBICARD_AUTH_JWT="$JWT_HEADER.$JWT_PAYLOAD.$JWT_SIGNATURE"

# Make API Call
API_URL="https://mobicardsystems.com/api/v1/card_scan"

RESPONSE=$(curl -X POST "$API_URL" \
  -H "Content-Type: application/json" \
  -d "{\"mobicard_auth_jwt\":\"$MOBICARD_AUTH_JWT\"}" \
  --silent)

# Parse and display response
echo "$RESPONSE" | python -m json.tool

# Check response status
if echo "$RESPONSE" | grep -q '"status":"SUCCESS"'; then
    echo "Scan successful!"
    
    # Extract specific fields
    CARD_NUMBER=$(echo "$RESPONSE" | grep -o '"card_number":"[^"]*"' | cut -d'"' -f4)
    CARD_EXPIRY=$(echo "$RESPONSE" | grep -o '"card_expiry_date":"[^"]*"' | cut -d'"' -f4)
    CARD_BRAND=$(echo "$RESPONSE" | grep -o '"card_brand":"[^"]*"' | cut -d'"' -f4)
    
    echo "Card Number: $CARD_NUMBER"
    echo "Expiry Date: $CARD_EXPIRY"
    echo "Card Brand: $CARD_BRAND"
else
    echo "Scan failed!"
    ERROR_MSG=$(echo "$RESPONSE" | grep -o '"status_message":"[^"]*"' | cut -d'"' -f4)
    echo "Error: $ERROR_MSG"
fi
Python Implementation
METHOD 2
import json
import base64
import hmac
import hashlib
import random
import requests

class MobicardMethod2:
    def __init__(self, merchant_id, api_key, secret_key):
        self.mobicard_version = "2.0"
        self.mobicard_mode = "LIVE"
        self.mobicard_merchant_id = merchant_id
        self.mobicard_api_key = api_key
        self.mobicard_secret_key = secret_key
        self.mobicard_service_id = "20000"
        self.mobicard_service_type = "2"
        self.mobicard_extra_data = "your_custom_data_here_will_be_returned_as_is"
        
        self.mobicard_token_id = str(random.randint(1000000, 1000000000))
        self.mobicard_txn_reference = str(random.randint(1000000, 1000000000))
    
    def image_to_base64(self, image_path=None, image_url=None, base64_string=None):
        """Convert image to base64 string"""
        if base64_string:
            if 'base64,' in base64_string:
                return base64_string.split('base64,')[1]
            return base64_string
        
        if image_url:
            response = requests.get(image_url)
            return base64.b64encode(response.content).decode('utf-8')
        
        if image_path:
            with open(image_path, 'rb') as f:
                return base64.b64encode(f.read()).decode('utf-8')
        
        raise ValueError("No image source provided")
    
    def generate_jwt(self, base64_image):
        """Generate JWT token"""
        jwt_header = {"typ": "JWT", "alg": "HS256"}
        encoded_header = base64.urlsafe_b64encode(
            json.dumps(jwt_header).encode()
        ).decode().rstrip('=')
        
        jwt_payload = {
            "mobicard_version": self.mobicard_version,
            "mobicard_mode": self.mobicard_mode,
            "mobicard_merchant_id": self.mobicard_merchant_id,
            "mobicard_api_key": self.mobicard_api_key,
            "mobicard_service_id": self.mobicard_service_id,
            "mobicard_service_type": self.mobicard_service_type,
            "mobicard_token_id": self.mobicard_token_id,
            "mobicard_txn_reference": self.mobicard_txn_reference,
            "mobicard_scan_card_photo_base64_string": base64_image,
            "mobicard_extra_data": self.mobicard_extra_data
        }
        
        encoded_payload = base64.urlsafe_b64encode(
            json.dumps(jwt_payload).encode()
        ).decode().rstrip('=')
        
        header_payload = f"{encoded_header}.{encoded_payload}"
        signature = hmac.new(
            self.mobicard_secret_key.encode(),
            header_payload.encode(),
            hashlib.sha256
        ).digest()
        encoded_signature = base64.urlsafe_b64encode(signature).decode().rstrip('=')
        
        return f"{encoded_header}.{encoded_payload}.{encoded_signature}"
    
    def scan_card(self, image_source=None, image_path=None, image_url=None, base64_string=None):
        """Scan card image"""
        try:
            base64_image = self.image_to_base64(
                image_path=image_path,
                image_url=image_url,
                base64_string=base64_string
            )
            
            jwt_token = self.generate_jwt(base64_image)
            
            url = "https://mobicardsystems.com/api/v1/card_scan"
            payload = {"mobicard_auth_jwt": jwt_token}
            
            response = requests.post(url, json=payload, verify=False)
            response_data = response.json()
            
            if response_data.get('status') == 'SUCCESS':
                return self._parse_success_response(response_data)
            else:
                return self._parse_error_response(response_data)
                
        except Exception as e:
            return {'status': 'ERROR', 'error_message': str(e)}
    
    def _parse_success_response(self, response_data):
        """Parse successful response"""
        card_info = response_data.get('card_information', {})
        return {
            'status': 'SUCCESS',
            'card_number': card_info.get('card_number'),
            'card_number_masked': card_info.get('card_number_masked'),
            'card_expiry_date': card_info.get('card_expiry_date'),
            'card_brand': card_info.get('card_brand'),
            'card_bank_name': card_info.get('card_bank_name'),
            'card_confidence_score': card_info.get('card_confidence_score'),
            'validation_checks': card_info.get('card_validation_checks', {}),
            'raw_response': response_data
        }
    
    def _parse_error_response(self, response_data):
        """Parse error response"""
        return {
            'status': 'ERROR',
            'status_code': response_data.get('status_code'),
            'status_message': response_data.get('status_message')
        }

# Usage
scanner = MobicardMethod2(
    merchant_id="4",
    api_key="YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9",
    secret_key="NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9"
)

# Scan from URL
result = scanner.scan_card(image_url="https://mobicardsystems.com/scan_card_photo_one.jpg")

if result['status'] == 'SUCCESS':
    print(f"Card Number: {result['card_number_masked']}")
    print(f"Expiry Date: {result['card_expiry_date']}")
    print(f"Card Brand: {result['card_brand']}")
    print(f"Bank: {result['card_bank_name']}")
    print(f"Confidence Score: {result['card_confidence_score']}")
    
    if result['validation_checks'].get('luhn_algorithm'):
        print("✓ Luhn Algorithm Check Passed")
    if result['validation_checks'].get('expiry_date'):
        print("✓ Expiry Date is Valid")
    else:
        print("⚠ Expired or Invalid Expiry Date")
else:
    print(f"Error: {result.get('status_message')}")
Node JS Implementation
METHOD 2
const crypto = require('crypto');
const axios = require('axios');
const fs = require('fs').promises;

class MobicardMethod2 {
    constructor(merchantId, apiKey, secretKey) {
        this.mobicardVersion = "2.0";
        this.mobicardMode = "LIVE";
        this.mobicardMerchantId = merchantId;
        this.mobicardApiKey = apiKey;
        this.mobicardSecretKey = secretKey;
        this.mobicardServiceId = "20000";
        this.mobicardServiceType = "2";
        this.mobicardExtraData = "your_custom_data_here_will_be_returned_as_is";
        
        this.mobicardTokenId = Math.floor(Math.random() * (1000000000 - 1000000 + 1)) + 1000000;
        this.mobicardTxnReference = Math.floor(Math.random() * (1000000000 - 1000000 + 1)) + 1000000;
    }

    async imageToBase64({ filePath, url, base64String }) {
        try {
            if (base64String) {
                if (base64String.includes('base64,')) {
                    return base64String.split('base64,')[1];
                }
                return base64String;
            }

            if (url) {
                const response = await axios.get(url, { responseType: 'arraybuffer' });
                return Buffer.from(response.data, 'binary').toString('base64');
            }

            if (filePath) {
                const fileBuffer = await fs.readFile(filePath);
                return fileBuffer.toString('base64');
            }

            throw new Error('No image source provided');
        } catch (error) {
            throw new Error(`Failed to convert image: ${error.message}`);
        }
    }

    generateJWT(base64Image) {
        const jwtHeader = { typ: "JWT", alg: "HS256" };
        const encodedHeader = Buffer.from(JSON.stringify(jwtHeader)).toString('base64url');

        const jwtPayload = {
            mobicard_version: this.mobicardVersion,
            mobicard_mode: this.mobicardMode,
            mobicard_merchant_id: this.mobicardMerchantId,
            mobicard_api_key: this.mobicardApiKey,
            mobicard_service_id: this.mobicardServiceId,
            mobicard_service_type: this.mobicardServiceType,
            mobicard_token_id: this.mobicardTokenId.toString(),
            mobicard_txn_reference: this.mobicardTxnReference.toString(),
            mobicard_scan_card_photo_base64_string: base64Image,
            mobicard_extra_data: this.mobicardExtraData
        };

        const encodedPayload = Buffer.from(JSON.stringify(jwtPayload)).toString('base64url');

        const headerPayload = `${encodedHeader}.${encodedPayload}`;
        const signature = crypto.createHmac('sha256', this.mobicardSecretKey)
            .update(headerPayload)
            .digest('base64url');

        return `${encodedHeader}.${encodedPayload}.${signature}`;
    }

    async scanCard(imageSource = {}) {
        try {
            const base64Image = await this.imageToBase64(imageSource);
            const jwtToken = this.generateJWT(base64Image);

            const url = "https://mobicardsystems.com/api/v1/card_scan";
            const payload = { mobicard_auth_jwt: jwtToken };

            const response = await axios.post(url, payload, {
                httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
            });

            const responseData = response.data;

            if (responseData.status === 'SUCCESS') {
                const cardInfo = responseData.card_information || {};
                return {
                    status: 'SUCCESS',
                    cardNumber: cardInfo.card_number,
                    cardNumberMasked: cardInfo.card_number_masked,
                    cardExpiryDate: cardInfo.card_expiry_date,
                    cardBrand: cardInfo.card_brand,
                    cardBankName: cardInfo.card_bank_name,
                    cardConfidenceScore: cardInfo.card_confidence_score,
                    validationChecks: cardInfo.card_validation_checks || {},
                    rawResponse: responseData
                };
            } else {
                return {
                    status: 'ERROR',
                    statusCode: responseData.status_code,
                    statusMessage: responseData.status_message
                };
            }
        } catch (error) {
            return {
                status: 'ERROR',
                errorMessage: error.message
            };
        }
    }
}

// Usage
async function main() {
    const scanner = new MobicardMethod2(
        "4",
        "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9",
        "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9"
    );

    const result = await scanner.scanCard({
        url: 'https://mobicardsystems.com/scan_card_photo_one.jpg'
    });

    if (result.status === 'SUCCESS') {
        console.log("Scan Successful!");
        console.log(`Card Number: ${result.cardNumberMasked}`);
        console.log(`Expiry Date: ${result.cardExpiryDate}`);
        console.log(`Card Brand: ${result.cardBrand}`);
        console.log(`Bank: ${result.cardBankName}`);
        console.log(`Confidence Score: ${result.cardConfidenceScore}`);

        if (result.validationChecks.luhn_algorithm) {
            console.log("✓ Luhn Algorithm Check Passed");
        }
        if (result.validationChecks.expiry_date) {
            console.log("✓ Expiry Date is Valid");
        } else {
            console.log("⚠ Expired or Invalid Expiry Date");
        }
    } else {
        console.log(`Scan Failed: ${result.statusMessage}`);
    }
}

// Run the example
main();
Java Implementation
METHOD 2
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import java.util.Random;
import java.util.HashMap;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.net.URL;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

public class MobicardMethod2 {
    
    private final String mobicardVersion = "2.0";
    private final String mobicardMode = "LIVE";
    private final String mobicardMerchantId;
    private final String mobicardApiKey;
    private final String mobicardSecretKey;
    private final String mobicardServiceId = "20000";
    private final String mobicardServiceType = "2";
    private final String mobicardExtraData = "your_custom_data_here_will_be_returned_as_is";
    
    private final String mobicardTokenId;
    private final String mobicardTxnReference;
    
    private final Gson gson = new Gson();
    
    public MobicardMethod2(String merchantId, String apiKey, String secretKey) {
        this.mobicardMerchantId = merchantId;
        this.mobicardApiKey = apiKey;
        this.mobicardSecretKey = secretKey;
        
        Random random = new Random();
        this.mobicardTokenId = String.valueOf(random.nextInt(900000000) + 1000000);
        this.mobicardTxnReference = String.valueOf(random.nextInt(900000000) + 1000000);
    }
    
    public String imageToBase64FromUrl(String imageUrl) throws IOException {
        URL url = new URL(imageUrl);
        try (InputStream in = url.openStream();
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            
            byte[] imageBytes = out.toByteArray();
            return Base64.getEncoder().encodeToString(imageBytes);
        }
    }
    
    public String generateJWT(String base64Image) throws Exception {
        Map jwtHeader = new HashMap<>();
        jwtHeader.put("typ", "JWT");
        jwtHeader.put("alg", "HS256");
        String encodedHeader = base64UrlEncode(gson.toJson(jwtHeader));
        
        Map jwtPayload = new HashMap<>();
        jwtPayload.put("mobicard_version", mobicardVersion);
        jwtPayload.put("mobicard_mode", mobicardMode);
        jwtPayload.put("mobicard_merchant_id", mobicardMerchantId);
        jwtPayload.put("mobicard_api_key", mobicardApiKey);
        jwtPayload.put("mobicard_service_id", mobicardServiceId);
        jwtPayload.put("mobicard_service_type", mobicardServiceType);
        jwtPayload.put("mobicard_token_id", mobicardTokenId);
        jwtPayload.put("mobicard_txn_reference", mobicardTxnReference);
        jwtPayload.put("mobicard_scan_card_photo_base64_string", base64Image);
        jwtPayload.put("mobicard_extra_data", mobicardExtraData);
        
        String encodedPayload = base64UrlEncode(gson.toJson(jwtPayload));
        
        String headerPayload = encodedHeader + "." + encodedPayload;
        String signature = generateHMAC(headerPayload, mobicardSecretKey);
        
        return encodedHeader + "." + encodedPayload + "." + signature;
    }
    
    public JsonObject scanCard(String base64Image) throws Exception {
        String jwtToken = generateJWT(base64Image);
        
        HttpClient client = HttpClient.newHttpClient();
        
        Map requestBody = new HashMap<>();
        requestBody.put("mobicard_auth_jwt", jwtToken);
        
        String jsonBody = gson.toJson(requestBody);
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://mobicardsystems.com/api/v1/card_scan"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();
        
        HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
        
        return gson.fromJson(response.body(), JsonObject.class);
    }
    
    public JsonObject scanCardFromUrl(String imageUrl) throws Exception {
        String base64Image = imageToBase64FromUrl(imageUrl);
        return scanCard(base64Image);
    }
    
    private String base64UrlEncode(String data) {
        return Base64.getUrlEncoder().withoutPadding().encodeToString(data.getBytes());
    }
    
    private String generateHMAC(String data, String key) throws Exception {
        Mac sha256Hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "HmacSHA256");
        sha256Hmac.init(secretKey);
        byte[] hmacBytes = sha256Hmac.doFinal(data.getBytes());
        return base64UrlEncode(new String(hmacBytes));
    }
    
    public static void main(String[] args) {
        try {
            MobicardMethod2 scanner = new MobicardMethod2(
                "4",
                "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9",
                "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9"
            );
            
            JsonObject result = scanner.scanCardFromUrl(
                "https://mobicardsystems.com/scan_card_photo_one.jpg"
            );
            
            if (result.has("status")) {
                String status = result.get("status").getAsString();
                
                if ("SUCCESS".equals(status)) {
                    System.out.println("Scan Successful!");
                    
                    if (result.has("card_information")) {
                        JsonObject cardInfo = result.getAsJsonObject("card_information");
                        
                        System.out.println("Card Number: " + 
                            cardInfo.get("card_number_masked").getAsString());
                        System.out.println("Expiry Date: " + 
                            cardInfo.get("card_expiry_date").getAsString());
                        System.out.println("Card Brand: " + 
                            cardInfo.get("card_brand").getAsString());
                        System.out.println("Bank: " + 
                            cardInfo.get("card_bank_name").getAsString());
                        System.out.println("Confidence Score: " + 
                            cardInfo.get("card_confidence_score").getAsString());
                        
                        if (cardInfo.has("card_validation_checks")) {
                            JsonObject validationChecks = 
                                cardInfo.getAsJsonObject("card_validation_checks");
                            
                            if (validationChecks.has("luhn_algorithm") && 
                                validationChecks.get("luhn_algorithm").getAsBoolean()) {
                                System.out.println("✓ Luhn Algorithm Check Passed");
                            }
                            
                            if (validationChecks.has("expiry_date")) {
                                if (validationChecks.get("expiry_date").getAsBoolean()) {
                                    System.out.println("✓ Expiry Date is Valid");
                                } else {
                                    System.out.println("⚠ Expired or Invalid Expiry Date");
                                }
                            }
                        }
                    }
                } else {
                    System.out.println("Scan Failed!");
                    if (result.has("status_message")) {
                        System.out.println("Error: " + result.get("status_message").getAsString());
                    }
                }
            }
            
        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

JavaScript Base64 Conversion Example (Frontend)

How to capture an image and convert it to base64 on the frontend for Method 2.

JavaScript Base64 Conversion
METHOD 2
// ===================== CONFIGURATION =====================
const MOBICARD_CONFIG = {
    backendEndpoint: '/api/mobicard/scan', // Your backend endpoint
    debugMode: false,
    maxRetryAttempts: 3,
    imageQuality: 0.92 // JPEG quality (0.0 to 1.0)
};

// ===================== DEBUGGING UTILITIES =====================
function mobicardDebugLog(message, type = 'info') {
    if (!MOBICARD_CONFIG.debugMode) return;
    
    const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
    const logMessage = `[MobiCard ${type.toUpperCase()}] [${timestamp}] ${message}`;
    
    console.log(logMessage);
    
    // Optional: Add to debug console UI if available
    if (window.debugConsole) {
        window.debugConsole.addLog(message, type);
    }
}

// ===================== IMAGE CAPTURE AND BASE64 CONVERSION =====================

/**
 * Capture image from canvas and convert to base64
 * @param  {HTMLCanvasElement} canvas - The canvas element containing the image
 * @returns  {string} Base64 encoded image data (without data URL prefix)
 */
function captureImageToBase64(canvas) {
    try {
        mobicardDebugLog('Converting canvas image to base64...', 'info');
        
        // Convert canvas to base64 JPEG
        const base64Image = canvas.toDataURL('image/jpeg', MOBICARD_CONFIG.imageQuality);
        
        // Remove the data URL prefix
        const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
        
        mobicardDebugLog(`Base64 conversion successful: ${Math.round(base64Data.length / 1024)}KB`, 'success');
        
        return base64Data;
    } catch (error) {
        mobicardDebugLog(`Base64 conversion failed: ${error.message}`, 'error');
        throw new Error(`Image conversion error: ${error.message}`);
    }
}

/**
 * Capture image from video stream (camera) and convert to base64
 * @param  {HTMLVideoElement} videoElement - Video element with camera stream
 * @returns  {string} Base64 encoded image data
 */
function captureFromCamera(videoElement) {
    try {
        mobicardDebugLog('Capturing image from camera...', 'info');
        
        // Create temporary canvas
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        
        // Set canvas dimensions to match video
        canvas.width = videoElement.videoWidth;
        canvas.height = videoElement.videoHeight;
        
        // Draw video frame to canvas
        ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
        
        mobicardDebugLog(`Captured frame: ${canvas.width}x${canvas.height}`, 'info');
        
        // Convert to base64
        return captureImageToBase64(canvas);
    } catch (error) {
        mobicardDebugLog(`Camera capture failed: ${error.message}`, 'error');
        throw error;
    }
}

// ===================== FILE UPLOAD HANDLING =====================

/**
 * Handle image file upload and convert to base64
 * @param  {File} file - Image file object
 * @returns  {Promise<string>} Promise resolving to base64 data
 */
function handleFileUpload(file) {
    return new Promise((resolve, reject) => {
        mobicardDebugLog(`Processing uploaded file: ${file.name} (${Math.round(file.size / 1024)}KB)`, 'info');
        
        // Validate file type
        if (!file.type.match('image.*')) {
            const error = 'Please select an image file (JPEG, PNG, etc.)';
            mobicardDebugLog(error, 'error');
            reject(new Error(error));
            return;
        }
        
        // Validate file size (max 5MB)
        const maxSize = 5 * 1024 * 1024; // 5MB
        if (file.size > maxSize) {
            const error = 'Image size should be less than 5MB';
            mobicardDebugLog(error, 'error');
            reject(new Error(error));
            return;
        }
        
        const reader = new FileReader();
        
        reader.onload = function(e) {
            try {
                const base64Image = e.target.result;
                // Remove data URL prefix
                const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
                
                mobicardDebugLog('File uploaded and converted to base64 successfully', 'success');
                resolve(base64Data);
            } catch (error) {
                mobicardDebugLog(`File processing error: ${error.message}`, 'error');
                reject(error);
            }
        };
        
        reader.onerror = function() {
            const error = 'Error reading file. Please try again.';
            mobicardDebugLog(error, 'error');
            reject(new Error(error));
        };
        
        reader.readAsDataURL(file);
    });
}

// ===================== API COMMUNICATION =====================

/**
 * Send base64 image data to backend for processing
 * @param  {string} base64Data - Base64 encoded image data
 * @param  {string} scanType - Type of scan ('camera' or 'upload')
 * @returns  {Promise<Object>} Promise resolving to API response
 */
async function sendToMobicardAPI(base64Data, scanType = 'camera') {
    try {
        mobicardDebugLog(`Sending ${scanType} image to MobiCard API...`, 'info');
        
        const formData = new FormData();
        formData.append('mobicard_scan_card_photo', dataURLtoBlob(`data:image/jpeg;base64,${base64Data}`));
        formData.append('scan_type', scanType);
        formData.append('timestamp', new Date().toISOString());
        
        const response = await fetch(MOBICARD_CONFIG.backendEndpoint, {
            method: 'POST',
            body: formData
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const data = await response.json();
        
        if (data.status === 'SUCCESS') {
            mobicardDebugLog('MobiCard API scan successful!', 'success');
            return data;
        } else {
            mobicardDebugLog(`MobiCard API scan failed: ${data.status_message || 'Unknown error'}`, 'error');
            throw new Error(data.status_message || 'Scan failed');
        }
    } catch (error) {
        mobicardDebugLog(`API communication error: ${error.message}`, 'error');
        throw error;
    }
}

// ===================== HELPER FUNCTIONS =====================

/**
 * Convert data URL to Blob object
 * @param  {string} dataURL - Data URL string
 * @returns  {Blob} Blob object
 */
function dataURLtoBlob(dataURL) {
    const arr = dataURL.split(',');
    const mime = arr[0].match(/:(.*?);/)[1];
    const bstr = atob(arr[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
    }
    
    return new Blob([u8arr], { type: mime });
}

/**
 * Update form fields with scanned card data
 * @param  {Object} cardInfo - Card information from API response
 */
function updateCardFormWithScannedData(cardInfo) {
    mobicardDebugLog('Updating form with scanned card data', 'info');
    
    try {
        // Raw card number
        const cardNumberField = document.getElementById('card_number');
        if (cardNumberField) {
            cardNumberField.value = cardInfo.card_number;
        }
        
        // Formatted card number display
        const formattedField = document.getElementById('formatted_card_number');
        if (formattedField) {
            formattedField.textContent = formatCardNumber(cardInfo.card_number);
        }
        
        // Expiry date
        const expiryField = document.getElementById('card_expiry');
        if (expiryField) {
            expiryField.value = cardInfo.card_expiry_date;
            
            // Add warning class if expiry check failed
            if (cardInfo.card_validation_checks && !cardInfo.card_validation_checks.expiry_date) {
                expiryField.classList.add('expired-warning');
                mobicardDebugLog('Card expiry validation failed', 'warning');
            }
        }
        
        // CVV field (focus for user input)
        const cvvField = document.getElementById('card_cvv');
        if (cvvField) {
            cvvField.focus();
        }
        
        // Card brand display
        const brandIcon = document.getElementById('card_brand_icon');
        if (brandIcon) {
            brandIcon.className = `card-brand ${cardInfo.card_brand.toLowerCase()}`;
            brandIcon.src = getBrandLogoUrl(cardInfo.card_brand);
            brandIcon.alt = cardInfo.card_brand;
            brandIcon.style.display = 'inline-block';
        }
        
        // Bank name display
        const bankField = document.getElementById('card_bank');
        if (bankField && cardInfo.card_bank_name) {
            bankField.textContent = cardInfo.card_bank_name;
        }
        
        // Confidence score display (optional)
        const confidenceField = document.getElementById('confidence_score');
        if (confidenceField && cardInfo.card_confidence_score) {
            const confidencePercent = (parseFloat(cardInfo.card_confidence_score) * 100).toFixed(1);
            confidenceField.textContent = `${confidencePercent}%`;
            confidenceField.className = `confidence-badge ${confidencePercent > 80 ? 'high-confidence' : 'low-confidence'}`;
        }
        
        mobicardDebugLog('Form updated successfully with card data', 'success');
        
    } catch (error) {
        mobicardDebugLog(`Error updating form: ${error.message}`, 'error');
        throw error;
    }
}

/**
 * Format card number with appropriate spacing
 * @param  {string} number - Raw card number
 * @returns  {string} Formatted card number
 */
function formatCardNumber(number) {
    // Remove all non-digits
    const clean = number.replace(/\D/g, '');
    
    // Check for American Express (15 digits starting with 34 or 37)
    if (clean.length === 15 && (clean.startsWith('34') || clean.startsWith('37'))) {
        // Amex format: XXXX XXXXXX XXXXX
        return clean.replace(/(\d{4})(\d{6})(\d{5})/, '$1 $2 $3');
    }
    
    // Standard format: XXXX XXXX XXXX XXXX (or XXXX XXXX XXXX XXXX XXXX for 19-20 digits)
    if (clean.length === 16) {
        return clean.replace(/(\d{4})/g, '$1 ').trim();
    } else if (clean.length === 19 || clean.length === 20) {
        return clean.replace(/(\d{4})/g, '$1 ').trim();
    }
    
    // Fallback: group in 4s
    return clean.replace(/(\d{4})/g, '$1 ').trim();
}

/**
 * Get brand logo URL based on card brand
 * @param  {string} brand - Card brand name
 * @returns  {string} Logo URL
 */
function getBrandLogoUrl(brand) {
    const brandLower = brand.toLowerCase();
    
    const logoUrls = {
        'visa': 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Visa_Inc._logo.svg',
        'mastercard': 'https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg',
        'amex': 'https://upload.wikimedia.org/wikipedia/commons/3/30/American_Express_logo.svg',
        'american express': 'https://upload.wikimedia.org/wikipedia/commons/3/30/American_Express_logo.svg',
        'discover': 'https://upload.wikimedia.org/wikipedia/commons/5/5a/Discover_Card_logo.svg',
        'diners club': 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Diners_Club_Logo3.svg',
        'jcb': 'https://upload.wikimedia.org/wikipedia/commons/4/40/JCB_logo.svg',
        'unionpay': 'https://upload.wikimedia.org/wikipedia/commons/3/33/UnionPay_logo.svg'
    };
    
    // Try exact match first, then partial match
    if (logoUrls[brandLower]) {
        return logoUrls[brandLower];
    }
    
    for (const [key, url] of Object.entries(logoUrls)) {
        if (brandLower.includes(key)) {
            return url;
        }
    }
    
    // Default generic card icon
    return 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/icons/credit-card.svg';
}

/**
 * Detect card type from card number
 * @param  {string} number - Card number (can include spaces)
 * @returns  {string} Card type identifier
 */
function detectCardType(number) {
    const clean = number.replace(/\D/g, '');
    
    // Visa: starts with 4
    if (/^4/.test(clean)) return 'visa';
    
    // MasterCard: starts with 51-55 or 2221-2720
    if (/^5[1-5]/.test(clean) || /^2[2-7][0-9]{2}/.test(clean)) return 'mastercard';
    
    // American Express: starts with 34 or 37
    if (/^3[47]/.test(clean)) return 'amex';
    
    // Discover: starts with 6011, 65, or 64[4-9]
    if (/^6011/.test(clean) || /^65/.test(clean) || /^64[4-9]/.test(clean)) return 'discover';
    
    // Diners Club: starts with 300-305, 36, or 38
    if (/^3(0[0-5]|6|8)/.test(clean)) return 'diners club';
    
    // JCB: starts with 2131, 1800, or 35
    if (/^(2131|1800|35)/.test(clean)) return 'jcb';
    
    // UnionPay: starts with 62
    if (/^62/.test(clean)) return 'unionpay';
    
    return 'unknown';
}

// ===================== COMPLETE WORKFLOW EXAMPLES =====================

/**
 * Complete camera scanning workflow
 * @param  {HTMLVideoElement} videoElement - Video element with camera stream
 */
async function completeCameraScanWorkflow(videoElement) {
    try {
        mobicardDebugLog('Starting camera scan workflow', 'info');
        
        // Step 1: Capture image from camera
        const base64Data = captureFromCamera(videoElement);
        
        // Step 2: Send to MobiCard API
        const response = await sendToMobicardAPI(base64Data, 'camera');
        
        // Step 3: Update form with results
        if (response.status === 'SUCCESS') {
            updateCardFormWithScannedData(response.card_information);
        }
        
        return response;
    } catch (error) {
        mobicardDebugLog(`Camera scan workflow failed: ${error.message}`, 'error');
        throw error;
    }
}

/**
 * Complete file upload scanning workflow
 * @param  {HTMLInputElement} fileInput - File input element
 */
async function completeFileUploadWorkflow(fileInput) {
    try {
        mobicardDebugLog('Starting file upload workflow', 'info');
        
        if (!fileInput.files || !fileInput.files[0]) {
            throw new Error('Please select an image file first');
        }
        
        // Step 1: Process uploaded file
        const base64Data = await handleFileUpload(fileInput.files[0]);
        
        // Step 2: Send to MobiCard API
        const response = await sendToMobicardAPI(base64Data, 'upload');
        
        // Step 3: Update form with results
        if (response.status === 'SUCCESS') {
            updateCardFormWithScannedData(response.card_information);
        }
        
        return response;
    } catch (error) {
        mobicardDebugLog(`File upload workflow failed: ${error.message}`, 'error');
        throw error;
    }
}

// ===================== EVENT HANDLER EXAMPLES =====================

// Example: Camera capture button click handler
document.addEventListener('DOMContentLoaded', function() {
    const captureButton = document.getElementById('captureButton');
    const videoElement = document.getElementById('cameraPreview');
    
    if (captureButton && videoElement) {
        captureButton.addEventListener('click', async function() {
            try {
                await completeCameraScanWorkflow(videoElement);
            } catch (error) {
                console.error('Capture failed:', error);
                alert(`Scan failed: ${error.message}`);
            }
        });
    }
    
    // Example: File upload change handler
    const uploadInput = document.getElementById('cardImageUpload');
    if (uploadInput) {
        uploadInput.addEventListener('change', async function(event) {
            try {
                await completeFileUploadWorkflow(event.target);
            } catch (error) {
                console.error('Upload failed:', error);
                alert(`Upload failed: ${error.message}`);
            }
        });
    }
});

// ===================== ERROR HANDLING AND RETRY LOGIC =====================

/**
 * Retry function with exponential backoff
 * @param  {Function} fn - Function to retry
 * @param  {number} maxRetries - Maximum number of retries
 * @param  {number} baseDelay - Base delay in milliseconds
 * @returns  {Promise} Promise that resolves with function result
 */
async function retryWithBackoff(fn, maxRetries = MOBICARD_CONFIG.maxRetryAttempts, baseDelay = 1000) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            mobicardDebugLog(`Attempt ${attempt}/${maxRetries}`, 'info');
            return await fn();
        } catch (error) {
            if (attempt === maxRetries) {
                mobicardDebugLog(`All ${maxRetries} attempts failed`, 'error');
                throw error;
            }
            
            const delay = baseDelay * Math.pow(2, attempt - 1);
            mobicardDebugLog(`Attempt ${attempt} failed, retrying in ${delay}ms`, 'warning');
            
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

// ===================== INITIALIZATION =====================

// Initialize MobiCard scanner when page loads
window.addEventListener('load', function() {
    mobicardDebugLog('MobiCard JavaScript SDK initialized', 'success');
    
    // Optional: Set up global error handler for API calls
    window.mobicardErrorHandler = function(error) {
        mobicardDebugLog(`Unhandled MobiCard error: ${error.message}`, 'error');
        
        // Display user-friendly error message
        const errorDiv = document.getElementById('mobicardError');
        if (errorDiv) {
            errorDiv.textContent = `Error: ${error.message}`;
            errorDiv.style.display = 'block';
            
            // Auto-hide after 5 seconds
            setTimeout(() => {
                errorDiv.style.display = 'none';
            }, 5000);
        }
    };
});

// Export functions for use in module systems (optional)
if (typeof module !== 'undefined' && module.exports) {
    module.exports = {
        captureImageToBase64,
        captureFromCamera,
        handleFileUpload,
        sendToMobicardAPI,
        formatCardNumber,
        detectCardType,
        getBrandLogoUrl,
        completeCameraScanWorkflow,
        completeFileUploadWorkflow,
        retryWithBackoff
    };
}

Sample Code : Full PHP Implementation (Method 2)

Complete working PHP code for Method 2 with integrated frontend camera scanning.

You may copy-paste the code below as-is but remember to store your 'api_key' and 'secret_key' in your .env file.

Complete PHP Implementation
METHOD 2
<?php
// ===================== PHP CONFIGURATION =====================
$mobicard_version = "2.0";
$mobicard_mode = "LIVE";
$mobicard_merchant_id = "4";
$mobicard_api_key = "YmJkOGY0OTZhMTU2ZjVjYTIyYzFhZGQyOWRiMmZjMmE2ZWU3NGIxZWM3ZTBiZSJ9";
$mobicard_secret_key = "NjIwYzEyMDRjNjNjMTdkZTZkMjZhOWNiYjIxNzI2NDQwYzVmNWNiMzRhMzBjYSJ9";
$mobicard_token_id = abs(rand(1000000, 1000000000));
$mobicard_txn_reference = abs(rand(1000000, 1000000000));
$mobicard_service_id = "20000";
$mobicard_service_type = "2";
$mobicard_extra_data = "card_scan_" . time();
$mobicard_request_access_token_url = "https://mobicardsystems.com/api/v1/card_scan";

// Debug mode
$debug_mode = false; // Set to false in production
?>

<?php
// ===================== PHP BACKEND HANDLER =====================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    
    // Enable error reporting for debugging
    if ($debug_mode) {
        error_reporting(E_ALL);
        ini_set('display_errors', 1);
    }
    
    // Check if it's an API call
    if (isset($_POST['action']) && $_POST['action'] === 'scan') {
        
        $response = ['status' => 'FAILED', 'status_message' => 'Initial error'];
        
        try {
            // Check for uploaded file
            if (isset($_FILES['mobicard_scan_card_photo'])) {
                $file = $_FILES['mobicard_scan_card_photo'];
                
                if ($file['error'] === UPLOAD_ERR_OK) {
                    // Read file and convert to base64
                    $imageData = file_get_contents($file['tmp_name']);
                    $base64Image = base64_encode($imageData);
                    
                    if ($debug_mode) {
                        error_log("Image size: " . strlen($imageData) . " bytes");
                        error_log("Base64 size: " . strlen($base64Image) . " characters");
                    }
                    
                    // Create JWT header
                    $header = [
                        "typ" => "JWT",
                        "alg" => "HS256"
                    ];
                    $headerEncoded = base64_encode(json_encode($header));
                    
                    // Create JWT payload
                    $payload = [
                        "mobicard_version" => $mobicard_version,
                        "mobicard_mode" => $mobicard_mode,
                        "mobicard_merchant_id" => $mobicard_merchant_id,
                        "mobicard_api_key" => $mobicard_api_key,
                        "mobicard_service_id" => $mobicard_service_id,
                        "mobicard_service_type" => $mobicard_service_type,
                        "mobicard_token_id" => $mobicard_token_id,
                        "mobicard_txn_reference" => $mobicard_txn_reference,
                        "mobicard_scan_card_photo_base64_string" => $base64Image,
                        "mobicard_extra_data" => $mobicard_extra_data
                    ];
                    
                    $payloadEncoded = strtr(base64_encode(json_encode($payload)), '+/=', '-_,');
                    
                    // Create signature
                    $signature = base64_encode(hash_hmac('sha256', 
                        $headerEncoded . '.' . $payloadEncoded, 
                        $mobicard_secret_key, 
                        true
                    ));
                    
                    $jwt = $headerEncoded . '.' . $payloadEncoded . '.' . $signature;
                    
                    if ($debug_mode) {
                        error_log("JWT created: " . substr($jwt, 0, 50) . "...");
                    }
                    
                    // Make API call
                    $ch = curl_init();
                    curl_setopt_array($ch, [
                        CURLOPT_URL => $mobicard_request_access_token_url,
                        CURLOPT_RETURNTRANSFER => true,
                        CURLOPT_POST => true,
                        CURLOPT_POSTFIELDS => json_encode(['mobicard_auth_jwt' => $jwt]),
                        CURLOPT_HTTPHEADER => [
                            'Content-Type: application/json',
                            'Accept: application/json'
                        ],
                        CURLOPT_SSL_VERIFYHOST => false,
                        CURLOPT_SSL_VERIFYPEER => false,
                        CURLOPT_TIMEOUT => 30,
                        CURLOPT_CONNECTTIMEOUT => 10
                    ]);
                    
                    $apiResponse = curl_exec($ch);
                    
                    // Debug: Log raw response
                    if ($debug_mode) {
                        error_log("RAW API RESPONSE: " . $apiResponse);
                    }
                    
                    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                    $curlError = curl_error($ch);
                    curl_close($ch);
                    
                    if ($debug_mode) {
                        error_log("API HTTP Code: " . $httpCode);
                        error_log("API Response length: " . strlen($apiResponse));
                        if ($curlError) {
                            error_log("CURL Error: " . $curlError);
                        }
                    }
                    
                    if ($httpCode === 200 && $apiResponse) {
                        $response = json_decode($apiResponse, true);
                        
                        if (!$response || json_last_error() !== JSON_ERROR_NONE) {
                            if ($debug_mode) {
                                error_log("JSON decode error: " . json_last_error_msg());
                                error_log("Response that failed to parse: " . substr($apiResponse, 0, 500));
                            }
                            $response = [
                                'status' => 'FAILED',
                                'status_code' => '500',
                                'status_message' => 'Invalid API response format: ' . json_last_error_msg()
                            ];
                        }
                    } else {
                        $response = [
                            'status' => 'FAILED',
                            'status_code' => $httpCode ?: '500',
                            'status_message' => ''
                        ];
                    }
                    
                } else {
                    $response = [
                        'status' => 'FAILED',
                        'status_code' => '400',
                        'status_message' => 'File upload error: ' . $file['error']
                    ];
                }
            } else {
                $response = [
                    'status' => 'FAILED',
                    'status_code' => '400',
                    'status_message' => 'No image file received'
                ];
            }
            
        } catch (Exception $e) {
            $response = [
                'status' => 'FAILED',
                'status_code' => '500',
                'status_message' => 'Server error: ' . $e->getMessage()
            ];
            
            if ($debug_mode) {
                error_log("Exception: " . $e->getMessage());
                error_log("Trace: " . $e->getTraceAsString());
            }
        }
        
        // Return JSON response
        header('Content-Type: application/json');
        echo json_encode($response);
        exit;
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>MobiCard Scanner</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="copyright" content="MobiCard">
    <meta name="author" content="MobiCard">
    
    <!-- Bootstrap 4 -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css">
    
    <style>
        body { 
            background: #000; 
            color: #fff; 
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            margin: 0;
            padding: 0;
            overflow-x: hidden;
        }
        
        .camera-container {
            position: relative;
            width: 100%;
            height: calc(100vh - 60px);
            background: #000;
            overflow: hidden;
        }
        
        video { 
            width: 100%; 
            height: 100%; 
            object-fit: cover; 
        }
        
        .scan-frame {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            border: 3px solid rgba(255, 0, 0, 0.8);
            border-radius: 15px;
            box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);
            pointer-events: none;
            transition: all 0.3s ease;
            z-index: 10;
        }
        
        /* Mobile: Card ratio 1.586 (standard card) */
        @media (max-width: 768px) {
            .scan-frame {
                width: 92% !important;
                height: 36% !important;
            }
        }
        
        /* Desktop: Slightly smaller */
        @media (min-width: 769px) {
            .scan-frame {
                width: 45% !important;
                height: 67% !important;
            }
        }
        
        .scan-frame.locked { 
            border-color: #0f0; 
            box-shadow: 0 0 25px rgba(0, 255, 0, 0.5), 0 0 0 9999px rgba(0,0,0,0.7); 
        }
        
        .scan-frame.bad { 
            border-color: #ff4444; 
            box-shadow: 0 0 25px rgba(255, 68, 68, 0.5), 0 0 0 9999px rgba(0,0,0,0.7); 
        }
        
        .scan-hint {
            position: absolute;
            top: 20px;
            width: 100%;
            text-align: center;
            font-size: 16px;
            color: #0f0;
            text-shadow: 0 0 10px #0f0;
            z-index: 20;
            background: rgba(0,0,0,0.5);
            padding: 10px;
            font-weight: bold;
        }
        
        .scan-line {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 4px;
            background: linear-gradient(to bottom, transparent, #0f0, transparent);
            animation: scanmove 2s linear infinite;
            z-index: 5;
            opacity: 0.9;
            box-shadow: 0 0 10px #0f0;
        }
        
        @keyframes  scanmove {
            0% { top: 0%; }
            100% { top: 100%; }
        }
        
        .floating-btn {
            position: absolute;
            bottom: 15px;
            left: 50%;
            transform: translateX(-50%);
            z-index: 20;
            background: white !important;
            color: black !important;
            font-weight: bold;
            border: none;
            padding: 15px 30px;
            border-radius: 30px;
            box-shadow: 0 5px 20px rgba(0,0,0,0.5);
            font-size: 16px;
            min-width: 180px;
        }
        
        .floating-btn:hover {
            background: #f8f9fa !important;
            transform: translateX(-50%) scale(1.05);
        }
        
        .upload-box {
            border: 2px dashed #666;
            padding: 40px 20px;
            text-align: center;
            margin: 20px;
            background: #111;
            border-radius: 12px;
            transition: all 0.3s;
        }
        
        .upload-box:hover {
            border-color: #0f0;
            background: #1a1a1a;
        }
        
        /* Modal Styling */
        #resultModal .modal-content { 
            background: linear-gradient(180deg, #ffffff, #f8f9fa);
            color: #212529; 
            border-radius: 16px;
            border: 1px solid rgba(0,0,0,0.1);
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
        }
        
        #resultModal .modal-header { 
            border-bottom: 2px solid #e9ecef; 
            background: linear-gradient(135deg, #f8f9fa, #e9ecef);
            border-radius: 16px 16px 0 0;
            padding: 20px;
        }
        
        #resultModal .modal-title {
            font-weight: 700;
            font-size: 1.4rem;
            color: #212529;
        }
        
        #resultModal .modal-body {
            padding: 25px;
        }
        
        #resultModal .modal-footer { 
            border-top: 2px solid #e9ecef; 
            padding: 20px;
            border-radius: 0 0 16px 16px;
        }
        
        .card-info-block {
            background: #ffffff;
            border-radius: 12px;
            padding: 18px;
            margin-bottom: 16px;
            border: 1px solid #e9ecef;
            box-shadow: 0 4px 12px rgba(0,0,0,0.05);
            transition: all 0.3s;
        }
        
        .card-info-block:hover {
            box-shadow: 0 6px 20px rgba(0,0,0,0.1);
            transform: translateY(-2px);
        }
        
        .card-label {
            font-size: 12px;
            text-transform: uppercase;
            color: #6c757d;
            letter-spacing: 0.8px;
            margin-bottom: 8px;
            font-weight: 600;
        }
        
        .card-value {
            font-size: 20px;
            font-weight: 700;
            color: #212529;
            font-family: 'Courier New', monospace;
        }
        
        .status-badge {
            display: inline-block;
            padding: 6px 14px;
            border-radius: 20px;
            font-size: 13px;
            font-weight: 700;
            letter-spacing: 0.5px;
        }
        
        .status-success { 
            background: linear-gradient(135deg, #28a745, #20c997); 
            color: white; 
            box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
        }
        
        .status-warning { 
            background: linear-gradient(135deg, #ffc107, #fd7e14); 
            color: white; 
            box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
        }
        
        .status-danger { 
            background: linear-gradient(135deg, #dc3545, #c82333); 
            color: white; 
            box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3);
        }
        
        .brand-logo {
            width: 50px;
            height: auto;
            vertical-align: middle;
            margin-left: 15px;
            filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1));
        }
        
        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.95);
            z-index: 99999;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
            color: white;
            backdrop-filter: blur(10px);
        }
        
        .spinner {
            width: 70px;
            height: 70px;
            border: 5px solid rgba(255,255,255,0.1);
            border-radius: 50%;
            border-top-color: #0f0;
            animation: spin 1s ease-in-out infinite;
            margin-bottom: 25px;
            box-shadow: 0 0 30px rgba(0, 255, 0, 0.3);
        }
        
        @keyframes  spin {
            to { transform: rotate(360deg); }
        }
        
        .loading-text {
            font-size: 18px;
            font-weight: 500;
            text-align: center;
            max-width: 300px;
            line-height: 1.5;
        }
        
        .retry-badge {
            position: absolute;
            top: 70px;
            right: 20px;
            background: rgba(0,0,0,0.7);
            color: white;
            padding: 8px 16px;
            border-radius: 20px;
            font-size: 14px;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255,255,255,0.1);
            z-index: 20;
        }
        
        .debug-console {
            position: fixed;
            bottom: 0;
            left: 0;
            width: 100%;
            background: rgba(0,0,0,0.95);
            color: #0f0;
            font-family: 'Courier New', monospace;
            font-size: 12px;
            max-height: 200px;
            overflow-y: auto;
            padding: 15px;
            border-top: 2px solid #0f0;
            z-index: 99998;
            display: none;
        }
        
        .debug-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
            padding-bottom: 10px;
            border-bottom: 1px solid #333;
        }
        
        .debug-toggle {
            background: #0f0;
            color: #000;
            border: none;
            padding: 5px 10px;
            border-radius: 4px;
            font-weight: bold;
            cursor: pointer;
            font-size: 12px;
        }
        
        .debug-log {
            margin: 5px 0;
            padding: 5px;
            border-left: 3px solid #0f0;
            padding-left: 10px;
        }
        
        .debug-error {
            color: #ff4444;
            border-left-color: #ff4444;
        }
        
        .debug-success {
            color: #44ff44;
            border-left-color: #44ff44;
        }
        
        .debug-warning {
            color: #ffff44;
            border-left-color: #ffff44;
        }
        
        footer {
            text-align: center;
            padding: 15px;
            background: #111;
            color: #666;
            font-size: 12px;
            border-top: 1px solid #222;
        }
        
        /* Tabs styling */
        .nav-tabs {
            border-bottom: 2px solid #111;
            background: #000;
            padding: 0 20px;
        }
        
        .nav-tabs .nav-link {
            color: #aaa;
            border: none;
            border-bottom: 3px solid transparent;
            padding: 15px 20px;
            font-weight: 600;
            transition: all 0.3s;
        }
        
        .nav-tabs .nav-link:hover {
            color: #fff;
            background: #1a1a1a;
        }
        
        .nav-tabs .nav-link.active {
            color: #fff;
            background: #111;
            border-bottom: 3px solid #0f0;
        }
        
        .tab-content {
            position: relative;
        }
        
        .quality-indicator {
            position: absolute;
            bottom: 80px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.7);
            padding: 10px 20px;
            border-radius: 20px;
            font-size: 14px;
            z-index: 15;
            display: none;
        }
        
        /* Reload overlay */
        .reload-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.85);
            z-index: 30;
            display: none;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            color: #fff;
            text-align: center;
        }
        
        .reload-icon {
            font-size: 48px;
            margin-bottom: 20px;
            cursor: pointer;
            animation: pulse 2s infinite;
            color: #0f0;
        }
        
        @keyframes  pulse {
            0% { transform: scale(1); opacity: 0.7; }
            50% { transform: scale(1.1); opacity: 1; }
            100% { transform: scale(1); opacity: 0.7; }
        }
        
        .reload-message {
            font-size: 16px;
            color: #ccc;
            margin-bottom: 20px;
        }
        
        .brand-logo {
            margin-left: 275px;
            margin-top: -228px;
        }
        
        .card-info-block {
            line-height: 5px;
        }
        
        .hidden {
            display: none;
        }
        
        /* Editable field styling */
        .editable-field {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
            font-size: 18px;
            font-weight: 700;
            letter-spacing: 1px;
            color: #0b1220;
            margin-left: -10px;
            line-height: 2;
            border: 1px solid #e7e7e7;
            background: transparent;
            width: 100%;
            padding: 6px 8px;
            border-radius: 4px;
            transition: border-color 0.2s ease;
        }
        
        .editable-field:focus {
            border-color: #007bff;
            background: rgba(0, 123, 255, 0.05);
            outline: none;
        }
        
        .editable-field.error {
            border-color: #dc3545;
            background: rgba(220, 53, 69, 0.05);
        }
        
        .field-container {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .edit-icon {
            color: #6c757d;
            cursor: pointer;
            font-size: 16px;
            transition: color 0.3s;
        }
        
        .edit-icon:hover {
            color: #007bff;
        }
        
        /* Mobile adjustments */
        @media (max-width: 768px) {
            
            .brand-logo {
                margin-left: 255px;
                margin-top: -225px;
            }
            
            .editable-field {
                font-size: 18px;
            }
            
            .floating-btn {
                padding: 12px 24px;
                min-width: 160px;
                font-size: 14px;
                bottom: 120px !important;
            }
            
            .scan-frame {
                position: absolute;
                top: 40%;
            }
        }
    </style>
</head>
<body>

<!-- Debug Console -->
<div id="debugConsole" class="debug-console">
    <div class="debug-header">
        <strong>DEBUG CONSOLE</strong>
        <button class="debug-toggle" onclick="toggleDebugConsole()">HIDE</button>
    </div>
    <div id="debugLogs"></div>
</div>

<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
    <div class="spinner"></div>
    <div id="loadingText" class="loading-text">Initializing camera...</div>
</div>

<!-- Tabs -->
<ul class="nav nav-tabs nav-justified">
    <li class="nav-item">
        <a class="nav-link active" id="scanTab" data-toggle="tab" href="#scanSection">Scan Card</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" id="uploadTab" data-toggle="tab" href="#uploadSection">Upload Card</a>
    </li>
</ul>

<div class="tab-content">
    <!-- Scan Section -->
    <div class="tab-pane fade show active" id="scanSection">
        <div class="camera-container" id="cameraContainer">
            <video id="cameraPreview" autoplay playsinline></video>
            <div class="scan-hint" id="scanHint">Align card inside the red frame</div>
            <div class="scan-line"></div>
            <div class="scan-frame" id="scanFrame"></div>
            <div class="quality-indicator" id="qualityIndicator">Quality: Good</div>
            <div class="retry-badge hidden" id="retryBadge">Attempt: <span id="attemptCount">0</span>/8</div>
            <button class="btn btn-light floating-btn" id="captureBtn">Capture Card</button>
            
            <!-- Reload overlay -->
            <div class="reload-overlay" id="reloadOverlay">
                <div class="reload-icon" id="reloadIcon">⟳</div>
                <div class="reload-message">Tap to refresh scanner</div>
            </div>
        </div>
        <canvas id="captureCanvas" style="display: none;"></canvas>
    </div>
    
    <!-- Upload Section -->
    <div class="tab-pane fade" id="uploadSection">
        <div class="container">
            <div class="upload-box">
                <h5 style="color: #fff; margin-bottom: 25px;">Select Card Image</h5>
                <input type="file" id="uploadInput" class="form-control mt-3" accept="image/*" 
                       style="background: #222; color: #fff; border: 1px solid #444; padding: 15px;">
                <button class="btn btn-light mt-4" id="uploadBtn" style="padding: 5px 5px 5px 5px; font-weight: bold;">
                    Upload & Scan
                </button>
            </div>
        </div>
    </div>
</div>

<!-- Results Modal -->
<div class="modal fade" id="resultModal" tabindex="-1" role="dialog">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Your Checkout Page</h5>
                <img id="brandLogo" class="brand-logo" src="" alt="" onerror="this.style.display='none'">
            </div>
            <div class="modal-body">
                <div class="card-info-block">
                    <div class="card-label">Card Number</div>
                    <div class="field-container">
                        <input type="text" id="cardNumberDisplay" class="editable-field" 
                               value="**** **** **** ****" maxlength="19" required>                        
                    </div>
                </div>
                
                <div class="card-info-block hidden">
                    <div class="card-label">Card Number (Raw) <span class="text-danger">*</span></div>
                    <input type="hidden" id="cardNumberRaw" class="form-control editable-field" 
                           placeholder="Enter raw card number" maxlength="19"
                           required>
                </div>
                
                <div class="card-info-block">
                    <div class="card-label">Expiry Date</div>
                    <input type="text" id="expiryDisplay" class="editable-field" 
                           value="MM/YY" pattern="(0[1-9]|1[0-2])\/([0-9]{2})" title="Please enter a valid date in MM/YY format" maxlength="5" placeholder="MM/YY" required>
                </div>
                
                <div class="card-info-block mt-3">
                    <div class="card-label">CVV*</div>
                    <input type="tel" id="cvvInput" class="editable-field" 
                           placeholder="" maxlength="4" pattern="\d{3,4}"
                           autocomplete="off">
                </div>
                
                <div class="card-info-block hidden">
                    <div class="card-label">Card Brand</div>
                    <input type="text" id="cardBrand" class="editable-field" value="BRAND">
                </div>
                
                <div class="card-info-block hidden">
                    <div class="card-label">Bank</div>
                    <input type="text" id="cardBank" class="editable-field" value="BANK NAME">
                </div>
                
                <div class="card-info-block hidden">
                    <div class="card-label">Confidence Score</div>
                    <div class="card-value" id="confidenceScore">0.00</div>
                </div>
                
                <div class="row mt-3 hidden">
                    <div class="col-6">
                        <div class="card-info-block text-center">
                            <div class="card-label">Luhn Check</div>
                            <div id="luhnCheck" class="status-badge status-success">PASS</div>
                        </div>
                    </li>
                    <div class="col-6">
                        <div class="card-info-block text-center">
                            <div class="card-label">Expiry Check</div>
                            <div id="expiryCheck" class="status-badge status-warning">CHECK</div>
                        </div>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" id="proceedBtn">Proceed with Payment</button>
            </div>
        </div>
    </div>
</div>

<footer>
    <p>&copy; <span id="year"></span> MobiCard ScanAPI</p>
</footer>

<!-- JS -->
<script>
    document.getElementById("year").textContent = new Date().getFullYear();
</script>

<!-- JavaScript -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>

<script>
// ===================== CONFIGURATION =====================
const DEBUG_MODE = <?php echo $debug_mode ? 'true' : 'false'; ?>;
const MOBICARD_PARAMS = {
    version: "<?php echo $mobicard_version; ?>",
    mode: "<?php echo $mobicard_mode; ?>",
    merchantId: "<?php echo $mobicard_merchant_id; ?>",
    apiKey: "<?php echo $mobicard_api_key; ?>",
    secretKey: "<?php echo $mobicard_secret_key; ?>",
    tokenId: "<?php echo $mobicard_token_id; ?>",
    txnRef: "<?php echo $mobicard_txn_reference; ?>",
    serviceId: "<?php echo $mobicard_service_id; ?>",
    serviceType: "<?php echo $mobicard_service_type; ?>",
    extraData: "<?php echo $mobicard_extra_data; ?>",
    apiUrl: "<?php echo $mobicard_request_access_token_url; ?>"
};

// ===================== DEBUGGING UTILITIES =====================
function debugLog(message, type = 'info') {
    if (!DEBUG_MODE) return;
    
    const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
    const logDiv = document.getElementById('debugLogs');
    const logEntry = document.createElement('div');
    
    logEntry.className = `debug-log debug-${type}`;
    logEntry.innerHTML = `<strong>[${timestamp}]</strong> ${message}`;
    logDiv.appendChild(logEntry);
    logDiv.scrollTop = logDiv.scrollHeight;
    
    // Also log to console
    console.log(`[MobiCard ${type.toUpperCase()}] ${message}`);
}

function toggleDebugConsole() {
    const consoleDiv = document.getElementById('debugConsole');
    const button = document.querySelector('.debug-toggle');
    
    if (consoleDiv.style.display === 'none') {
        consoleDiv.style.display = 'block';
        button.textContent = 'HIDE';
    } else {
        consoleDiv.style.display = 'none';
        button.textContent = 'SHOW';
    }
}

// Show debug console if in debug mode
if (DEBUG_MODE) {
    document.getElementById('debugConsole').style.display = 'block';
    debugLog('Debug mode enabled', 'success');
}

// ===================== GLOBAL VARIABLES =====================
let cameraStream = null;
let scanActive = true;
let scanAttempt = 0;  // Renamed from currentAttempt to be specific to scanning
let uploadAttempt = 0; // Separate counter for upload attempts
const SCAN_MAX_ATTEMPTS = 15;
const UPLOAD_MAX_ATTEMPTS = 5; // Different limit for upload
let scanInterval = null;
let qualityCheckInterval = null;

// Add a global flag at the top with other variables
let initial_capture_and_submit_flag = 0;
let success_flag = 0;

// ===================== CAMERA TIMEOUT HANDLING =====================
let cameraTimeout = null;
const CAMERA_TIMEOUT_DURATION = 60000; // 5 minutes (300000 ms)

function resetCameraTimeout() {
    // Clear existing timeout
    if (cameraTimeout) {
        clearTimeout(cameraTimeout);
    }
    
    // Set new timeout
    cameraTimeout = setTimeout(() => {
        debugLog('Camera timeout - shutting down due to inactivity', 'warning');
        showTimeoutOverlay();
    }, CAMERA_TIMEOUT_DURATION);
}

function showTimeoutOverlay() {
    stopCamera();
    scanActive = false;
    
    // Create or show timeout overlay
    let timeoutOverlay = document.getElementById('timeoutOverlay');
    if (!timeoutOverlay) {
        timeoutOverlay = document.createElement('div');
        timeoutOverlay.id = 'timeoutOverlay';
        timeoutOverlay.className = 'reload-overlay';
        timeoutOverlay.innerHTML = `
            <div class="reload-icon" id="timeoutReloadIcon">⟳</div>
            <div class="reload-message">Camera timed out due to inactivity</div>
            <div class="reload-message">Tap to restart camera</div>
        `;
        document.querySelector('.camera-container').appendChild(timeoutOverlay);
        
        // Add click handler
        timeoutOverlay.addEventListener('click', function() {
            hideTimeoutOverlay();
        });
    }
    
    timeoutOverlay.style.display = 'flex';
}

function hideTimeoutOverlay() {
    const timeoutOverlay = document.getElementById('timeoutOverlay');
    if (timeoutOverlay) {
        timeoutOverlay.style.display = 'none';
    }
    
    // Reset and restart camera
    scanAttempt = 0;
    scanActive = true;
    initializeCamera();
}

// ===================== ACTIVITY DETECTION =====================
function setupActivityDetection() {
    // Reset timeout on any user interaction
    const events = ['mousemove', 'keydown', 'click', 'touchstart', 'scroll'];
    
    events.forEach(event => {
        document.addEventListener(event, resetCameraTimeout, { passive: true });
    });
    
    // Also reset on camera/video interaction
    if (video) {
        video.addEventListener('play', resetCameraTimeout);
        video.addEventListener('pause', resetCameraTimeout);
    }
    
    debugLog('Activity detection enabled', 'info');
}

const video = document.getElementById('cameraPreview');
const canvas = document.getElementById('captureCanvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const scanHint = document.getElementById('scanHint');
const scanFrame = document.getElementById('scanFrame');
const loadingOverlay = document.getElementById('loadingOverlay');
const loadingText = document.getElementById('loadingText');
const retryBadge = document.getElementById('retryBadge');
const attemptCount = document.getElementById('attemptCount');
const qualityIndicator = document.getElementById('qualityIndicator');
const reloadOverlay = document.getElementById('reloadOverlay');
const reloadIcon = document.getElementById('reloadIcon');
const captureBtn = document.getElementById('captureBtn');

// ===================== CAMERA INITIALIZATION (SIMPLE LIKE YOUR FILE) =====================
async function initializeCamera() {
    try {
        debugLog('Initializing camera...');
        
        // Check for media devices support
        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
            throw new Error('Camera API not supported');
        }

        // Modify your motion detection block to only run auto-submit if flag is active
        if (initial_capture_and_submit_flag === 0) {
            initial_capture_and_submit_flag++;
            
            loadingOverlay.style.display = 'none';
            
            // Delay second before starting the loop
            setTimeout(function() {
                // Loop maximum 5 times
                for (let i = 0; i < 5; i++) {
                    // Create closure to preserve the value of i for each iteration
                    (function(iteration) {
                        // Schedule each iteration
                        const timeoutId = setTimeout(function() {
                        
                            scanActive = false;
                            
                            captureAndProcess();
                            
                            debugLog(`Running captureAndProcess() - iteration ${iteration + 1}/5`);
                        
                        }, iteration * 2000);//Never below 2000 (2 seconds apart)
                        
                    })(i);
                }
            }, 100);
        }

        // Simple approach like your file - try to get camera immediately
        const constraints = {
            video: {
                facingMode: { ideal: 'environment' },
                width: { ideal: 1920 },
                height: { ideal: 1080 }
            },
            audio: false
        };
        
        debugLog('Requesting camera with constraints', 'info');
        cameraStream = await navigator.mediaDevices.getUserMedia(constraints);
        
        if (!cameraStream) {
            throw new Error('No camera stream obtained');
        }
        
        video.srcObject = cameraStream;
        
        // Wait for video to be ready
        await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                reject(new Error('Camera timeout'));
            }, 5000);
            
            video.onloadedmetadata = () => {
                clearTimeout(timeout);
                video.play().then(resolve).catch(reject);
            };
            
            video.onerror = () => {
                clearTimeout(timeout);
                reject(new Error('Video error'));
            };
        });
        
        debugLog(`Camera ready: ${video.videoWidth}x${video.videoHeight}`, 'success');

        // Adjust scan frame based on device
        adjustScanFrame();
        
        // Start quality checking
        startQualityCheck();
        
        // Hide loading after video starts playing
        setTimeout(() => {
            loadingOverlay.style.display = 'none';
            startAutoScan();
            debugLog('Camera initialized successfully', 'success');
        }, 10);
        
        // Start camera timeout tracking
        resetCameraTimeout();
        
    } catch (error) {
        debugLog(`Camera initialization failed: ${error.message}`, 'error');
        console.error('Camera error:', error);
        
        loadingText.textContent = `Camera error: ${error.message}. Please allow camera permissions or use upload.`;
        
        // Simple error handling like your file
        setTimeout(() => {
            loadingOverlay.style.display = 'none';
            $('#uploadTab').tab('show');
            alert("Camera access denied. Please allow camera permissions.");
        }, 2000);
    }
}

function adjustScanFrame() {
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    const frame = document.getElementById('scanFrame');
    
    if (isMobile) {
        // Mobile: Card ratio 1.586 (credit card standard)
        frame.style.width = '92%';
        frame.style.height = `${92 / 1.586}%`; // Approximately 58%
        debugLog('Set mobile scan frame dimensions', 'info');
    } else {
        // Desktop: Slightly smaller
        frame.style.width = '45%';
        frame.style.height = `${45 / 1.586}%`; // Approximately 28.4%
        debugLog('Set desktop scan frame dimensions', 'info');
    }
}

// ===================== QUALITY CHECKING =====================
function startQualityCheck() {
    if (qualityCheckInterval) clearInterval(qualityCheckInterval);
    
    qualityCheckInterval = setInterval(() => {
        if (!video.videoWidth || scanActive === false) return;
        
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        ctx.drawImage(video, 0, 0);
        
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;
        
        // Check brightness
        let brightness = 0;
        for (let i = 0; i < data.length; i += 4) {
            brightness += (data[i] + data[i + 1] + data[i + 2]) / 3;
        }
        brightness /= (data.length / 4);
        
        // Check contrast (edge detection)
        let edges = 0;
        for (let i = 0; i < data.length - 4; i += 4) {
            const pixel1 = (data[i] + data[i + 1] + data[i + 2]) / 3;
            const pixel2 = (data[i + 4] + data[i + 5] + data[i + 6]) / 3;
            if (Math.abs(pixel1 - pixel2) > 30) edges++;
        }
        
        // Update UI based on quality
        if (brightness < 50) {
            qualityIndicator.textContent = 'Quality: Too Dark';
            qualityIndicator.style.color = '#ff4444';
            scanHint.textContent = 'Move to better lighting';
        } else if (brightness > 200) {
            qualityIndicator.textContent = 'Quality: Too Bright';
            qualityIndicator.style.color = '#ff4444';
            scanHint.textContent = 'Reduce glare';
        } else if (edges < 10000) {
            qualityIndicator.textContent = 'Quality: Blurry';
            qualityIndicator.style.color = '#ffff44';
            scanHint.textContent = 'Hold steady and focus';
        } else {
            qualityIndicator.textContent = 'Quality: Good';
            qualityIndicator.style.color = '#44ff44';
            scanHint.textContent = 'Perfect! Scanning...';
        }
        
        qualityIndicator.style.display = 'block';
        
    }, 1000);
}

// ===================== AUTO SCAN FUNCTIONALITY =====================
function startAutoScan() {
    if (scanInterval) clearInterval(scanInterval);
    
    scanInterval = setInterval(() => {
        if (scanActive && scanAttempt < SCAN_MAX_ATTEMPTS) {
            captureAndProcess();
        } else if (scanAttempt >= SCAN_MAX_ATTEMPTS) {
            clearInterval(scanInterval);
            scanHint.textContent = 'Max scan attempts reached. Try manual capture or upload.';
            scanHint.style.color = '#ff5555';
            scanFrame.classList.add('bad');
            debugLog('Maximum scan attempts reached', 'warning');
        }
    }, 2500); // 2.5 seconds between auto-scans
}

// ===================== CAPTURE AND PROCESS =====================
function captureAndProcess() {
    // Reset camera timeout on any scan activity
    resetCameraTimeout();
    
    if (!scanActive || !video.videoWidth) {
        // debugLog('Scan not active or video not ready', 'warning');
        // return;
    }
    
    // Update UI
    scanAttempt++;
    attemptCount.textContent = scanAttempt;
    scanHint.textContent = 'Scanning...';
    scanHint.style.color = '#ffff55';
    scanFrame.classList.remove('bad');
    scanFrame.classList.add('locked');
    
    debugLog(`Scan attempt ${scanAttempt}/${SCAN_MAX_ATTEMPTS}`, 'info');
    
    // Capture frame
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    ctx.drawImage(video, 0, 0);
    
    // Get scan frame position for cropping
    const frameRect = scanFrame.getBoundingClientRect();
    const containerRect = document.querySelector('.camera-container').getBoundingClientRect();
    
    // Calculate crop coordinates relative to video
    const cropX = (frameRect.left - containerRect.left) / containerRect.width * canvas.width;
    const cropY = (frameRect.top - containerRect.top) / containerRect.height * canvas.height;
    const cropWidth = frameRect.width / containerRect.width * canvas.width;
    const cropHeight = frameRect.height / containerRect.height * canvas.height;
    
    // Create and crop canvas
    const croppedCanvas = document.createElement('canvas');
    croppedCanvas.width = cropWidth;
    croppedCanvas.height = cropHeight;
    const croppedCtx = croppedCanvas.getContext('2d');
    
    croppedCtx.drawImage(canvas, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
    
    // Convert to base64 with quality optimization
    const base64Image = croppedCanvas.toDataURL('image/jpeg', 0.85);
    
    debugLog(`Image captured: ${Math.round(base64Image.length / 1024)}KB`, 'info');
    
    // Submit to API - use scan-specific function
    submitScanToAPI(base64Image);
}

// ===================== SCAN-SPECIFIC API SUBMISSION =====================
function submitScanToAPI(base64Image) {
    scanActive = false;
    
    // Extract base64 data
    const base64Data = base64Image.split(',')[1];
    
    if (!base64Data) {
        debugLog('Invalid base64 image data', 'error');
        handleScanError('Invalid image data');
        return;
    }
    
    debugLog('Submitting scan to API...', 'info');
    
    // Create form data
    const formData = new FormData();
    formData.append('mobicard_scan_card_photo', dataURLtoBlob(base64Image));
    formData.append('mobicard_transaction_access_token', MOBICARD_PARAMS.apiKey);
    formData.append('mobicard_token_id', MOBICARD_PARAMS.tokenId);
    formData.append('action', 'scan');
    
    // AJAX call for scanning
    $.ajax({
        url: window.location.href, // Submit to same page
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        dataType: 'json', // Expect JSON response
        beforeSend: function() {
            debugLog('Scan API request started', 'info');
        },
        success: function(response) {
            debugLog('Scan API response received', 'success');
            
            // Safely log response
            try {
                if (typeof response === 'string') {
                    debugLog(`Response (string): ${response.substring(0, 200)}...`, 'info');
                    response = JSON.parse(response);
                } else if (response && typeof response === 'object') {
                    debugLog(`Response (object): ${JSON.stringify(response).substring(0, 200)}...`, 'info');
                }
            } catch (e) {
                debugLog(`Error parsing response: ${e.message}`, 'error');
                handleScanError('Invalid server response format');
                return;
            }
            
            if (response && response.status === 'SUCCESS' && success_flag === 0) {

                
                // Stop camera and show reload overlay
                stopCamera();
                showReloadOverlay();
				
				success_flag++;
                debugLog('Card scan successful!', 'success');
                populateModal(response);
                
                // Hide loading overlay before showing modal
                loadingOverlay.style.display = 'none';
                
                // Show modal
                $('#resultModal').modal('show');
                
                // Stop camera and show reload overlay
                stopCamera();
                showReloadOverlay();
            } else {
                const errorMsg = response ? (response.status_message || 'Unknown error') : 'No response';
                debugLog(`Scan failed: ${errorMsg}`, 'error');
                handleScanError(errorMsg || 'Failed to read card');
            }
        },
        error: function(xhr, status, error) {
            debugLog(`Scan AJAX error: ${status} - ${error}`, 'error');
            
            // Try to parse response even on error
            if (xhr.responseText) {
                try {
                    const errorResponse = JSON.parse(xhr.responseText);
                    debugLog(`Error response: ${JSON.stringify(errorResponse)}`, 'error');
                } catch (e) {
                    debugLog(`Raw error response: ${xhr.responseText.substring(0, 200)}`, 'error');
                }
            }
            
            handleScanError(`Scan failed: ${error || 'Network error'}`);
        },
        complete: function() {
            debugLog('Scan API request completed', 'info');
        }
    });
}

// ===================== UPLOAD-SPECIFIC API SUBMISSION =====================
function submitUploadToAPI(base64Image) {
    uploadAttempt++;
    
    debugLog(`Upload attempt ${uploadAttempt}/${UPLOAD_MAX_ATTEMPTS}`, 'info');
    
    // Check upload attempt limit
    if (uploadAttempt >= UPLOAD_MAX_ATTEMPTS) {
        debugLog('Maximum upload attempts reached', 'warning');
        handleUploadError('Maximum upload attempts reached. Please try a different image.');
        return;
    }
    
    // Extract base64 data
    const base64Data = base64Image.split(',')[1];
    
    if (!base64Data) {
        debugLog('Invalid base64 image data for upload', 'error');
        handleUploadError('Invalid image data');
        return;
    }
    
    debugLog('Submitting upload to API...', 'info');
    
    // Create form data
    const formData = new FormData();
    formData.append('mobicard_scan_card_photo', dataURLtoBlob(base64Image));
    formData.append('mobicard_transaction_access_token', MOBICARD_PARAMS.apiKey);
    formData.append('mobicard_token_id', MOBICARD_PARAMS.tokenId);
    formData.append('action', 'scan');
    
    // AJAX call for upload
    $.ajax({
        url: window.location.href,
        type: 'POST',
        data: formData,
        processData: false,
        contentType: false,
        dataType: 'json',
        beforeSend: function() {
            debugLog('Upload API request started', 'info');
            loadingText.textContent = `Uploading image (attempt ${uploadAttempt}/${UPLOAD_MAX_ATTEMPTS})...`;
        },
        success: function(response) {
            debugLog('Upload API response received', 'success');
            
            // Safely log response
            try {
                if (typeof response === 'string') {
                    debugLog(`Response (string): ${response.substring(0, 200)}...`, 'info');
                    response = JSON.parse(response);
                } else if (response && typeof response === 'object') {
                    debugLog(`Response (object): ${JSON.stringify(response).substring(0, 200)}...`, 'info');
                }
            } catch (e) {
                debugLog(`Error parsing upload response: ${e.message}`, 'error');
                handleUploadError('Invalid server response format');
                return;
            }
            
            if (response && response.status === 'SUCCESS' && success_flag === 0) {

                
                // Stop camera and show reload overlay
                stopCamera();
                showReloadOverlay();
				
                debugLog('Card upload successful!', 'success');
                populateModal(response);
                
                // Reset upload attempts on success
                uploadAttempt = 0;
                
                // Hide loading overlay before showing modal
                loadingOverlay.style.display = 'none';
                
                // Show modal
                $('#resultModal').modal('show');

                
                // Stop camera and show reload overlay
                stopCamera();
                showReloadOverlay();
                
                // Switch to scan tab after successful upload
                // $('#scanTab').tab('show');
            } else {
                const errorMsg = response ? (response.status_message || 'Unknown error') : 'No response';
                debugLog(`Upload failed: ${errorMsg}`, 'error');
                
                // Show retry option
                handleUploadError(errorMsg || 'Failed to read card', true);
            }
        },
        error: function(xhr, status, error) {
            debugLog(`Upload AJAX error: ${status} - ${error}`, 'error');
            
            let errorMessage = 'Upload failed';
            if (status === 'timeout') {
                errorMessage = 'Upload timeout. Please try again.';
            } else if (xhr.status === 0) {
                errorMessage = 'Network error. Check your connection.';
            } else if (xhr.status === 500) {
                errorMessage = 'Server error. Please try again later.';
            } else {
                errorMessage = `Error ${xhr.status}: ${error}`;
            }
            
            handleUploadError(errorMessage, true);
        },
        complete: function() {
            debugLog('Upload API request completed', 'info');
        }
    });
}

function dataURLtoBlob(dataURL) {
    const arr = dataURL.split(',');
    const mime = arr[0].match(/:(.*?);/)[1];
    const bstr = atob(arr[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    
    while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
    }
    
    return new Blob([u8arr], { type: mime });
}

// ===================== HANDLE SCAN ERROR =====================
function handleScanError(message) {
    loadingOverlay.style.display = 'none';
    scanHint.textContent = message;
    scanHint.style.color = '#ff5555';
    scanFrame.classList.remove('locked');
    scanFrame.classList.add('bad');
    
    debugLog(`Scan error: ${message}`, 'warning');
    
    // Reset after delay if we have attempts left
    if (scanAttempt < SCAN_MAX_ATTEMPTS) {
        setTimeout(() => {
            scanActive = true;
            scanHint.textContent = 'Align card inside the red frame';
            scanHint.style.color = '#0f0';
            scanFrame.classList.remove('bad');
            debugLog('Resuming scanning...', 'info');
        }, 1500);
    } else {
        // Max attempts reached - stop camera and show reload
        stopCamera();
        showReloadOverlay();
    }
}

// ===================== HANDLE UPLOAD ERROR =====================
function handleUploadError(message, showRetry = false) {
    loadingOverlay.style.display = 'none';
    
    if (showRetry && uploadAttempt < UPLOAD_MAX_ATTEMPTS) {
        // Show retry prompt
        if (confirm(`${message}\n\nWould you like to try again? (Attempt ${uploadAttempt}/${UPLOAD_MAX_ATTEMPTS})`)) {
            // User wants to retry - trigger upload again
            const uploadBtn = document.getElementById('uploadBtn');
            uploadBtn.click();
        }
    } else {
        alert(message);
    }
    
    debugLog(`Upload error: ${message}`, 'warning');
}

// ===================== SHOW/HIDE RELOAD OVERLAY =====================
function showReloadOverlay() {
	stopCamera();
    scanActive = false;
    scanHint.textContent = 'Tap refresh to scan again';
    scanHint.style.color = '#0f0';
    scanFrame.classList.remove('locked', 'bad');
    qualityIndicator.style.display = 'none';
    reloadOverlay.style.display = 'flex';
}

function hideReloadOverlay() {
	
    reloadOverlay.style.display = 'none';
    scanActive = true;
    scanHint.textContent = 'Align card inside the red frame';
	success_flag = 0;
    scanAttempt = 0; // Reset scan attempts when reloading
    
    // Restart camera if needed
    if (!cameraStream) {
        initializeCamera();
    } else {
        startAutoScan();
    }
	
}

// Reload icon click handler
reloadIcon.addEventListener('click', function() {
    debugLog('Reload button clicked', 'info');
    resetCameraTimeout();  // Reset timeout on reload
    hideReloadOverlay();
	// success_flag = 0;
    scanAttempt = 0; // Reset scan attempts
    // location.reload();
});

// ===================== POPULATE RESULTS MODAL =====================
function populateModal(data) {
    debugLog('Populating modal with card data', 'info');
    
    try {
        // Card number with formatting
        const cardNum = data.card_information.card_number;
        const formattedNum = formatCardNumber(cardNum);
        
        // Set formatted number
        document.getElementById('cardNumberDisplay').value = formattedNum;
        
        // Set raw number (editable)
        document.getElementById('cardNumberRaw').value = cardNum;
        
        // Expiry
        document.getElementById('expiryDisplay').value = data.card_information.card_expiry_date;
        
        // Brand
        const brand = data.card_information.card_brand;
        document.getElementById('cardBrand').value = brand;
        
        // Bank
        document.getElementById('cardBank').value = data.card_information.card_bank_name || 'Unknown';
        
        // Confidence score
        const confidence = parseFloat(data.card_information.card_confidence_score) * 100;
        document.getElementById('confidenceScore').textContent = confidence.toFixed(1) + '%';
        
        // Validation checks
        const checks = data.card_information.card_validation_checks;
        document.getElementById('luhnCheck').textContent = checks.luhn_algorithm ? 'PASS' : 'FAIL';
        document.getElementById('luhnCheck').className = `status-badge ${checks.luhn_algorithm ? 'status-success' : 'status-danger'}`;
        
        document.getElementById('expiryCheck').textContent = checks.expiry_date ? 'VALID' : 'INVALID';
        document.getElementById('expiryCheck').className = `status-badge ${checks.expiry_date ? 'status-success' : 'status-danger'}`;
        
        // Update status badge
        document.getElementById('statusBadge').textContent = 'SUCCESS';
        
        // Set brand logo
        const logoUrl = getBrandLogoUrl(brand);
        const logoImg = document.getElementById('brandLogo');
        if (logoUrl) {
            logoImg.src = logoUrl;
            logoImg.style.display = 'inline';
        } else {
            logoImg.style.display = 'none';
        }
        
        debugLog('Modal populated successfully', 'success');
        
    } catch (error) {
        debugLog(`Error populating modal: ${error.message}`, 'error');
    }
}

function formatCardNumber(number) {
    // Remove all non-digits
    const clean = number.replace(/\D/g, '');
    
    // Check for American Express (15 digits)
    if (clean.length === 15 && (clean.startsWith('34') || clean.startsWith('37'))) {
        return clean.replace(/(\d{4})(\d{6})(\d{5})/, '$1 $2 $3');
    }
    
    // Standard formatting (16 digits)
    return clean.replace(/(\d{4})/g, '$1  ').trim();
}

function getBrandLogoUrl(brand) {
    const brandLower = brand.toLowerCase();
    
    if (brandLower.includes('visa')) {
        return 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Visa_Inc._logo.svg';
    } else if (brandLower.includes('mastercard')) {
        return 'https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg';
    } else if (brandLower.includes('amex') || brandLower.includes('american')) {
        return 'https://upload.wikimedia.org/wikipedia/commons/3/30/American_Express_logo.svg';
    } else if (brandLower.includes('discover')) {
        return 'https://upload.wikimedia.org/wikipedia/commons/5/5a/Discover_Card_logo.svg';
    }
    
    return '';
}

// ===================== UPLOAD FUNCTIONALITY =====================
document.getElementById('uploadBtn').addEventListener('click', function() {
    
    // Reset upload attempts when starting new upload
    uploadAttempt = 0;
    success_flag = 0;

    stopCamera();

    const fileInput = document.getElementById('uploadInput');
    
    if (!fileInput.files || !fileInput.files[0]) {
        alert('Please select an image file first');
        return;
    }
    
    const file = fileInput.files[0];
    
    // Validate file type
    if (!file.type.match('image.*')) {
        alert('Please select an image file (JPEG, PNG, etc.)');
        return;
    }
    
    // Validate file size (max 5MB)
    if (file.size > 5 * 1024 * 1024) {
        alert('Image size should be less than 5MB');
        return;
    }
    
    debugLog(`Uploading file: ${file.name} (${Math.round(file.size / 1024)}KB)`, 'info');
    
    const reader = new FileReader();
    
    loadingOverlay.style.display = 'flex';
    loadingText.textContent = 'Uploading image...';
    
    reader.onload = function(e) {
        debugLog('File read successfully', 'success');
        submitUploadToAPI(e.target.result);
    };
    
    reader.onerror = function() {
        debugLog('File read error', 'error');
        alert('Error reading file. Please try again.');
        loadingOverlay.style.display = 'none';
    };
    
    reader.readAsDataURL(file);
});

// ===================== EVENT LISTENERS =====================
document.getElementById('captureBtn').addEventListener('click', function() {
    debugLog('Manual capture triggered', 'info');
    resetCameraTimeout();  // Reset timeout on manual capture
    captureAndProcess();
});

// Tab switching - UPDATED WITH TIMEOUT HANDLING
$('#uploadTab').on('click', function() {
    debugLog('Switching to upload tab', 'info');
    
    
    stopCamera();
    scanActive = false;
    
    showReloadOverlay();
	
    const timeoutOverlay = document.getElementById('timeoutOverlay');
    if (timeoutOverlay) {
        timeoutOverlay.style.display = 'none';
    }
    
    // Reset upload UI when switching to upload tab
    document.getElementById('uploadInput').value = '';
    
    debugLog('Camera stopped for upload tab', 'info');
});

$('#scanTab').on('click', function() {
    debugLog('Switching to scan tab', 'info');
    
    
    stopCamera();
    scanActive = false;
    
    showReloadOverlay();
	
	
});

// Proceed button
document.getElementById('proceedBtn').addEventListener('click', function() {
    const cvv = document.getElementById('cvvInput').value;
    const cardNumberRaw = document.getElementById('cardNumberRaw').value;
    const cardNumberDisplay = document.getElementById('cardNumberDisplay').value;
    const expiry = document.getElementById('expiryDisplay').value;
    
    // Validate raw card number
    if (!cardNumberRaw || cardNumberRaw.replace(/\D/g, '').length < 13) {
        alert('Please enter a valid raw card number (minimum 13 digits)');
        document.getElementById('cardNumberRaw').focus();
        return;
    }
    
    // Validate expiry format (MM/YY)
    const expiryRegex = /^(0[1-9]|1[0-2])\/([0-9]{2})$/;
    if (!expiryRegex.test(expiry)) {
        alert('Please enter expiry date in MM/YY format');
        document.getElementById('expiryDisplay').focus();
        return;
    }
    
    if (cvv && (cvv.length < 3 || cvv.length > 4)) {
        alert('CVV must be 3 or 4 digits');
        document.getElementById('cvvInput').focus();
        return;
    }
    
    const cardData = {
        number_raw: cardNumberRaw,
        number_formatted: cardNumberDisplay,
        expiry: expiry,
        cvv: cvv || '',
        brand: document.getElementById('cardBrand').value
    };
    
    debugLog('Proceeding with card data:', 'success', cardData);
    
    // Here you would typically submit to your payment processor
    alert('Payment processing would start here with card data:\n\n' + 
          JSON.stringify(cardData, null, 2));
    
    $('#resultModal').modal('hide');
});

// ===================== CLEANUP FUNCTIONS =====================
function stopCamera() {
    // Clear camera timeout
    if (cameraTimeout) {
        clearTimeout(cameraTimeout);
        cameraTimeout = null;
        debugLog('Camera timeout cleared', 'info');
    }
    
    if (cameraStream) {
        cameraStream.getTracks().forEach(track => {
            track.stop();
            debugLog('Camera track stopped', 'info');
        });
        cameraStream = null;
    }
    
    if (scanInterval) {
        clearInterval(scanInterval);
        scanInterval = null;
    }
    
    if (qualityCheckInterval) {
        clearInterval(qualityCheckInterval);
        qualityCheckInterval = null;
    }
    
    debugLog('Camera and intervals stopped', 'info');
}

// ===================== INITIALIZATION =====================
document.addEventListener('DOMContentLoaded', function() {
    debugLog('Page loaded', 'success');
    debugLog(`Debug mode: ${DEBUG_MODE}`, 'info');
    debugLog(`API URL: ${MOBICARD_PARAMS.apiUrl}`, 'info');
    
    // Setup activity detection
    setupActivityDetection();
    
    // Initialize camera immediately (simple approach like your file)
    initializeCamera();
    
    // Modal close handler
    $('#resultModal').on('hidden.bs.modal', function() {

		debugLog('Closing Modal', 'info');
		
		// Clear camera timeout
		if (cameraTimeout) {
			clearTimeout(cameraTimeout);
			cameraTimeout = null;
		}
		
		stopCamera();
		scanActive = false;
		
		// Show overlays
		showReloadOverlay();
		const timeoutOverlay = document.getElementById('timeoutOverlay');
		if (timeoutOverlay) {
			timeoutOverlay.style.display = 'none';
		}
		
		// Reset upload UI when switching to upload tab
		document.getElementById('uploadInput').value = '';
		
		debugLog('Camera stopped for upload tab', 'info');
		
    });
    
    // Handle page visibility changes
    document.addEventListener('visibilitychange', function() {
        if (document.hidden) {
            debugLog('Page hidden, pausing scanner', 'warning');
            scanActive = false;
            if (scanInterval) clearInterval(scanInterval);
        } else {
            debugLog('Page visible, resuming scanner', 'info');
            scanActive = true;
            startAutoScan();
        }
    });
});

// Card number formatting for editable fields
document.getElementById('cardNumberDisplay').addEventListener('input', function(e) {
    let value = e.target.value.replace(/\D/g, '');
    
    // Format as user types
    if (value.length === 15 && (value.startsWith('34') || value.startsWith('37'))) {
        // Amex format
        value = value.replace(/(\d{4})(\d{6})(\d{5})/, '$1 $2 $3');
    } else {
        // Standard format
        value = value.replace(/(\d{4})/g, '$1 ').trim();
    }
    
    e.target.value = value;
    
    // Also update raw field if it's empty or matches
    const rawField = document.getElementById('cardNumberRaw');
    if (!rawField.value || rawField.value.replace(/\D/g, '') === e.target.value.replace(/\D/g, '')) {
        rawField.value = e.target.value.replace(/\D/g, '');
    }
});

// Raw card number validation
document.getElementById('cardNumberRaw').addEventListener('input', function(e) {
    let value = e.target.value.replace(/\D/g, '');
    e.target.value = value;
    
    // Update formatted display
    const displayField = document.getElementById('cardNumberDisplay');
    if (value.length === 15 && (value.startsWith('34') || value.startsWith('37'))) {
        displayField.value = value.replace(/(\d{4})(\d{6})(\d{5})/, '$1 $2 $3');
    } else {
        displayField.value = value.replace(/(\d{4})/g, '$1 ').trim();
    }
});

// Expiry date formatting
document.getElementById('expiryDisplay').addEventListener('input', function(e) {
    let value = e.target.value.replace(/\D/g, '');
    
    if (value.length >= 2) {
        value = value.substring(0, 2) + '/' + value.substring(2, 4);
    }
    
    e.target.value = value;
});
</script>

</body>
</html>

Success Response Format

Both methods return the same success response format when card scanning is successful.

JSON Success Response

{
    "status": "SUCCESS",
    "status_code": "200",
    "status_message": "SUCCESS",
    "card_scan_request_id": "18678121768809362",
    "mobicard_txn_reference": "327005622",	
	"mobicard_token_id": "325026456",
    "timestamp": "2026-01-19 07:56:02",
    "card_information": {
        "card_number": "5173350006475601",
        "card_number_masked": "5173********5601",
        "card_expiry_date": "12/19",
        "card_expiry_month": "12",
        "card_expiry_year": "19",
        "card_brand": "MASTERCARD",
        "card_category": "PREPAID",
        "card_holder_name": "JOHN DOE",
        "card_bank_name": "KCB BANK KENYA LIMITED",
        "card_confidence_score": "0.7700",
        "card_validation_checks": {
            "luhn_algorithm": true,
            "brand_prefix": false,
            "expiry_date": false
        },
        "card_token": "e39765d989fe7b1a5f45d68645364faf923d5c36125bb5ea2b6b4dbcbca6424d845d545955ff3cee4e71748252833c4049d0b6e7bef00511ab7a320f09eba7026"
    },
    "card_exif_information": {
        "card_exif_flag": 1,
        "card_exif_tamper_flag": 0,
        "card_exif_is_instant_photo_flag": 1,
        "card_exif_original_timestamp": "",
        "card_exif_file_datetime": "2026-01-19 07:56:02",
        "card_exif_file_datetime_digitized": "",
        "card_exif_device_model": "",
        "card_exif_device_make": ""
    },
    "card_risk_information": {
        "card_possible_screenshot_flag": 1,
        "card_possible_edited_flag": 0,
        "card_reencode_suspected_flag": 0,
        "card_deepfake_risk_flag": 0,
        "card_risk_score": "0.0900"
    },
    "card_biin_information": {
        "card_biin_flag": 1,
        "card_biin_number": "51733500",
        "card_biin_scheme": "MASTERCARD",
        "card_biin_prefix": "",
        "card_biin_type": "PREPAID",
        "card_biin_brand": "Mastercard Prepaid General Spend",
        "card_biin_prepaid": "Yes",
        "card_biin_bank_name": "KCB BANK KENYA LIMITED",
        "card_biin_bank_url": "",
        "card_biin_bank_city": "",
        "card_biin_bank_phone": "",
        "card_biin_bank_logo": "",
        "card_biin_country_two_letter_code": null,
        "card_biin_country_name": "KENYA",
        "card_biin_country_numeric": "404",
        "card_biin_risk_flag": 0
    },
    "addendum_data": "your_custom_data_here_will_be_returned_as_is in the mobicard_extra_data field"
}
        
Response Fields Explanation:
  • card_validation_checks: Validates card number (Luhn algorithm), brand prefix, and expiry date
  • card_confidence_score: 0-1 score indicating OCR accuracy (X 100 for percentage)
  • card_risk_information: Fraud detection metrics including screenshot detection. Please note that the values provided serve as possibilities.
  • card_biin_information: Bank Identification Number details including bank name and country
  • card_exif_information: Image metadata for forensic analysis
  • card_token: Reduce your compliance burden (scope) by storing this token along with the masked card number, in place of actual card data. The token can be used in our Tokenization API (for detokenization) to gain access the actual card info from the vault whenever the card needs to be used.
IMPORTANT SECURITY NOTICE
Server-Side Validation
For editability reasons, the raw card data in the code samples above is sent back to the client side in the rare case that the end user might want to correct any OCR errors in the card information.
For enhanced security, you may need to validate the card information by performing a server-to-server Detokenization API request using the card_token value provided in the Scan Card API Success response to retrieve the card information directly as well as validate the fields below against the corresponding original Scan Card API request details as shown below (Check for a match for mobicard_txn_reference and mobicard_token_id):
  • mobicard_custom_one: Corresponds to the original scan card API request parameter mobicard_txn_reference
  • mobicard_custom_two: Corresponds to the original scan card API request parameter mobicard_token_id

  • mobicard_custom_three: Corresponds to the original scan card API response parameter card_scan_request_id
  • addendum_data: Corresponds to the original scan card API response parameter timestamp

Error Response Format

Error responses have a simplified format with only 3 fields of essential information as shown below.

Use the "status" field to determine if any API request is successful of not and to determine which subsequent fields can be retrieved. The value for the "status" response parameter is always either "SUCCESS" or "FAILED" for this API.

JSON Error Response

{
    "status": "FAILED",
    "status_code": "400",
    "status_message": "BAD REQUEST"
}
        

Status Codes Reference

Complete list of status codes returned by the API.

Status Code Status Status Message Interpretation Action Required
200 SUCCESS SUCCESS Process the card data
400 FAILED BAD REQUEST - Invalid parameters or malformed request Check request parameters
429 FAILED TOO MANY REQUESTS - Rate limit exceeded Wait before making more requests
250 FAILED INSUFFICIENT TOKENS - Token account balance insufficient Top up your account
500 FAILED UNAVAILABLE - Server error Try again later or contact support
430 FAILED TIMEOUT - Request timed out or Finalized Issue new token/Refresh and retry

API Request Parameters Reference

Complete reference of all request parameters used in the API.

Parameter Required Description Example Value
mobicard_version Yes API version "2.0"
mobicard_mode Yes Environment mode "TEST" or "LIVE"
mobicard_merchant_id Yes Your merchant ID ""
mobicard_api_key Yes Your API key ""
mobicard_secret_key Yes Your secret key ""
mobicard_service_id Yes Scan Card service ID "20000"
mobicard_service_type Yes Method type (1 for UI, 2 for Base64) "1" or "2"
mobicard_token_id Yes Unique token identifier String/number
mobicard_txn_reference Yes Your transaction reference String/number
mobicard_scan_card_photo_base64_string Method 2 only Base64 encoded card image "/9j/4AAQSkZJRgABAQ..."
mobicard_extra_data No Custom data returned in response Any string

API Response Parameters Reference

Complete reference of all response parameters returned by the API.

The value for the "status" response parameter is always either "SUCCESS" or "FAILED" for this API. Use this to determine subsequent actions.

All flags are always returned and as either 0 or 1

Parameter Always Returned Description Example Value
status Yes Transaction status "SUCCESS" or "FAILED"
status_code Yes HTTP status code "200"
status_message Yes Status description "SUCCESS"
card_scan_request_id Yes Unique scan request identifier "18678121768809362"
mobicard_txn_reference Yes Your original transaction reference "327005622"
mobicard_token_id Yes Your unique API request id "325026456"
timestamp Yes Response timestamp "2026-01-19 07:56:02"
card_information.card_number Yes Full card number "5173350006475601"
card_information.card_number_masked Yes Masked card number (for display) "5173********5601"
card_information.card_expiry_date Yes Card expiry in MM/YY format "12/19"
card_information.card_expiry_month Yes Expiry month (2 digits) "12"
card_information.card_expiry_year Yes Expiry year (2 digits) "19"
card_information.card_brand Yes Card brand/scheme "MASTERCARD"
card_information.card_category Yes (defaults to "") Card category/type "PREPAID"
card_information.card_holder_name Yes (defaults to "") Card-holder name "JOHN DOE"
card_information.card_bank_name Yes (defaults to "") Issuing bank name "KCB BANK KENYA LIMITED"
card_information.card_confidence_score Yes OCR confidence score (0.0-1.0) "0.7700" (77%)
card_information.card_validation_checks.luhn_algorithm Yes Luhn algorithm validation result true
card_information.card_validation_checks.brand_prefix Yes Brand prefix validation result false
card_information.card_validation_checks.expiry_date Yes Expiry date validation result false
card_information.card_token Yes Hashed card token for reference "e39765d989fe7b1a5f45d68645364faf..."
card_exif_information.card_exif_flag Yes EXIF data availability flag 1
card_exif_information.card_exif_tamper_flag Yes EXIF data tamper flag 0
card_exif_information.card_exif_is_instant_photo_flag Yes Instant photo detection flag 1
card_exif_information.card_exif_original_timestamp Yes (defaults to "") Original photo timestamp ""
card_exif_information.card_exif_file_datetime Yes (defaults to "") File datetime from EXIF "2026-01-19 07:56:02"
card_exif_information.card_exif_file_datetime_digitized Yes (defaults to "") Digitized datetime from EXIF ""
card_exif_information.card_exif_device_model Yes (defaults to "") Camera device model ""
card_exif_information.card_exif_device_make Yes (defaults to "") Camera device manufacturer ""
card_risk_information.card_possible_screenshot_flag Yes Screenshot detection flag 1
card_risk_information.card_possible_edited_flag Yes Image editing detection flag 0
card_risk_information.card_reencode_suspected_flag Yes Re-encoding suspicion flag 0
card_risk_information.card_deepfake_risk_flag Yes Deepfake detection flag 0
card_risk_information.card_risk_score Yes Overall risk score (0.0-1.0) "0.0900" (9%)
card_biin_information.card_biin_flag Yes BIIN data availability flag 1
card_biin_information.card_biin_number Yes (defaults to "") Bank Identification Number "51733500"
card_biin_information.card_biin_scheme Yes (defaults to "") Card scheme from BIIN "MASTERCARD"
card_biin_information.card_biin_prefix Yes (defaults to "") Card prefix ""
card_biin_information.card_biin_type Yes (defaults to "") Card type from BIIN "PREPAID"
card_biin_information.card_biin_brand Yes (defaults to "") Card brand description "Mastercard Prepaid General Spend"
card_biin_information.card_biin_prepaid Yes (defaults to "") Prepaid card indicator "Yes"
card_biin_information.card_biin_bank_name Yes (defaults to "") Issuing bank name from BIIN "KCB BANK KENYA LIMITED"
card_biin_information.card_biin_bank_url Yes (defaults to "") Bank website URL ""
card_biin_information.card_biin_bank_city Yes (defaults to "") Bank city ""
card_biin_information.card_biin_bank_phone Yes (defaults to "") Bank phone number ""
card_biin_information.card_biin_bank_logo Yes (defaults to "") Bank logo URL ""
card_biin_information.card_biin_country_two_letter_code Yes (defaults to "") Country ISO code null
card_biin_information.card_biin_country_name Yes (defaults to "") Country name "KENYA"
card_biin_information.card_biin_country_numeric Yes (defaults to "") Country numeric code "404"
card_biin_information.card_biin_risk_flag Yes Fraud Control (Chargebacks). Turns on for high risk BIINs. 0
addendum_data Only when sent in request Custom data echoed back from request "your_custom_data_here_will_be_returned_as_is"

Best Practices & Tips

Image Quality Guidelines:
  • Use well-lit conditions for scanning
  • Ensure card is flat and not curved
  • Avoid glare and reflections
  • Position card completely within frame
  • Minimum image resolution: 800x600 pixels
  • Supported formats: jpg, png, gif, bmp, webp, tiff, ico, ico, heic, heif, jp2, jpx, jpm, pdf
Security Considerations:
  • Never store raw card images
  • You may store the masked card number along with the tokenized card info (card_token) and use it in the Tokenization API (for detokenization) when the card details need to be used from the vault.
  • Store your 'api_key' and 'secret_key' in your .env file and do not expose it publicly.
  • Implement proper PCI DSS compliance
  • Use HTTPS for all API calls
  • Validate all responses on server-side
  • Implement rate limiting on your end
  • Log all scan attempts for audit
  • Turn all DEBUG_MODE to 'false' in Production Environment
Performance Tips:
  • Implement client-side validation first
  • Cache successful token responses
  • Implement retry logic with exponential backoff
  • Monitor card_confidence_score for quality control
  • Use card_risk_information.card_risk_score for fraud detection
  • Use card_exif_information.card_exif_is_instant_photo_flag for fraud control
  • Use card_exif_information.card_exif_tamper_flag for fraud control
  • Handle error status codes appropriately especially 250 and 430 for smoother flow