diff --git a/.changeset/silent-bikes-exercise.md b/.changeset/silent-bikes-exercise.md new file mode 100644 index 0000000..c0eefd7 --- /dev/null +++ b/.changeset/silent-bikes-exercise.md @@ -0,0 +1,5 @@ +--- +"react-daraja": patch +--- + +completed stkPushRequest function 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/README.md b/README.md index ee530e6..ea6b6dd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,75 @@ -# react-daraja +# React Daraja -A react library to interact with Safaricom`s daraja APIS +React Daraja is a typesafe Javascript library designed to simplify interactions with the Safaricom Daraja API, specifically for STK push requests. This library is suitable for both Node.js and React environments, allowing developers to seamlessly integrate M-Pesa payments into their applications. -## We are not ready yet. A comprehensive documentation will be available when we go live +## Installation -Right now i am still testing this library. +To install React Daraja, run the following command in your project's terminal: -Peace. +```bash + npm install react-daraja +``` + +## Getting Started + +Before using the library, make sure to set up the required environment variables in the .env file. These variables include: + +- **NODE_ENV**: Set the environment to either "production" or "development." + +- **MPESA_CONSUMER_KEY**: Consumer Key obtained from Safaricom Daraja. + +- **MPESA_CONSUMER_SECRET**: Consumer Secret obtained from Safaricom Daraja. + +- **MPESA_BUSINESS_SHORT_CODE**: Your M-Pesa business short code. For Sandbox use the code **174379** + +- **MPESA_TRANSACTION_TYPE**: Set the transaction type, either "CustomerPayBillOnline" or "CustomerBuyGoodsOnline." + +- **MPESA_API_PASS_KEY**: Your M-Pesa API pass key. For sandbox use **bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919** + +The library throws errors if any of this values are missing from your .env file. + +## Usage/Examples + +- **STK Push Request** + +It allows you to initiate an STK push request for M-Pesa payments in **node**. Here is an example of how to use it: + +```javascript +import { stkPushRequest } from "react-daraja"; + +const makeSTKPushRequest = async () => { + try { + const response = await stkPushRequest({ + phoneNumber: "254719428019", + amount: "100", + callbackURL: "https://example.com/api/stk-push-callback", + transactionDesc: "Payment for your service", + accountReference: "user123@example.com", + }); + + console.log("STK Push Request Successful:", response); + } catch (error) { + console.error("Error:", error.message); + } +}; + +makeSTKPushRequest(); +``` + +and thats it!. The library handles access tokens etc and caches them gracefully. + +Make sure to replace the placeholder values with your actual data. + +Note: Ensure that your environment variables are correctly set, and the provided values match your Safaricom Daraja account configuration. + +Don`t call stkPushRequest from the frontend though. You wil run into all forms of cors errors. + +## Compatibility + +React Daraja is compatible with Node.js and React environments. It provides a simple interface for initiating M-Pesa transactions using the Safaricom Daraja API. + +Some APIs are exclusively for Node Environments and some are just React Components so are only used in react. + +## License + +This library is licensed under the MIT License. Feel free to contribute or open issues on the GitHub repository. More APIs and components coming. Watch this repo for alerts. diff --git a/src/access-token.ts b/src/access-token.ts index 95f719f..57cef29 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,16 +18,16 @@ export const generateAccessToken = async (): Promise => { `${BASE_URL}/oauth/v1/generate?grant_type=client_credentials`, { headers: { - Authorization: `Bearer ${encodedCredentials}`, - "Access-Control-Allow-Origin": "*", + Authorization: `Basic ${encodedAuthString}`, }, } ); - cache.put("act", res.data, res.data.expires_in); + cache.put("act", res.data, 3600); return res.data; } catch (err: any) { + console.error(err); throw new Error( `Error occurred with status code ${err.response?.status}, ${err.response?.statusText}` ); diff --git a/src/env.ts b/src/env.ts index 5136b21..4d99b0b 100644 --- a/src/env.ts +++ b/src/env.ts @@ -19,17 +19,19 @@ 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 = - ENVIRONMENT === "production" - ? assertValue( - process.env.PRODUCTION_PASS_KEY, - "Missing environment variable: PRODUCTION_PASS_KEY" - ) - : null; +export const MPESA_TRANSACTION_TYPE = assertValue( + process.env.MPESA_TRANSACTION_TYPE, + "Missing environment variable: MPESA_TRANSACTION_TYPE" +); + +export const PASSKEY = assertValue( + process.env.MPESA_API_PASS_KEY, + "Missing environment variable: PASSKEY" +); function assertValue(v: T | undefined, errorMessage: string): T { if (v === undefined) { 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..9e400e5 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,30 @@ 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(); + const accessTokenResponse = await generateAccessToken(); - const res: STKPushResponse = await axios.post( + const res = await axios.post( `${BASE_URL}/mpesa/stkpush/v1/processrequest`, stkPushBody, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${accessTokenResponse.access_token}`, }, } ); - return res; + return res.data; } 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..7f89c12 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..c381c32 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,15 @@ export const generatePassword = ( timestamp: string ): string => { const concatenatedString = `${businessShortCode}${passkey}${timestamp}`; - const encodedString = encode(concatenatedString); - return encodedString; + // Check if the environment is Node.js + if (typeof btoa === "undefined") { + // Node.js environment + const encodedString = Buffer.from(concatenatedString).toString("base64"); + return encodedString; + } else { + // Browser environment + const encodedString = btoa(concatenatedString); + return encodedString; + } };