YallaPay Sudan

Flutter SDK for the YallaPaySudan payment gateway. Supports one-time payments, recurring subscriptions, webhook signature verification, and in-app WebView checkout.

Features

  • One-time payments -- Generate payment links via the YallaPaySudan API
  • Subscriptions -- Recurring billing with configurable intervals (day/month/year)
  • In-app checkout -- Built-in WebView widget with redirect detection
  • Webhook verification -- HMAC-SHA256 signature verification with replay attack protection
  • Type-safe -- Immutable models, sealed exceptions, and exhaustive error handling

Screenshots

Main Screen Checkout Payment Success Result
Main Checkout Success Result

Getting Started

1. Install

dependencies:
  yalla_pay_sudan: ^1.0.0
flutter pub get

2. Get your credentials

Go to the YallaPaySudan Dashboard > Developer tab:

  • Copy your Auth Token (test or production)
  • Set up a Webhook Secret under Webhooks
  • Note the Base URL for your environment

3. Choose your environment

Environment Base URL Token prefix
Sandbox https://gateway-dev.yallapaysudan.com/api/v1 test_sk_*
Production https://gateway.yallapaysudan.com/api/v1 sk_*

4. Create the client

import 'package:yalla_pay_sudan/yalla_pay_sudan.dart';

// Sandbox (testing)
final client = YallaPayClient(
  YallaPayConfig.sandbox(apiKey: 'your-test-token'),
);

// Production (live)
final client = YallaPayClient(
  YallaPayConfig.live(apiKey: 'your-live-token'),
);

// Or manual configuration
final client = YallaPayClient(
  YallaPayConfig(
    apiKey: 'your-auth-token',
    baseUrl: ApiConstants.sandboxBaseUrl,
  ),
);

Usage

One-Time Payment

try {
  final response = await client.createPayment(
    PaymentRequest(
      amount: 5000,                       // SDG, minimum 1,000
      clientReferenceId: 'order-123',     // Must be unique per payment
      description: 'Product purchase',
      paymentSuccessfulRedirectUrl: 'https://myapp.com/success',
      paymentFailedRedirectUrl: 'https://myapp.com/failed',
    ),
  );

  print('Checkout URL: ${response.paymentUrl}');
} on PaymentException catch (e) {
  print('API error: ${e.message} (code: ${e.responseCode})');
} on NetworkException catch (e) {
  print('Network error: ${e.message}');
}

Note: clientReferenceId must be unique for every payment request. Reusing an ID returns a "Duplicate client reference ID" error.

Subscription Payment

final response = await client.createSubscription(
  SubscriptionRequest(
    amount: 3000,
    clientReferenceId: 'sub-456',
    description: 'Monthly plan',
    subscriptionConfiguration: SubscriptionConfiguration(
      interval: SubscriptionInterval.month,
      intervalCycle: 1,       // Every 1 month
      totalCycles: 12,        // 12 months total (omit for indefinite)
    ),
  ),
);

In-App WebView Checkout

Open the checkout page directly inside your app. The redirect URLs are automatically read from the PaymentResponse:

final result = await YallaPayCheckoutWebView.show(
  context,
  response: response,
);

switch (result?.status) {
  case PaymentStatus.successful:
    // Payment completed
  case PaymentStatus.failed:
    // Payment failed
  case PaymentStatus.cancelled:
  case null:
    // User dismissed the checkout
}

The WebView intercepts navigation to your success/failure redirect URLs and closes automatically. If the user presses back, the result is PaymentStatus.cancelled.

You can also pass URLs manually if needed:

final result = await YallaPayCheckoutWebView.show(
  context,
  paymentUrl: response.paymentUrl,
  successUrlPattern: 'https://myapp.com/success',
  failedUrlPattern: 'https://myapp.com/failed',
);

Or embed the widget directly in your widget tree:

YallaPayCheckoutWebView(
  paymentUrl: response.paymentUrl,
  successUrlPattern: 'https://myapp.com/success',
  failedUrlPattern: 'https://myapp.com/failed',
  onCheckoutResult: (result) {
    // Handle result
  },
)

Check Payment Status

Check the status of a payment without relying on webhooks or redirects:

final status = await client.getPaymentStatus(
  clientReferenceId: 'order-123',
  transactionDate: '2025-12-05',  // YYYY-MM-DD
);

if (status.isSuccessful) {
  print('Paid ${status.amount} SDG — ref: ${status.paymentReferenceId}');
} else {
  print('Status: ${status.status}'); // failed, cancelled, revoked, expired
}

Webhook Verification

For Dart backends (Shelf, Dart Frog, Serverpod) -- verify that incoming webhooks are authentic:

final client = YallaPayClient(
  YallaPayConfig(
    apiKey: 'your-auth-token',
    webhookSecret: 'your-webhook-secret',
  ),
);

try {
  final payload = client.verifyWebhook(
    signature: headers['YallaPay-Signature']!,
    timestamp: headers['YallaPay-TimeStamp']!,
    rawBody: requestBody,
  );

  switch (payload.status) {
    case PaymentStatus.successful:
      await markOrderPaid(payload.clientReferenceId);
    case PaymentStatus.failed:
      await markOrderFailed(payload.clientReferenceId);
    case PaymentStatus.cancelled:
      await markOrderCancelled(payload.clientReferenceId);
  }
} on InvalidSignatureException {
  // Return HTTP 401 -- invalid signature or replay attack
}

How it works: YallaPaySudan signs "$timestamp.$rawBody" with HMAC-SHA256 using your webhook secret. This package recomputes the signature and compares it using constant-time comparison to prevent timing attacks. Timestamps older than 5 minutes (configurable) are rejected to prevent replay attacks.

Error Handling

All exceptions extend the sealed YallaPayException class for exhaustive matching:

try {
  final response = await client.createPayment(request);
} on YallaPayException catch (e) {
  switch (e) {
    case PaymentException():
      print('API error: ${e.responseCode} -- ${e.message}');
    case NetworkException():
      print('Network error: ${e.message}');
    case InvalidSignatureException():
      print('Signature error: ${e.message}');
  }
}
Exception When
PaymentException API returned an error (with responseCode)
NetworkException Timeout, no connection, DNS failure
InvalidSignatureException Webhook HMAC mismatch or expired timestamp
ArgumentError Invalid input (amount < 1000, empty reference ID)

API Reference

YallaPayConfig

Property Type Default Description
apiKey String required Auth token from dashboard
baseUrl String gateway.yallapaysudan.com/api/v1 API base URL
webhookSecret String? null For webhook signature verification
webhookTimestampTolerance Duration 5 minutes Max age for webhook timestamps
connectTimeout Duration 30 seconds HTTP connection timeout
receiveTimeout Duration 30 seconds HTTP response timeout
enableLogging bool false Log requests/responses (debug only)

PaymentResponse

Property Type Description
responseCode String "0" = success
responseMessage String Human-readable message
paymentUrl String Checkout URL for the customer
isSuccess bool Whether responseCode is "0"
successRedirectUrl String? Auto-filled from your payment request
failedRedirectUrl String? Auto-filled from your payment request

WebhookPayload

Property Type Description
clientReferenceId String Your transaction ID
paymentReferenceId String YallaPaySudan transaction ID
status PaymentStatus successful/failed/cancelled
isSuccessful bool Convenience getter

Cleanup

Always dispose the client when done:

client.dispose();

License

MIT

Libraries

yalla_pay_sudan