An Angular library that abstracts the complexity of taking Monnify payments — as a component, a directive, or a service.
1.3.2 and earlier had a bug where AngularMonnifyService.loadScript() contained an if (this.isTestMode) whose two branches were identical — both requested https://sandbox.sdk.monnify.com/plugin/monnify.js.
In practice this had no runtime effect, and there is nothing for you to reconcile. The two hosts serve a byte-for-byte identical file:
sandbox.sdk.monnify.com/plugin/monnify.js sha256 4b217d7df359faaa2eb21e7461cb0e6c62ec309eb204f462b5b7328dca7fc6be
sdk.monnify.com/plugin/monnify.js sha256 4b217d7df359faaa2eb21e7461cb0e6c62ec309eb204f462b5b7328dca7fc6be
and the SDK selects its environment from your API key prefix (MK_PROD_), not from the host the script came from. Live keys always reached the live gateway. The dead branch was still a real bug — relying on two URLs staying identical is fragile, and Monnify could diverge them at any time — so 2.0.0 fixes it and a regression test now asserts the two modes can never resolve to the same URL.
The changes that will affect you are the Angular version requirement, the package output format, and several genuine correctness bugs — see Migrating.
| angular-monnify | Angular |
|---|---|
2.x |
16 – 20 |
1.x |
8 – 12 (unmaintained) |
2.0.0 is compiled with Angular 20 in Ivy partial compilation mode. The declared peer range is ^16 || ^17 || ^18 || ^19 || ^20; Angular 20 is the version it is built and tested against, and the one we recommend.
npm install angular-monnifyRegister your merchant credentials once, at the root of the app. Pick whichever style your app uses.
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAngularMonnify } from 'angular-monnify';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
// apiKey, contractCode, isTestMode (defaults to false = production)
provideAngularMonnify('MK_TEST_GC3B8XG2XX', '5867418298', true),
],
});forRoot() works exactly as it did in 1.x:
import { NgModule } from '@angular/core';
import { AngularMonnifyModule } from 'angular-monnify';
@NgModule({
imports: [AngularMonnifyModule.forRoot('MK_TEST_GC3B8XG2XX', '5867418298', true)],
})
export class AppModule {}In feature modules, import the module without forRoot():
@NgModule({
imports: [AngularMonnifyModule],
declarations: [CheckoutPage],
})
export class CheckoutModule {}
isTestModeis the third argument. Passtruefor the sandbox,false(or omit it) for live payments.
There are two ways to trigger checkout. Both are standalone in 2.x, so a standalone component can import either one directly:
import { Component } from '@angular/core';
import { AngularMonnifyComponent, AngularMonnifyDirective } from 'angular-monnify';
@Component({
selector: 'app-checkout',
standalone: true,
imports: [AngularMonnifyComponent, AngularMonnifyDirective],
templateUrl: './checkout.component.html',
})
export class CheckoutComponent {}Renders its own <button> and projects your content into it.
<angular-Monnify
[customerFullName]="'John Doe'"
[customerMobileNumber]="'08121281921'"
[customerEmail]="'john@example.com'"
[paymentDescription]="'Order #1234'"
[amount]="5000"
[reference]="reference"
(paymentInit)="paymentInit()"
(onClose)="paymentCancel($event)"
(onComplete)="paymentDone($event)"
>
Pay with Monnify
</angular-Monnify>The component also accepts [class] and [style], which are applied to the button it renders:
<angular-Monnify [class]="'btn btn-primary btn-lg'" ...>Pay</angular-Monnify>Attach it to any clickable element you already have — a plain <button>, an <ion-button>, anything.
<button
angular-Monnify
class="btn btn-primary"
[customerFullName]="'John Doe'"
[customerMobileNumber]="'08121281921'"
[customerEmail]="'john@example.com'"
[paymentDescription]="'Order #1234'"
[amount]="5000"
[reference]="reference"
(paymentInit)="paymentInit()"
(onClose)="paymentCancel($event)"
(onComplete)="paymentDone($event)"
>
Pay with Monnify
</button>The directive does not declare
classorstyleinputs, soclass/[class]/[ngClass]on the host behave natively. (In 1.x the directive declared them and silently swallowed your class binding.)
import { Component } from '@angular/core';
@Component({
selector: 'app-checkout',
templateUrl: './checkout.component.html',
})
export class CheckoutComponent {
// Monnify rejects a re-used reference, so mint a fresh one per attempt.
reference = `${Math.floor(Math.random() * 1_000_000_000 + 1)}`;
paymentInit(): void {
this.reference = `${Math.floor(Math.random() * 1_000_000_000 + 1)}`;
console.log('Payment initialized');
}
paymentDone(response: unknown): void {
console.log('Payment successful', response);
}
paymentCancel(response: unknown): void {
console.log('Payment modal closed', response);
}
}
(onComplete)is required. If nothing is subscribed to it,pay()refuses to open checkout and returns an error string instead — there would be no way to learn the outcome of the payment.
Instead of individual inputs, bind a single monnifyOptions object:
<button
angular-Monnify
[monnifyOptions]="options"
(paymentInit)="paymentInit()"
(onClose)="paymentCancel($event)"
(onComplete)="paymentDone($event)"
>
Pay with Monnify
</button>import { Component } from '@angular/core';
import { MonnifyOptions } from 'angular-monnify';
@Component({
selector: 'app-checkout',
templateUrl: './checkout.component.html',
})
export class CheckoutComponent {
options: MonnifyOptions = {
amount: 5000,
currency: 'NGN',
reference: `${Math.floor(Math.random() * 1_000_000_000 + 1)}`,
customerFullName: 'John Doe',
customerEmail: 'monnify@monnify.com',
customerMobileNumber: '08121281921',
apiKey: 'MK_TEST_GC3B8XG2XX',
contractCode: '5867418298',
paymentDescription: 'Test Pay',
isTestMode: true,
metadata: { name: 'Damilare', age: 45 },
};
paymentInit(): void {}
paymentDone(response: unknown): void {}
paymentCancel(response: unknown): void {}
}
monnifyOptionsis only used when it has two or more keys; otherwise the library falls back to reading the individual inputs.
import { MonnifyOptions } from 'angular-monnify';
options: MonnifyOptions = {
// ...the required fields...
incomeSplitConfig: [
{ subAccountCode: 'MFY_SUB_342113621921', feePercentage: 50, splitAmount: 1900, feeBearer: true },
{ subAccountCode: 'MFY_SUB_342113621922', feePercentage: 50, splitAmount: 2100, feeBearer: true },
],
};apiKey, contractCode and isTestMode set on the component/directive take precedence over the values you registered at the root:
// root
provideAngularMonnify('globalApiKey', 'globalContract', false);<!-- this one payment uses apiKey2 / contractCode2 instead -->
<button
angular-Monnify
[apiKey]="'apiKey2'"
[contractCode]="'contractCode2'"
...
></button>Available on both AngularMonnifyComponent and AngularMonnifyDirective. All are optional; the ones marked required must be resolvable either from the input, from monnifyOptions, or (for apiKey / contractCode / isTestMode) from your root registration.
| Input | Type | Notes |
|---|---|---|
amount |
number |
Required. Amount to charge. |
reference |
string |
Required. Unique per transaction; Monnify rejects duplicates. |
customerFullName |
string |
Required. Full name of the customer. |
customerEmail |
string |
Required. Email address of the customer. |
customerMobileNumber |
string |
Required. Phone number of the customer. |
apiKey |
string |
Required (input or root). From the Monnify dashboard. |
contractCode |
string |
Required (input or root). From the Monnify dashboard. |
currency |
string |
Defaults to 'NGN'. |
isTestMode |
boolean |
true → sandbox SDK, false → live SDK. Defaults to false. |
paymentDescription |
string |
Used as the account name for bank-transfer payments. |
redirectUrl |
string |
Where Monnify sends the customer after payment. |
incomeSplitConfig |
MonnifySplitOptions[] |
How to split the payment across sub-accounts. |
metadata |
object |
Arbitrary extra data echoed back to you. |
monnifyOptions |
Partial<MonnifyOptions> |
All of the above as one object (used when it has ≥ 2 keys). |
class |
string |
Component only. Applied to the rendered button. |
style |
object |
Component only. Applied to the rendered button. |
| Output | Payload | Fires when |
|---|---|---|
paymentInit |
— | Validation passed and checkout is about to open. |
onComplete |
unknown |
Monnify reports the payment finished. Subscribing is mandatory. |
onClose |
unknown |
The customer dismissed the checkout modal. |
| Field | Type | Notes |
|---|---|---|
subAccountCode |
string |
Required. Sub-account that receives the split. |
feeBearer |
boolean |
Whether the sub-account bears transaction fees. |
feePercentage |
number |
Share of the transaction fee borne by the sub-account. |
splitPercentage |
string |
Percentage of the payment to route to the sub-account. |
splitAmount |
number |
Flat amount to route to the sub-account. |
Injectable with providedIn: 'root', so you can use it directly if you want to drive checkout yourself:
import { inject } from '@angular/core';
import { AngularMonnifyService } from 'angular-monnify';
const monnify = inject(AngularMonnifyService);
monnify.getScriptUrl(); // the SDK url for the configured mode
monnify.getScriptUrl(true); // force the sandbox url
await monnify.loadScript(); // inject the SDK <script>, resolves once loaded
monnify.checkInput(options); // '' when valid, otherwise the error message
monnify.getMonnifyOptions(o); // merge with root config + apply defaultsThe SDK URLs are exported too, if you need them for a CSP allow-list:
import { MONNIFY_LIVE_SDK_URL, MONNIFY_SANDBOX_SDK_URL } from 'angular-monnify';
// 'https://sdk.monnify.com/plugin/monnify.js'
// 'https://sandbox.sdk.monnify.com/plugin/monnify.js'This is the reason 2.0 exists. Confirm isTestMode is false for production builds, and reconcile transactions taken while on 1.x.
1.x supported Angular 8–12. If you are still on Angular 12 or lower, upgrade Angular first — ng update one major at a time.
The 1.x README documented (close)="paymentCancel()". No such output has ever existed; that binding silently did nothing. The output is and always was onClose:
- (close)="paymentCancel()"
+ (onClose)="paymentCancel($event)"The 1.x options table listed customerName. The actual input is customerFullName.
The 1.x README bound it as a string ([amount]="'5000000'"). With Angular's strict template checking that is now a compile error:
- [amount]="'5000000'"
+ [amount]="5000000"It never used them, and declaring them meant a [class] binding on the host was swallowed instead of applied. Remove nothing — your existing class / [class] / [ngClass] now simply works. The component still has both inputs.
AngularMonnifyComponent and AngularMonnifyDirective are now standalone, and provideAngularMonnify() is the standalone counterpart to forRoot(). AngularMonnifyModule.forRoot() still works — nothing forces you to migrate.
- A missing
apiKeynow reports'ANGULAR-Monnify: Monnify apiKey cannot be empty'(was the ungrammatical'Please insert a your apiKey'). - A
contractCodesupplied only viaforRoot()/provideAngularMonnify()is now honoured; 1.x always reported it missing unless it was also set on the component. - Checkout can be re-opened after the modal is closed. In 1.x an internal flag was set and never cleared, so the second and every later click did nothing.
- A per-payment
isTestMode: falseis no longer overridden by a roottrue. loadScript()now rejects if the SDK fails to load, instead of hanging forever.- The stray
console.log('loaded')on every checkout is gone.
Fork the repo and open a pull request — contributions are welcome.
npm install
npm run build:lib # build the library with ng-packagr
npm test # unit tests (Karma + Chrome)
npm run lint # ESLint
npm start # run the Ionic demo appStar the repo, or share it. And follow me on Twitter!
Thanks! Dansteve Adekanbi — dansteve.com
The MIT License (MIT). Please see the License File for more information.

