DirhamClub
DirhamClub / API reference

API Reference

REST-style JSON APIs, widget session tokens, and iframe embedding for the DirhamClub wallet in your web application.

Introduction

# DirhamClub Wallet Integration Guide

This guide explains how to integrate the DirhamClub centralized wallet into **any web application**. It is **not** tied to a specific host product: you register a client app, store each end user’s dirhamUserId in your own user model, call DirhamClub’s HTTP APIs from your backend, and optionally embed the hosted wallet UI in an iframe.

DirhamClub is a multi-tenant wallet platform: balances, ledger transactions, and Stripe top-ups live in DirhamClub; your app orchestrates identity, linking, and business rules.

---

Table of Contents

---

Overview

DirhamClub provides:

  • **Centralized wallet management**: All wallet balances and transactions are stored in DirhamClub
  • **Stripe integration**: Secure payment processing for wallet top-ups
  • **Widget-based UI**: Embedded wallet interface via iframe
  • **Server-to-server APIs**: Backend operations for wallet transactions
  • **User account linking**: Connect your app's users to DirhamClub accounts

Key Concepts

  • **dirhamUserId**: Identifier linking your app's user to a DirhamClub user (not unique - one DirhamClub account can be linked to multiple app users)
  • **Widget Session Token**: Short-lived token (5 minutes) for secure iframe embedding
  • **Client App**: Your application registered in DirhamClub with credentials for API access

---

Architecture

code
┌─────────────────┐
│  Your App       │
│  (Frontend)     │
│                 │
│  ┌───────────┐  │
│  │ DirhamClub│  │
│  │  Widget   │  │◄──────┐
│  │  (iframe) │  │       │
│  └───────────┘  │       │
│       │         │       │
└───────┼─────────┘       │
        │                 │
        │ HTTP            │ postMessage
        │                 │
┌───────┼─────────┐       │
│  Your App       │       │
│  (Backend)      │       │
│                 │       │
│  ┌───────────┐  │       │
│  │ DirhamClub│  │◄──────┘
│  │  Service  │  │
│  └───────────┘  │
└───────┼─────────┘
        │
        │ HTTPS
        │
┌───────┼─────────┐
│  DirhamClub     │
│  (Next.js App)  │
│                 │
│  • Widget APIs  │
│  • Server APIs  │
│  • Hosted auth  │
│    (in widget)  │
└─────────────────┘

---

Prerequisites

  • **DirhamClub Application Running**
  • DirhamClub must be deployed and accessible
  • Default: http://localhost:3000 (development)
  • **Client App Registration in DirhamClub**
  • Register your app in DirhamClub admin panel (/admin/client-apps)
  • Obtain clientId and clientSecret
  • Configure allowedOrigins (your frontend URL)
  • Configure allowedIframeParents (your frontend URL)
  • Set scopes: wallet:read, wallet:write
  • **User Model with dirhamUserId Field**
  • Your user model must include a dirhamUserId field (String, optional, sparse - NOT unique, to allow one DirhamClub account for multiple users)

---

DirhamClub HTTP API reference

