Skip to content

Angular — quickstart

A complete, runnable Angular checkout. We’ll use the standalone pattern (Angular 17+ default). Module-based projects work the same way; only the registration step differs (see Configuration).

A single page with a “Pay $12.50” button. Tapping it asks your backend to mint a Pdirect session, then mounts the <pdirect-pay-checkout> component and walks the customer through a wallet payment.

The merchant backend mints a session bound to (amount, currency, customer_reference) and returns the session_token to your Angular app. Node example:

your-backend/routes/checkout.ts
import crypto from "node:crypto";
export async function startCheckout(req, res) {
const r = await fetch(
"https://app.api.gtwy.pdirect.com/api/v1/internal/sessions/create",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDIRECT_MERCHANT_KEY_ID}:${process.env.PDIRECT_MERCHANT_SECRET}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
amount: "12.50",
currency: "usd",
customer_reference: "cust_abc123",
ttl_seconds: 1800,
}),
},
);
res.json(await r.json());
}

The Angular app never sees merchant_secret. Treat the result as ephemeral — a fresh session per checkout.

src/main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideHttpClient } from "@angular/common/http";
import {
PDIRECT_ENV_CONFIG,
PDIRECT_PAY_CONFIG,
} from "@mms/pdirect-pay";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
{
provide: PDIRECT_ENV_CONFIG,
useValue: {
baseUrl: "https://app.api.gtwy.pdirect.com",
},
},
{
provide: PDIRECT_PAY_CONFIG,
useValue: { isProduction: false, acceptLanguage: "en" },
},
],
}).catch((err) => console.error(err));

The session_token is set per-checkout on the component below, not at bootstrap.

src/app/checkout/checkout.component.ts
import { Component, signal, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { firstValueFrom } from "rxjs";
import {
PdirectPayCheckoutComponent,
PdirectPayConfig,
PdirectPaymentBody,
PdirectPayOnResponse,
PdirectPayOnError,
PdirectPayThemeConfig,
} from "@mms/pdirect-pay";
@Component({
selector: "app-checkout",
standalone: true,
imports: [PdirectPayCheckoutComponent],
template: `
@if (sessionToken()) {
<pdirect-pay-checkout
[configs]="configs()"
[paymentBody]="paymentBody"
[themeConfig]="themeConfig"
(response)="onSuccess($event)"
(error)="onFailure($event)"
(closed)="onClose()"
/>
} @else {
<button
type="button"
class="pay-button"
[disabled]="busy()"
(click)="onStart()">
Pay $12.50
</button>
}
`,
styles: [`
.pay-button {
padding: 0.75rem 1.5rem;
border-radius: 9999px;
border: none;
background: #2563eb;
color: white;
font-weight: 600;
cursor: pointer;
}
.pay-button[disabled] { opacity: 0.6; cursor: progress; }
`],
})
export class CheckoutComponent {
private http = inject(HttpClient);
sessionToken = signal<string | null>(null);
busy = signal(false);
configs = () =>
({
// `token` carries the session_token your backend minted.
// The SDK sends it as Authorization: Bearer <session_token>.
token: this.sessionToken()!,
isProduction: false,
acceptLanguage: "en",
} satisfies PdirectPayConfig);
paymentBody: PdirectPaymentBody = {
// amount / currency / customerReference are bound to the session
// — leave them unset and the SDK omits them on the wire.
paymentMethod: "wallet",
userInfo: "full",
paymentMethodInfo: "full",
feeCoveredBy: "buyer",
deliveryBehaviour: "direct_delivery",
notifyUrl: "https://merchant.example.com/pdirect/webhook",
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
phoneNumber: "+243998857000",
walletNumber: "3005710720108954",
walletPin: "048698",
};
themeConfig: PdirectPayThemeConfig = {
primaryColor: "#2563eb",
secondaryColor: "#1e40af",
tertiaryColor: "#3bfbda",
};
async onStart() {
this.busy.set(true);
try {
const { session_token } = await firstValueFrom(
this.http.post<{ session_token: string }>(
"/api/checkout/start",
{},
),
);
this.sessionToken.set(session_token);
} finally {
this.busy.set(false);
}
}
onSuccess(r: PdirectPayOnResponse) {
console.log("payment ok:", r.transactionId, r.paymentStatus);
this.sessionToken.set(null);
}
onFailure(e: PdirectPayOnError) {
console.warn("payment failed:", e.errorCode, e.message);
this.sessionToken.set(null);
}
onClose() {
this.sessionToken.set(null);
}
}
src/app/app.component.ts
import { Component } from "@angular/core";
import { CheckoutComponent } from "./checkout/checkout.component";
@Component({
selector: "app-root",
standalone: true,
imports: [CheckoutComponent],
template: `<main><app-checkout /></main>`,
styles: [`main { display: grid; place-items: center; min-height: 100vh; }`],
})
export class AppComponent {}
Terminal window
ng serve
# → http://localhost:4200

Click Pay $12.50. The component asks your backend for a session_token, then mounts the <pdirect-pay-checkout> widget; in sandbox the wallet flow resolves to approved within a few seconds and onSuccess fires with a transactionId.

Skip the widget when you need a custom UI:

import { Component, OnInit, inject } from "@angular/core";
import {
PdirectPaymentService,
PdirectPayApiResponseCode,
PdirectPaymentBody,
} from "@mms/pdirect-pay";
@Component({ /* ... */ })
export class CustomCheckoutComponent implements OnInit {
private payments = inject(PdirectPaymentService);
ngOnInit() {
// setAppKey is kept as the configuration helper for backwards
// compat — the string you pass is the per-checkout session_token
// your backend minted server-to-server.
this.payments.setAppKey(sessionTokenFromYourBackend);
this.payments.setAcceptLanguage("en");
const body: PdirectPaymentBody = {
// amount / currency / customerReference are bound to the
// session — omit them.
paymentMethod: "wallet",
userInfo: "full",
paymentMethodInfo: "full",
// ...
};
this.payments.createPayment(body).subscribe((res) => {
if (res.isSuccess) {
console.log("transaction:", res.data?.transactionId);
} else {
console.warn("failed:", res.apiResponseCode, res.message);
}
});
}
}

PdirectPaymentService returns RxJS Observable<PdirectHttpResponse<T>>. The wrapper exposes isSuccess, statusCode, apiResponseCode, and the parsed data. See Error handling.

The same pattern applies to every payment method — only the paymentMethod and method-specific fields change.