API Reference
PaymentEngine
The orchestrator. Wraps a PaymentApi with retry, state-machine validation, and event emission.
PaymentEngine (in packages/paynow_core/lib/src/payment_engine.dart) is the central coordinator. It's what PayNowWebSdk delegates to under the hood.
final engine = PaymentEngine(
api: createPaymentApi(),
retryPolicy: const RetryPolicy(),
// optional offlineActionStore for queueing during outages
);
What it does on every call
- Wraps the call in
_withRetryaccording toRetryPolicy. - Catches
PaymentApiExceptionand decides retry vs rethrow. - On success, runs the returned session through
PaymentStateMachine.transition(which validates the move and bumps the version). - Emits a
PaymentEventon the internalEventBus. - Returns the merged session to the caller.
Public surface
Methods (mirror PaymentApi)
createInvoice,generateQr,validateAccount,submitAccount,submitOtp,refreshStatus
Each takes the same request DTOs as PaymentApi.
void hydrate(PaymentSession session)
Inject a session without making an API call. Used when a session is initiated server-side (via merchantInitiateMiddleware) and the browser receives the paymentId.
Stream<PaymentEvent> get events
Broadcast stream of every state change. Subscribe in your UI:
engine.events.listen((event) {
setState(() => _session = event.currentSession);
});
PaymentSession? get currentSession
The most recent session (null before any call).
Future<void> replayQueuedActions()
When configured with an OfflineActionStore, drains queued actions and replays them through the engine. Use after the device comes back online.
PaymentEvent
class PaymentEvent {
final String paymentId;
final PaymentSession? previousSession;
final PaymentSession currentSession;
final DateTime occurredAt;
}
previousSession is null on the first emit and on hydrate.
Why an engine instead of calling PaymentApi directly
The state machine catches programmer errors (illegal state transitions throw, surfacing flows that wouldn't make sense to the customer). The retry policy keeps transient backend blips from breaking the UX. The event bus decouples the page state from the call site — useful for the status poller, for tests, and for the future Flutter SDK where multiple widgets observe the same session.
Engine in tests
Pass a fake PaymentApi:
class _FakeApi implements PaymentApi {
PaymentSession nextResponse = ...;
@override
Future<PaymentSession> fetchStatus(FetchStatusRequest r) async => nextResponse;
// ... other methods
}
final engine = PaymentEngine(api: _FakeApi(), retryPolicy: const RetryPolicy(maxAttempts: 1));
engine.hydrate(initialSession);
final newSession = await engine.refreshStatus(FetchStatusRequest(paymentId: '...'));