Paths are relative to your DirhamClub base URL (e.g. https://wallet.example.com).

For a normal integration you only use **(A) server-to-server wallet APIs** from your backend and **(B) widget session + widget data/top-up APIs** (session created on your server; browser talks to DirhamClub with ?session=). End-user signup/login runs **inside the iframe** (or DirhamClub pages), not via extra REST calls from your app—so auth, profile, admin, and JWT-wallet routes are omitted here.

**Client app setup:** Create the app and copy clientId / clientSecret in the DirhamClub admin UI (/admin/client-apps). No need to call admin HTTP APIs for integration.

---

A. Server-to-server wallet (your backend only)

Authenticate with **clientId** + **clientSecret**. Never send the secret to the browser.

**GET (query string)**

| Method | Path | Query parameters | |--------|------|------------------| | GET | /api/server/wallet | clientId, clientSecret, dirhamUserId | | GET | /api/server/wallet/check-balance | clientId, clientSecret, dirhamUserId, requiredAmount |

**GET /api/server/wallet response:** { "balance", "transactions", "isActive" } **GET check-balance response:** { "balance", "requiredAmount", "hasSufficientBalance" } User unknown in DirhamClub → 401, often with requiresAuth / code: USER_NOT_FOUND.

**POST (JSON body)**

| Method | Path | Body | |--------|------|------| | POST | /api/server/wallet/debit | clientId, clientSecret, dirhamUserId, amount, referenceType, referenceId, optional description | | POST | /api/server/wallet/credit | Same fields as debit |

**Success:** { "balance", "message" }. **Errors:** 401 bad client credentials; 404 user not found; 400 insufficient balance (debit).

Debit/credit are **idempotent** on the pair (referenceType, referenceId).

---

B. Widget (iframe)

**1. Create session — call from your server**

| Method | Path | Body (JSON) | |--------|------|-------------| | POST | /api/widget/session | clientId, clientSecret, dirhamUserId, optional parentOrigin |

**Response:** { "sessionToken", "expiresAt" } (short-lived, typically ~5 minutes).

**2. Calls from the browser (widget uses these; your iframe loads hosted pages below)**

| Method | Path | Query / body | |--------|------|----------------| | GET | /api/widget/wallet | ?session=<token> | | POST | /api/widget/topup/create-intent | ?session=<token>, body { "amount": number } | | POST | /api/widget/topup/confirm | ?session=<token>, body { "paymentIntentId": string } |

**GET /api/widget/wallet:** balance, last **10** transactions, isActive, user: { firstName, lastName }.

**Iframe URLs (src)**

  • /widget/wallet?session=<token> — omit session to show embedded login/signup first
  • /widget/topup?session=<token>

---

postMessage (widget ↔ your page)

Listen on window for message; in production, verify event.origin is your DirhamClub host.

| event.data.type | Meaning | |-------------------|--------| | DIRHAM_WALLET_UPDATED | Refresh UI / reload iframe | | DIRHAM_WALLET_LOADED | Widget ready | | DIRHAM_OPEN_TOPUP | Open top-up (e.g. new window with /widget/topup?session=...) | | DIRHAM_AUTH_REQUIRED | User must complete DirhamClub auth | | DIRHAM_AUTH_SUCCESS | Includes dirhamUserId — persist on your user, then create a new widget session | | DIRHAM_LOGOUT_REQUEST | Clear link / reload iframe without session if switching accounts |

---

Backend Integration

1. Environment Variables

Add to your .env file:

env
# DirhamClub Configuration
DIRHAMCLUB_BASE_URL=http://localhost:3000
DIRHAMCLUB_CLIENT_ID=your_client_id_here
DIRHAMCLUB_CLIENT_SECRET=your_client_secret_here

2. User Model Update

Add dirhamUserId field to your User schema:

javascript
// Example: MongoDB/Mongoose
const userSchema = new Schema({
  // ... existing fields
  dirhamUserId: {
    type: String,
    default: null,
    sparse: true  // NOT unique - allows one DirhamClub account to be linked to multiple app users
  }
});

**Important**: The dirhamUserId field should NOT have a unique: true constraint if you want several of your app’s users to share one DirhamClub wallet (e.g. family or team accounts). If you require strictly one app user per DirhamClub user, you may enforce uniqueness in your own schema instead.

**Migration Note**: If you have an existing unique index on dirhamUserId, you need to drop it. See the migration section below.

3. DirhamClub Service

Create a service file to communicate with DirhamClub APIs:

**File: services/dirhamclub_service/dirhamclub.service.js**

javascript
const axios = require('axios');

const DIRHAMCLUB_BASE_URL = process.env.DIRHAMCLUB_BASE_URL || 'http://localhost:3000';
const DIRHAMCLUB_CLIENT_ID = process.env.DIRHAMCLUB_CLIENT_ID;
const DIRHAMCLUB_CLIENT_SECRET = process.env.DIRHAMCLUB_CLIENT_SECRET;

/**
 * Create widget session token for iframe embedding
 */
async function createWidgetSession(dirhamUserId, parentOrigin) {
  try {
    if (!DIRHAMCLUB_CLIENT_ID || !DIRHAMCLUB_CLIENT_SECRET) {
      throw new Error('DirhamClub client credentials not configured');
    }

    const response = await axios.post(
      `${DIRHAMCLUB_BASE_URL}/api/widget/session`,
      {
        clientId: DIRHAMCLUB_CLIENT_ID,
        clientSecret: DIRHAMCLUB_CLIENT_SECRET,
        dirhamUserId,
        parentOrigin,
      },
      {
        headers: { 'Content-Type': 'application/json' },
        timeout: 10000,
      }
    );

    return response.data;
  } catch (error) {
    if (error.response) {
      const errorData = error.response.data || {};
      const apiError = new Error(errorData.error || 'DirhamClub API error');
      if (errorData.requiresAuth) {
        apiError.requiresAuth = true;
      }
      if (errorData.code) {
        apiError.code = errorData.code;
      }
      throw apiError;
    }
    throw error;
  }
}

/**
 * Get wallet from DirhamClub (server-to-server)
 */
async function getWallet(dirhamUserId) {
  try {
    const response = await axios.get(
      `${DIRHAMCLUB_BASE_URL}/api/server/wallet`,
      {
        params: {
          clientId: DIRHAMCLUB_CLIENT_ID,
          clientSecret: DIRHAMCLUB_CLIENT_SECRET,
          dirhamUserId,
        },
        timeout: 10000,
      }
    );
    return response.data;
  } catch (error) {
    // Handle errors (preserve requiresAuth flag)
    if (error.response) {
      const errorData = error.response.data || {};
      const apiError = new Error(errorData.error || 'DirhamClub API error');
      if (errorData.requiresAuth) {
        apiError.requiresAuth = true;
      }
      throw apiError;
    }
    throw error;
  }
}

/**
 * Check wallet balance (server-to-server)
 */
async function checkWalletBalance(dirhamUserId, requiredAmount) {
  try {
    const response = await axios.get(
      `${DIRHAMCLUB_BASE_URL}/api/server/wallet/check-balance`,
      {
        params: {
          clientId: DIRHAMCLUB_CLIENT_ID,
          clientSecret: DIRHAMCLUB_CLIENT_SECRET,
          dirhamUserId,
          requiredAmount,
        },
        timeout: 10000,
      }
    );
    return response.data;
  } catch (error) {
    if (error.response) {
      const errorData = error.response.data || {};
      const apiError = new Error(errorData.error || 'DirhamClub API error');
      if (errorData.requiresAuth) {
        apiError.requiresAuth = true;
      }
      throw apiError;
    }
    throw error;
  }
}

/**
 * Debit wallet (server-to-server)
 */
async function debitWallet(dirhamUserId, amount, referenceType, referenceId, description) {
  try {
    const response = await axios.post(
      `${DIRHAMCLUB_BASE_URL}/api/server/wallet/debit`,
      {
        clientId: DIRHAMCLUB_CLIENT_ID,
        clientSecret: DIRHAMCLUB_CLIENT_SECRET,
        dirhamUserId,
        amount,
        referenceType,
        referenceId,
        description,
      },
      {
        headers: { 'Content-Type': 'application/json' },
        timeout: 10000,
      }
    );
    return response.data;
  } catch (error) {
    if (error.response) {
      const errorData = error.response.data || {};
      const apiError = new Error(errorData.error || 'DirhamClub API error');
      if (errorData.requiresAuth) {
        apiError.requiresAuth = true;
      }
      throw apiError;
    }
    throw error;
  }
}

/**
 * Credit wallet (server-to-server)
 */
async function creditWallet(dirhamUserId, amount, referenceType, referenceId, description) {
  try {
    const response = await axios.post(
      `${DIRHAMCLUB_BASE_URL}/api/server/wallet/credit`,
      {
        clientId: DIRHAMCLUB_CLIENT_ID,
        clientSecret: DIRHAMCLUB_CLIENT_SECRET,
        dirhamUserId,
        amount,
        referenceType,
        referenceId,
        description,
      },
      {
        headers: { 'Content-Type': 'application/json' },
        timeout: 10000,
      }
    );
    return response.data;
  } catch (error) {
    if (error.response) {
      const errorData = error.response.data || {};
      const apiError = new Error(errorData.error || 'DirhamClub API error');
      if (errorData.requiresAuth) {
        apiError.requiresAuth = true;
      }
      throw apiError;
    }
    throw error;
  }
}

module.exports = {
  createWidgetSession,
  getWallet,
  checkWalletBalance,
  debitWallet,
  creditWallet,
};

4. Backend routes (bridge to DirhamClub)

Expose **only** what your frontend needs. Paths are yours to choose; keep them behind your own session/auth.

Typical endpoints:

| Your route (example) | DirhamClub call | Purpose | |---------------------|-----------------|---------| | POST /api/integrations/dirhamclub/widget-session | POST /api/widget/session with clientId, clientSecret, dirhamUserId, parentOrigin | Short-lived iframe token | | POST /api/integrations/dirhamclub/link-account | *(none — update your DB)* | Persist dirhamUserId after widget DIRHAM_AUTH_SUCCESS | | POST /api/integrations/dirhamclub/unlink-account | *(none — update your DB)* | Clear link when user switches DirhamClub accounts |

**Widget session handler (Express-style sketch)**

javascript
// POST /api/integrations/dirhamclub/widget-session
router.post('/widget-session', authenticateUser, async (req, res) => {
  let parentOrigin;
  try {
    parentOrigin =
      req.headers.origin ||
      (req.headers.referer ? new URL(req.headers.referer).origin : undefined);
  } catch {
    parentOrigin = undefined;
  }

  const appUser = await loadUser(req.user.id); // your ORM / DB
  if (!appUser?.dirhamUserId) {
    return res.status(400).json({
      error: 'DirhamClub account not linked',
      requiresAuth: true,
    });
  }

  try {
    const session = await createWidgetSession(appUser.dirhamUserId, parentOrigin);
    return res.json(session);
  } catch (error) {
    const status = error.requiresAuth || error.code === 'USER_NOT_FOUND' ? 400 : 500;
    return res.status(status).json({
      error: error.message || 'Failed to create widget session',
      requiresAuth: !!error.requiresAuth,
      code: error.code,
    });
  }
});

**Link / unlink (sketch)**

javascript
// POST /api/integrations/dirhamclub/link-account  body: { dirhamUserId }
router.post('/link-account', authenticateUser, async (req, res) => {
  const { dirhamUserId } = req.body;
  if (!dirhamUserId) return res.status(400).json({ error: 'dirhamUserId is required' });

  let user = await loadUser(req.user.id);
  if (!user) return res.status(404).json({ error: 'User not found' });

  if (user.dirhamUserId && user.dirhamUserId !== dirhamUserId) {
    await clearDirhamLink(req.user.id); // e.g. MongoDB $unset for sparse indexes
    user = await loadUser(req.user.id);
  }

  user.dirhamUserId = dirhamUserId;
  await saveUser(user);
  return res.json({ success: true, dirhamUserId });
});

// POST /api/integrations/dirhamclub/unlink-account
router.post('/unlink-account', authenticateUser, async (req, res) => {
  const user = await loadUser(req.user.id);
  const prev = user?.dirhamUserId;
  await clearDirhamLink(req.user.id);
  return res.json({ success: true, unlinkedDirhamUserId: prev });
});

Use **let** (not const) for user if you reassign after refetch. Prefer **removing** the field ($unset in MongoDB) over setting null when using sparse unique indexes.

5. Using server APIs from your domain logic

Where you previously debited a local wallet, call **POST /api/server/wallet/debit** (via your dirhamclub.service helpers) with:

  • A stable **referenceType** for your domain (ORDER_PAYMENT, BOOKING_HOLD, etc.)
  • A **unique referenceId per business attempt** (see Idempotency)

Refunds or goodwill credits use **POST /api/server/wallet/credit** with their own referenceType / referenceId pair.

Read-only flows: **GET /api/server/wallet** and **GET /api/server/wallet/check-balance**.

---

Frontend Integration

The following is a **reference pattern** (React + Vite-style). Adapt fetch helpers, routing, and styling to your stack. The important parts are: (1) never put clientSecret in the browser, (2) load the iframe from your DirhamClub base URL with a session from your backend, (3) handle postMessage from the widget.

1. Environment variables

env
# DirhamClub deployment (iframe src + postMessage origin checks)
VITE_DIRHAMCLUB_URL=https://your-dirhamclub-host

# Your own backend base URL (must expose widget-session + link/unlink)
VITE_YOUR_API_BASE_URL=https://api.yourapp.com

2. API constants (your backend, not DirhamClub)

Point these at the bridge routes you implemented under Backend integration:

javascript
const API = import.meta.env.VITE_YOUR_API_BASE_URL;

export const DIRHAM_WIDGET_SESSION_URL = `${API}/api/integrations/dirhamclub/widget-session`;
export const DIRHAM_LINK_ACCOUNT_URL = `${API}/api/integrations/dirhamclub/link-account`;
export const DIRHAM_UNLINK_ACCOUNT_URL = `${API}/api/integrations/dirhamclub/unlink-account`;

3. Example: wallet modal + iframe

**Example file:** components/DirhamWalletModal.jsx (name as you like)

javascript
import React, { useState, useEffect, useRef } from 'react';
import { MdClose } from 'react-icons/md';
import styles from './DirhamWalletModal.module.css';
import doApi from '../../utils/doApi';
import {
  DIRHAM_WIDGET_SESSION_URL,
  DIRHAM_LINK_ACCOUNT_URL,
  DIRHAM_UNLINK_ACCOUNT_URL,
} from '../../config/dirhamEndpoints';
import toast from 'react-hot-toast';

const DIRHAMCLUB_BASE_URL = import.meta.env.VITE_DIRHAMCLUB_URL || 'http://localhost:3000';

const DirhamWalletModal = ({ isOpen, onClose }) => {
  const [sessionToken, setSessionToken] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const iframeRef = useRef(null);

  useEffect(() => {
    if (isOpen && !sessionToken) {
      createWidgetSession();
    }

    // Listen for messages from widget
    const handleMessage = async (event) => {
      // In production, verify event.origin matches DirhamClub domain
      if (event.data.type === 'DIRHAM_WALLET_UPDATED') {
        toast.success('Wallet updated');
        // Reload iframe
        if (iframeRef.current) {
          iframeRef.current.src = iframeRef.current.src;
        }
      } else if (event.data.type === 'DIRHAM_WALLET_LOADED') {
        console.log('Wallet loaded:', event.data);
      } else if (event.data.type === 'DIRHAM_OPEN_TOPUP') {
        openTopupModal();
      } else if (event.data.type === 'DIRHAM_AUTH_REQUIRED') {
        toast.error('Please login to DirhamClub to access your wallet');
      } else if (event.data.type === 'DIRHAM_AUTH_SUCCESS') {
        // User authenticated, link the account and retry session creation
        if (event.data.dirhamUserId) {
          try {
            await linkDirhamClubAccount(event.data.dirhamUserId);
            await createWidgetSession();
          } catch (error) {
            console.error('Error in auth success flow:', error);
            try {
              await createWidgetSession();
            } catch (sessionError) {
              console.error('Error creating session after auth:', sessionError);
            }
          }
        }
      } else if (event.data.type === 'DIRHAM_LOGOUT_REQUEST') {
        // User wants to logout and switch account
        handleLogout();
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [isOpen, sessionToken]);

  const linkDirhamClubAccount = async (dirhamUserId) => {
    try {
      const [response, err] = await doApi({
        url: DIRHAM_LINK_ACCOUNT_URL,
        method: 'POST',
        data: { dirhamUserId },
      });

      if (err) {
        throw new Error(err.error || 'Failed to link account');
      }

      toast.success('DirhamClub account linked successfully!');
    } catch (error) {
      console.error('Error linking account:', error);
      toast.error(error.message || 'Failed to link account');
      throw error;
    }
  };

  const handleLogout = async () => {
    try {
      // Unlink the account from backend
      const [response, err] = await doApi({
        url: DIRHAM_UNLINK_ACCOUNT_URL,
        method: 'POST',
      });

      if (err) {
        console.error('Error unlinking account:', err);
        // Still proceed with logout even if unlink fails
      } else {
        toast.success('Logged out from DirhamClub. You can now login with a different account.');
      }

      // Clear session token and reload widget to show auth UI
      setSessionToken(null);
      setError('');
      
      // Reload iframe to show login/signup form
      if (iframeRef.current) {
        iframeRef.current.src = `${DIRHAMCLUB_BASE_URL}/widget/wallet`;
      }
    } catch (error) {
      console.error('Error during logout:', error);
      // Still proceed with logout
      setSessionToken(null);
      setError('');
      if (iframeRef.current) {
        iframeRef.current.src = `${DIRHAMCLUB_BASE_URL}/widget/wallet`;
      }
    }
  };

  const createWidgetSession = async () => {
    setLoading(true);
    setError('');

    try {
      const [response, err] = await doApi({
        url: DIRHAM_WIDGET_SESSION_URL,
        method: 'POST',
        setLoading,
      });

      if (err) {
        // If auth required, load widget without session - it will show auth UI
        if (err.requiresAuth || err.error?.includes('not linked')) {
          setSessionToken(null);
          setError('');
          return;
        }
        throw new Error(err.error || 'Failed to create widget session');
      }

      if (!response || !response.sessionToken) {
        throw new Error('Invalid response from server');
      }

      setSessionToken(response.sessionToken);
    } catch (error) {
      console.error('Error creating widget session:', error);
      if (error.message?.includes('not linked')) {
        setError('AUTH_REQUIRED');
      } else {
        setError(error.message || 'Failed to load wallet');
        toast.error(error.message || 'Failed to load wallet');
      }
    } finally {
      setLoading(false);
    }
  };

  const openTopupModal = () => {
    if (!sessionToken) return;

    const topupUrl = `${DIRHAMCLUB_BASE_URL}/widget/topup?session=${sessionToken}`;
    const topupWindow = window.open(
      topupUrl, 
      'DirhamClub Topup', 
      'width=500,height=700,resizable=yes,scrollbars=yes'
    );
    
    // Listen for messages from topup window
    const handleTopupMessage = (event) => {
      if (event.data.type === 'DIRHAM_WALLET_UPDATED') {
        toast.success('Wallet topped up successfully!');
        if (iframeRef.current) {
          iframeRef.current.src = iframeRef.current.src;
        }
        topupWindow?.close();
        window.removeEventListener('message', handleTopupMessage);
      }
    };
    
    window.addEventListener('message', handleTopupMessage);
  };

  if (!isOpen) return null;

  const widgetUrl = `${DIRHAMCLUB_BASE_URL}/widget/wallet${sessionToken ? `?session=${sessionToken}` : ''}`;

  return (
    <div className={styles.overlay} onClick={onClose}>
      <div className={styles.modal} onClick={(e) => e.stopPropagation()}>
        <div className={styles.header}>
          <div className={styles.title}>
            <h2>Wallet</h2>
          </div>
          <button className={styles.closeButton} onClick={onClose}>
            <MdClose size={20} />
          </button>
        </div>

        {loading && (
          <div className={styles.loading}>
            Loading wallet...
          </div>
        )}

        {error && error !== 'AUTH_REQUIRED' && (
          <div className={styles.error}>
            {error}
            <button onClick={createWidgetSession} className={styles.retryButton}>
              Retry
            </button>
          </div>
        )}

        {(widgetUrl || !sessionToken) && !loading && error !== 'AUTH_REQUIRED' && (
          <iframe
            ref={iframeRef}
            src={widgetUrl}
            className={styles.iframe}
            title="DirhamClub Wallet"
            allow="payment *;"
            sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
          />
        )}
      </div>
    </div>
  );
};

export default DirhamWalletModal;

4. Styles (example)

**Example:** DirhamWalletModal.module.css

css
.overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  padding: 20px;
}

.modal {
  background: white;
  border-radius: 12px;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
  max-width: 600px;
  width: 100%;
  max-height: 80vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 24px;
  border-bottom: 1px solid #e5e7eb;
  background: linear-gradient(135deg, #075e54 0%, #0a3b83 100%);
  color: white;
  flex-shrink: 0;
}

.title h2 {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 600;
}

.closeButton {
  background: none;
  border: none;
  color: white;
  cursor: pointer;
  padding: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: background-color 0.2s;
}

.closeButton:hover {
  background-color: rgba(255, 255, 255, 0.2);
}

.loading {
  padding: 2rem;
  text-align: center;
  color: #666;
}

.error {
  padding: 2rem;
  text-align: center;
  color: #dc2626;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  align-items: center;
}

.retryButton {
  padding: 0.5rem 1rem;
  background-color: #2563eb;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.875rem;
}

.retryButton:hover {
  background-color: #1d4ed8;
}

.iframe {
  width: 100%;
  flex: 1;
  border: none;
  min-height: 500px;
}

@media (max-width: 768px) {
  .modal {
    max-width: 100%;
    max-height: 100vh;
    border-radius: 0;
  }

  .overlay {
    padding: 0;
  }

  .iframe {
    min-height: 400px;
  }
}

5. Mount the modal

javascript
import DirhamWalletModal from '../components/DirhamWalletModal';

const [showWallet, setShowWallet] = useState(false);

<DirhamWalletModal isOpen={showWallet} onClose={() => setShowWallet(false)} />;

---

User Authentication Flow

Initial State

  • User registers/logs into your app (no DirhamClub account yet)
  • User clicks "Wallet" or attempts a payment
  • Widget opens without session token
  • Widget shows login/signup form embedded in iframe

First-Time Authentication

  • User creates account in DirhamClub widget (or logs in if already exists)
  • Widget sends DIRHAM_AUTH_SUCCESS message with dirhamUserId
  • Frontend calls your backend link endpoint (e.g. POST /api/integrations/dirhamclub/link-account) to persist dirhamUserId
  • Frontend retries widget session creation
  • Widget loads successfully with session token

Subsequent Uses

  • User clicks "Wallet"
  • Frontend requests widget session from backend
  • Backend checks if user.dirhamUserId exists
  • If exists, creates session token and returns it
  • Widget loads with session token
  • User can view wallet, top up, see transactions

Account Linking

  • **One-time process**: Once dirhamUserId is linked to your user, it persists
  • **Persistent**: Link is stored in your database, survives logout/login
  • **Secure**: Linking happens server-side after user authenticates in DirhamClub
  • **Multiple app users per DirhamClub account**: If your schema allows it, many rows in your DB can share the same dirhamUserId (no uniqueness on that field)
  • **Re-linking**: Users can switch to a different DirhamClub account by using the logout button in the wallet widget
  • **Logout**: Users can logout from their current DirhamClub account and login with a different account
  • **Important**: When unlinking, use MongoDB's $unset operator to remove the field (not set to null) to avoid sparse index conflicts

Retries and idempotency (host app design)

DirhamClub treats **referenceType + referenceId** as an idempotency key for debits/credits. If your product allows users to **retry** the same checkout or payment flow, generate a **new** referenceId for each new attempt so the ledger records a new charge. Reusing the same pair returns the original outcome without deducting again.

Your own business rules (e.g. whether a merchant can re-accept a cancelled order) are **outside** DirhamClub; wallet balance is always authoritative via DirhamClub APIs.

---

Session Management

Widget Session Token

  • **Lifetime**: 5 minutes
  • **Format**: ws_<uuid> (e.g., ws_a1b2c3d4e5f6...)
  • **Storage**: Database (DirhamClub) with TTL index
  • **Purpose**: Secure iframe embedding without exposing JWT tokens

Session Flow

code
1. User opens wallet modal
   ↓
2. Frontend requests session from backend
   ↓
3. Backend calls DirhamClub API with clientId/clientSecret
   ↓
4. DirhamClub validates credentials and user
   ↓
5. DirhamClub creates session token (5min expiry)
   ↓
6. Frontend embeds widget with session token in URL
   ↓
7. Widget validates session token on each API call
   ↓
8. After 5 minutes, session expires
   ↓
9. User needs to reopen modal to get new session

Session Expiry Handling

  • Sessions expire after 5 minutes
  • Widget shows error when session expires
  • User must close and reopen wallet modal to get new session
  • Frontend should handle expired sessions gracefully

---

Wallet Operations

Frontend Operations (via Widget)

  • **View Balance**: Displayed in widget
  • **View Transactions**: Displayed in widget
  • **Top Up**: Opens topup modal, processes Stripe payment

Backend Operations (Server-to-Server)

#### 1. Get Wallet

javascript
const wallet = await getDirhamClubWallet(dirhamUserId);
// Returns: { balance, transactions, isActive }

#### 2. Check Balance

javascript
const result = await checkDirhamClubBalance(dirhamUserId, requiredAmount);
// Returns: { balance, requiredAmount, hasSufficientBalance }

#### 3. Debit Wallet (Payment)

javascript
// IMPORTANT: new referenceId per new payable attempt (idempotency)
const uniqueReferenceId = `${orderId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;

const result = await debitDirhamClubWallet(
  dirhamUserId,
  amount,
  'ORDER_PAYMENT', // your domain-specific referenceType
  uniqueReferenceId, // new id per attempt
  'Payment description'
);
// Returns: { balance, message } from HTTP API

**Note**: Reusing the same referenceType + referenceId replays the first outcome (no second debit). See Idempotency, linking, and data model notes.

#### 4. Credit Wallet (Refund)

javascript
const result = await creditDirhamClubWallet(
  dirhamUserId,
  amount,
  'ORDER_REFUND',
  `${orderId}_refund`, // distinct referenceId from original debit unless intentional idempotent replay
  'Refund description'
);
// Returns: { balance, message }

Reference types

referenceType is a **string label** for your ledger. DirhamClub does not enforce an enum for server APIs—use consistent names in your integration, for example:

  • ORDER_PAYMENT, SUBSCRIPTION_CHARGE, MARKETPLACE_ESCROW
  • ORDER_REFUND, CHARGEBACK_CREDIT
  • Internal flows may use TOPUP, BONUS, etc. (often created inside DirhamClub services)

---

Error Handling

Frontend Error Handling

javascript
// Handle requiresAuth error
if (err.requiresAuth) {
  // Show wallet modal - it will display auth UI
  setShowWallet(true);
}

// Handle account not linked
if (err.code === 'ACCOUNT_NOT_LINKED') {
  // Widget will show login/signup form
  setShowWallet(true);
}

// Handle other errors
toast.error(err.message || 'An error occurred');

Backend Error Handling

javascript
try {
  const result = await checkDirhamClubBalance(dirhamUserId, amount);
  return result;
} catch (error) {
  // Preserve requiresAuth flag
  if (error.requiresAuth) {
    error.requiresAuth = true;
  }
  throw error;
}

Common Error Codes

  • USER_NOT_FOUND: User doesn't exist in DirhamClub (needs authentication)
  • ACCOUNT_NOT_LINKED: User hasn't linked DirhamClub account
  • INSUFFICIENT_BALANCE: Not enough balance for operation
  • INVALID_CLIENT: Client credentials invalid
  • SESSION_EXPIRED: Widget session token expired

---

Security Considerations

1. Client Credentials

  • **Never expose** clientSecret in frontend code
  • Store credentials in backend environment variables only
  • Rotate secrets periodically via DirhamClub admin panel

2. Origin Validation

  • Configure allowedOrigins and allowedIframeParents in DirhamClub
  • Widget validates parent origin to prevent unauthorized embedding
  • In production, use HTTPS and exact domain matching

3. Session Token Security

  • Session tokens are short-lived (5 minutes)
  • Tokens are unique and single-use per session
  • Tokens are validated on every widget API call

4. postMessage Security

  • In production, verify event.origin matches DirhamClub domain
  • Only process messages from trusted origins
  • Example:
javascript
const handleMessage = (event) => {
  const DIRHAMCLUB_ORIGIN = 'https://dirhamclub.ae';
  if (event.origin !== DIRHAMCLUB_ORIGIN) {
    return; // Ignore messages from untrusted origins
  }
  // Process message
};

5. User Data

  • dirhamUserId should be treated as sensitive
  • Never expose dirhamUserId in frontend logs or URLs
  • Validate user ownership before operations

---

Idempotency, linking, and data model notes

MongoDB: sparse / unique indexes on `dirhamUserId`

If dirhamUserId is optional and you use a **sparse unique** index, avoid setting the field to null when unlinking—use **$unset** so the index does not see duplicate nulls. That pattern is common when storing the link on a users collection.

Sharing one DirhamClub wallet across several app accounts

If your product needs it, omit a uniqueness constraint on dirhamUserId in **your** database so multiple rows can hold the same value. If you previously had unique: true on that field, drop that index in a migration (for example db.users.dropIndex("dirhamUserId_1") in MongoDB).

Idempotent debits and user-visible retries

javascript
const uniqueReferenceId = `${businessId}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;

Use a **new** referenceId whenever the user starts a **new** payable attempt. Reuse the same pair only when you intentionally want the API to return the same result without double-charging (e.g. safe retries of the same HTTP request).

Widget logout / account switching

The embedded wallet can send DIRHAM_LOGOUT_REQUEST. Clear the stored dirhamUserId for the current app user (or only in session, depending on your policy), call your unlink API if you use one, then reload the iframe **without** a session token so the user can authenticate as a different DirhamClub user.

---

Complete Integration Checklist

Backend

  • [ ] Add environment variables (DIRHAMCLUB_BASE_URL, DIRHAMCLUB_CLIENT_ID, DIRHAMCLUB_CLIENT_SECRET)
  • [ ] Add dirhamUserId (or equivalent) to your user/account model; decide on uniqueness vs sharing
  • [ ] Drop or adjust DB indexes that conflict with optional / shared links
  • [ ] Implement a small server module calling DirhamClub **server** and **widget session** APIs
  • [ ] Expose authenticated routes: widget session, link account, unlink account (paths of your choice)
  • [ ] Route all spend/refund flows through POST /api/server/wallet/debit and .../credit with correct idempotency keys
  • [ ] Map requiresAuth / USER_NOT_FOUND to your product UX
  • [ ] Handle DIRHAM_LOGOUT_REQUEST on the client if you support account switching

Frontend

  • [ ] Configure public URL of DirhamClub (VITE_DIRHAMCLUB_URL or equivalent)
  • [ ] Point the app at **your** backend bridge URLs (session + link + unlink)
  • [ ] Embed /widget/wallet (and optional /widget/topup) with session query param from your API
  • [ ] Implement postMessage handling and strict event.origin checks in production
  • [ ] Test auth, link, unlink, top-up, and session expiry

DirhamClub Setup

  • [ ] Register client app in DirhamClub admin panel
  • [ ] Configure allowedOrigins (your frontend URL)
  • [ ] Configure allowedIframeParents (your frontend URL)
  • [ ] Set scopes: wallet:read, wallet:write
  • [ ] Save clientId and clientSecret
  • [ ] Test widget session creation

Testing

  • [ ] Test first-time user flow (no DirhamClub account)
  • [ ] Test account linking flow
  • [ ] Test wallet display after linking
  • [ ] Test logout and re-linking with different account
  • [ ] Test top-up flow
  • [ ] Test payment deduction
  • [ ] Test refund credit
  • [ ] Test session expiry handling
  • [ ] Test error scenarios

---

Additional resources

  • **This file** — integration-only endpoints: DirhamClub HTTP API reference
  • **Admin UI** — register client apps: /admin/client-apps on your DirhamClub deployment
  • **Full API list** — dirhamclub/README.md; architecture: dirhamclub/docs/

---

Support

For issues or questions:

  • Check error messages and logs
  • Verify environment variables are set correctly
  • Verify client app is registered and active in DirhamClub
  • Verify user has linked DirhamClub account
  • Check network connectivity to DirhamClub

---

**Last updated**: March 2025 **Version**: 1.1.0 (general integration + full API reference)