diff --git a/.npmignore b/.npmignore index ead5aa4..bc2e73c 100644 --- a/.npmignore +++ b/.npmignore @@ -8,4 +8,5 @@ index.css index.ts tailwind.config.js tsconfig.json -types.ts \ No newline at end of file +types.ts +src/ \ No newline at end of file diff --git a/src/access-token.ts b/src/access-token.ts index 95f719f..091a864 100644 --- a/src/access-token.ts +++ b/src/access-token.ts @@ -5,7 +5,7 @@ import { AccessTokenResponse } from "../types"; export const generateAccessToken = async (): Promise => { const credentials = `${CONSUMER_KEY}:${CONSUMER_SECRET}`; - const encodedCredentials = btoa(credentials); + const encodedAuthString = Buffer.from(credentials).toString("base64"); const token: AccessTokenResponse = cache.get("act"); @@ -18,7 +18,7 @@ export const generateAccessToken = async (): Promise => { `${BASE_URL}/oauth/v1/generate?grant_type=client_credentials`, { headers: { - Authorization: `Bearer ${encodedCredentials}`, + Authorization: `Basic ${encodedAuthString}`, "Access-Control-Allow-Origin": "*", }, } diff --git a/src/env.ts b/src/env.ts index 5136b21..b58fdfd 100644 --- a/src/env.ts +++ b/src/env.ts @@ -19,15 +19,20 @@ export const CONSUMER_SECRET = assertValue( ); export const BUSINESS_SHORT_CODE = assertValue( - process.env.BUSINESS_SHORT_CODE, - "Missing environment variable: BUSINESS_SHORT_CODE" + process.env.MPESA_BUSINESS_SHORT_CODE, + "Missing environment variable: MPESA_BUSINESS_SHORT_CODE" ); -export const PRODUCTION_PASS_KEY = +export const MPESA_TRANSACTION_TYPE = assertValue( + process.env.MPESA_TRANSACTION_TYPE, + "Missing environment variable: MPESA_TRANSACTION_TYPE" +); + +export const PASSKEY = ENVIRONMENT === "production" ? assertValue( - process.env.PRODUCTION_PASS_KEY, - "Missing environment variable: PRODUCTION_PASS_KEY" + process.env.MPESA_API_PASS_KEY, + "Missing environment variable: PASSKEY" ) : null; diff --git a/src/index.ts b/src/index.ts index cada82b..236f62e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ import { generateAccessToken } from "./access-token"; export const getStateOfALNMOnlinePayment = async ( stateOfALNMOnlinePaymentBody: StateOfALNMOnlinePaymentBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: StateOfALNMOnlinePaymentResponse = await axios.post( @@ -35,7 +35,7 @@ export const getStateOfALNMOnlinePayment = async ( stateOfALNMOnlinePaymentBody, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -51,7 +51,7 @@ export const getStateOfALNMOnlinePayment = async ( export const registerC2BUrl = async ( registerUrlBody: RegisterUrlBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: RegisterUrlResponse = await axios.post( @@ -59,7 +59,7 @@ export const registerC2BUrl = async ( registerUrlBody, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -75,7 +75,7 @@ export const registerC2BUrl = async ( export const b2cPaymentRequest = async ( b2CBody: B2CRequestBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: B2CRequestResponse = await axios.post( @@ -83,7 +83,7 @@ export const b2cPaymentRequest = async ( b2CBody, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -98,7 +98,7 @@ export const b2cPaymentRequest = async ( export const b2bPaymentRequest = async ( body: BusinessRequestBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: BusinessRequestResponse = await axios.post( @@ -106,7 +106,7 @@ export const b2bPaymentRequest = async ( body, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -121,7 +121,7 @@ export const b2bPaymentRequest = async ( export const getTransactionStatus = async ( transactionStatusBody: TransactionStatusBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: TransactionStatusResponse = await axios.post( @@ -129,7 +129,7 @@ export const getTransactionStatus = async ( transactionStatusBody, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -144,7 +144,7 @@ export const getTransactionStatus = async ( export const getAccountBalance = async ( accountBalance: AccountBalanceBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: AccountBalanceResponse = await axios.post( @@ -152,7 +152,7 @@ export const getAccountBalance = async ( accountBalance, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -167,7 +167,7 @@ export const getAccountBalance = async ( export const reverseC2BTransaction = async ( body: ReverseC2BTransactionBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: ReverseC2BTransactionResponse = await axios.post( @@ -175,7 +175,7 @@ export const reverseC2BTransaction = async ( body, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -190,7 +190,7 @@ export const reverseC2BTransaction = async ( export const remitTax = async ( body: TaxRemittanceBody ): Promise => { - const accessToken = await generateAccessToken(); + const { access_token } = await generateAccessToken(); try { const res: TaxRemittanceResponse = await axios.post( @@ -198,7 +198,7 @@ export const remitTax = async ( body, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${access_token}`, }, } ); @@ -212,3 +212,4 @@ export const remitTax = async ( export { stkPushRequest } from "./stk-push"; export { QRCodeDisplay } from "../components/QRCodeDisplay"; +export * from "../types"; diff --git a/src/stk-push.ts b/src/stk-push.ts index 59cb4af..c80ba4d 100644 --- a/src/stk-push.ts +++ b/src/stk-push.ts @@ -1,31 +1,39 @@ import axios from "axios"; -import { STKPushBody, STKPushResponse } from "../types"; -import { generateTimestamp, generatePassword } from "../util/utils"; import { - BASE_URL, - BUSINESS_SHORT_CODE, - ENVIRONMENT, - PRODUCTION_PASS_KEY, -} from "./env"; + AccountReference, + Amount, + CallBackURL, + PhoneNumber, + STKPushBody, + STKPushResponse, + TransactionDesc, + TransactionType, +} from "../types"; +import { generateTimestamp, generatePassword } from "../util/utils"; +import { BASE_URL, BUSINESS_SHORT_CODE, ENVIRONMENT, PASSKEY } from "./env"; import { generateAccessToken } from "./access-token"; -export const stkPushRequest = async ( - phoneNumber: string, - amount: string, - callbackURL: string, - transactionDesc: string, - transactionType: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline" -) => { +export const stkPushRequest = async ({ + phoneNumber, + amount, + callbackURL, + transactionDesc, + accountReference, +}: { + phoneNumber: PhoneNumber; + amount: Amount; + callbackURL: CallBackURL; + transactionDesc: TransactionDesc; + accountReference: AccountReference; +}) => { try { const timestamp = generateTimestamp(); - const password = - ENVIRONMENT === "production" - ? generatePassword( - BUSINESS_SHORT_CODE!, - PRODUCTION_PASS_KEY!, - timestamp - ) - : "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMTYwMjE2MTY1NjI3"; + + const password = generatePassword( + BUSINESS_SHORT_CODE!, + PASSKEY!, + timestamp + ); const stkPushBody: STKPushBody = { BusinessShortCode: BUSINESS_SHORT_CODE!, @@ -34,26 +42,32 @@ export const stkPushRequest = async ( Password: password, PartyA: phoneNumber, PhoneNumber: phoneNumber, - Amount: amount, + Amount: ENVIRONMENT === "production" ? amount : "1", CallBackURL: callbackURL, TransactionDesc: transactionDesc, - TransactionType: transactionType, + TransactionType: process.env + .MPESA_TRANSACTION_TYPE as unknown as TransactionType, + AccountReference: accountReference, }; - const accessToken = generateAccessToken(); + console.log(stkPushBody); + + const accessTokenResponse = await generateAccessToken(); const res: STKPushResponse = await axios.post( `${BASE_URL}/mpesa/stkpush/v1/processrequest`, stkPushBody, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${accessTokenResponse.access_token}`, }, } ); return res; } catch (err: any) { + console.error(err); + throw new Error( `Error occurred with status code ${err.response?.status}, ${err.response?.statusText}` ); diff --git a/tsconfig.json b/tsconfig.json index 6e42ac4..1eec827 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -51,7 +51,7 @@ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ diff --git a/types.ts b/types.ts index 8b95076..acfb46e 100644 --- a/types.ts +++ b/types.ts @@ -159,6 +159,57 @@ export interface StateOfALNMOnlinePaymentResponse { ResultDesc: string; } +/** + * This is the transaction type that is used to identify the transaction when sending the request to M-PESA. + * The transaction type for M-PESA Express is "CustomerPayBillOnline" for PayBill Numbers and "CustomerBuyGoodsOnline" for Till Numbers. + * @type {"CustomerPayBillOnline" | "CustomerBuyGoodsOnline"} + * @example "CustomerPayBillOnline" + */ +export type TransactionType = + | "CustomerPayBillOnline" + | "CustomerBuyGoodsOnline"; + +/** + * The Mobile Number to receive the STK Pin Prompt. This number can be the same as PartyA value above. + * @type {string} + * @example "2547XXXXXXXX" + */ +export type PhoneNumber = string; + +/** + * This is the Amount transacted normally a numeric value. Money that the customer pays to the Shortcode. + * Only whole numbers are supported. + * @type {string} + * @example "10" + */ +export type Amount = string; + +/** + * A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API. + * It is the endpoint to which the results will be sent by M-Pesa API. + * @type {string} + * @example "https://mydomain.com/path" + */ +export type CallBackURL = string; + +/** + * This is any additional information/comment that can be sent along with the request from your system. + * Maximum of 13 Characters. + * @type {string} + * @example "Payment for Order" + */ +export type TransactionDesc = string; + +/** + * Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier + * of the transaction for the CustomerPayBillOnline transaction type. + * Along with the business name, this value is also displayed to the customer in the STK Pin Prompt message. + * Maximum of 12 characters. + * @type {string} + * @example "ABC123456789" + */ +export type AccountReference = string; + /** * Request Parameter Description * @interface STKPushBody @@ -182,21 +233,9 @@ export interface STKPushBody { */ Timestamp: string; - /** - * This is the transaction type that is used to identify the transaction when sending the request to M-PESA. - * The transaction type for M-PESA Express is "CustomerPayBillOnline" for PayBill Numbers and "CustomerBuyGoodsOnline" for Till Numbers. - * @type {"CustomerPayBillOnline" | "CustomerBuyGoodsOnline"} - * @example "CustomerPayBillOnline" - */ - TransactionType: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline"; + TransactionType: TransactionType; - /** - * This is the Amount transacted normally a numeric value. Money that the customer pays to the Shortcode. - * Only whole numbers are supported. - * @type {string} - * @example "10" - */ - Amount: string; + Amount: Amount; /** * The phone number sending money. The parameter expected is a Valid Safaricom Mobile Number that is M-PESA registered @@ -214,38 +253,13 @@ export interface STKPushBody { */ PartyB: string; - /** - * The Mobile Number to receive the STK Pin Prompt. This number can be the same as PartyA value above. - * @type {string} - * @example "2547XXXXXXXX" - */ - PhoneNumber: string; + PhoneNumber: PhoneNumber; - /** - * A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API. - * It is the endpoint to which the results will be sent by M-Pesa API. - * @type {string} - * @example "https://mydomain.com/path" - */ - CallBackURL: string; + CallBackURL: CallBackURL; - /** - * Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier - * of the transaction for the CustomerPayBillOnline transaction type. - * Along with the business name, this value is also displayed to the customer in the STK Pin Prompt message. - * Maximum of 12 characters. - * @type {string} - * @example "ABC123456789" - */ - AccountReference?: string; + AccountReference?: AccountReference; - /** - * This is any additional information/comment that can be sent along with the request from your system. - * Maximum of 13 Characters. - * @type {string} - * @example "Payment for Order" - */ - TransactionDesc: string; + TransactionDesc: TransactionDesc; } /** diff --git a/util/utils.ts b/util/utils.ts index 1a054f0..9ce4d64 100644 --- a/util/utils.ts +++ b/util/utils.ts @@ -1,5 +1,3 @@ -import { encode } from "base-64"; - /** * Generates a timestamp in the format of YEAR+MONTH+DATE+HOUR+MINUTE+SECOND (YYYYMMDDHHMMSS). * @returns {string} Timestamp in the specified format. @@ -23,7 +21,7 @@ export const generatePassword = ( timestamp: string ): string => { const concatenatedString = `${businessShortCode}${passkey}${timestamp}`; - const encodedString = encode(concatenatedString); + const encodedString = btoa(concatenatedString); return encodedString; };