diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 0000000..b3c96d2 --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,60 @@ +# Contribution Guidelines + +Thank you for your interest in contributing to Daraja Multiplatform! This project is aims to streamline the integration of M-Pesa services across multiple platforms by providing a simplified interface to the [Daraja API](https://developer.safaricom.co.ke/APIs). All kinds of contributions are welcome, whether it's fixing bugs, adding new features, or improving documentation. + +## Getting Started + +To get started with contributing to this project: + +- Familiarize yourself with the project by going over the project [documentation](https://victorkabata.github.io/DarajaMultiplatform/) and exploring the [codebase](https://github.com/VictorKabata/DarajaMultiplatform/tree/main). +- Click the ["Fork"](https://github.com/VictorKabata/DarajaMultiplatform/fork) button on the Daraja Multiplatform repository page on GitHub. This creates a copy of the repository in your own account. +- Clone your forked repository to your local machine using Git. +```curl +git clone https://github.io/your-username/DarajaMultiplatform.git +``` +- Follow the [project's setup instructions]() to set up the development enviroment. + +## How to Contribute + +### Reporting Bugs + +If you encounter a bug, please [create a new issue](https://github.com/VictorKabata/DarajaMultiplatform/issues/new/choose) and include as much detail as possible, such as: + +- Steps to reproduce the issue. +- Expected outcome vs actual outcome. +- Relevant screenshots or error logs (if applicable). + +### Suggesting Features +For feature requests, [submit a new issue](https://github.com/VictorKabata/DarajaMultiplatform/issues/new/choose) and outline: +- The feature you’re proposing. +- Why it would improve the project. +- Any ideas on implementation(if available). + +### Making Changes +If you'd like to contribute code: +- Open an issue to discuss your idea and get feedback before you begin. +- Create a new branch for your specific contribution with a descriptive name that reflects the changes you plan to make. +```bash +git checkout -b feature/your-feature-name +``` + +- Make your changes: Write clear, concise, and well-documented code. Use clear and concise commit messages that describe the changes you've made in each commit to maintain a clean commit history. +- Ensure your changes don't break any existing functionalities by running the tests provided in the project and adding new test cases. +- Push your changes to your forked repository branch and open a pull request from your branch to the __develop__ branch of the upstream repository. All pull requests description must follow this [PR template](https://github.com/VictorKabata/DarajaMultiplatform/blob/develop/.github/pull_request_template.md). + +### Code Style + +This project uses ktLint and detekt to enforce code style and quality standards in all Kotlin files. Following these standards helps maintain a consistent codebase and makes the code easier to read and review. + +Run the lint check commands locally before commiting changes to catch any linting issues early + +```bash +./gradlew :daraja:ktlintFormat +``` + +```bash +./gradlew :daraja:detekt +``` + +## License +By contributing, you agree that your contributions will be licensed under the same license as the project. diff --git a/.github/workflows/publish_kmp_lib.yml b/.github/workflows/publish_kmp_lib.yml index 80a975f..4d443b9 100644 --- a/.github/workflows/publish_kmp_lib.yml +++ b/.github/workflows/publish_kmp_lib.yml @@ -54,4 +54,14 @@ jobs: POM_SCM_URL: ${{ secrets.POM_SCM_URL }} SIGNING_ID: ${{ secrets.SIGNING_ID }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} - SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} \ No newline at end of file + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + + - name: Create Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + draft: true + prerelease: true + tag_name: ${{ github.event.inputs.versionName }} + name: Release ${{ github.event.inputs.versionName }} \ No newline at end of file diff --git a/.github/workflows/publish_swift_package.yml b/.github/workflows/publish_swift_package.yml index 7d2013e..6d74e0f 100644 --- a/.github/workflows/publish_swift_package.yml +++ b/.github/workflows/publish_swift_package.yml @@ -2,10 +2,6 @@ name: Create and Publish Swift Package on: workflow_dispatch: - inputs: - iOSVersionName: - description: 'iOS Version Name (eg. 0.9.0)' - required: true jobs: build: @@ -60,5 +56,4 @@ jobs: source-directory: 'swiftpackage' destination-github-username: 'VictorKabata' destination-repository-name: 'DarajaSwiftPackage' - create-target-branch-if-needed: true - target-branch: ${{ github.event.inputs.iOSVersionName }} \ No newline at end of file + create-target-branch-if-needed: true \ No newline at end of file diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt index 6c3af62..eee7a56 100644 --- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt +++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt @@ -18,25 +18,17 @@ package com.vickbt.darajakmp import com.vickbt.darajakmp.network.DarajaApiService import com.vickbt.darajakmp.network.DarajaHttpClientFactory -import com.vickbt.darajakmp.network.models.C2BRegistrationRequest -import com.vickbt.darajakmp.network.models.C2BRequest -import com.vickbt.darajakmp.network.models.C2BResponse -import com.vickbt.darajakmp.network.models.DarajaException import com.vickbt.darajakmp.network.models.DarajaToken -import com.vickbt.darajakmp.network.models.DarajaTransactionRequest -import com.vickbt.darajakmp.network.models.DarajaTransactionResponse import com.vickbt.darajakmp.network.models.DynamicQrRequest import com.vickbt.darajakmp.network.models.DynamicQrResponse import com.vickbt.darajakmp.network.models.MpesaExpressRequest import com.vickbt.darajakmp.network.models.MpesaExpressResponse import com.vickbt.darajakmp.network.models.QueryMpesaExpressRequest import com.vickbt.darajakmp.network.models.QueryMpesaExpressResponse -import com.vickbt.darajakmp.utils.C2BResponseType import com.vickbt.darajakmp.utils.DarajaEnvironment import com.vickbt.darajakmp.utils.DarajaResult import com.vickbt.darajakmp.utils.DarajaTransactionCode import com.vickbt.darajakmp.utils.DarajaTransactionType -import com.vickbt.darajakmp.utils.capitalize import com.vickbt.darajakmp.utils.getDarajaPassword import com.vickbt.darajakmp.utils.getDarajaPhoneNumber import com.vickbt.darajakmp.utils.getDarajaTimestamp @@ -56,9 +48,9 @@ import kotlin.native.ObjCName * */ @ObjCName(swiftName = "Daraja") class Daraja( - private val consumerKey: String?, - private val consumerSecret: String?, - private val passKey: String?, + private val consumerKey: String, + private val consumerSecret: String, + private val passKey: String, private val environment: DarajaEnvironment? = DarajaEnvironment.SANDBOX_ENVIRONMENT, ) { private val darajaHttpClientFactory: HttpClient = @@ -74,10 +66,10 @@ class Daraja( * @param [environment] * */ data class Builder( - @ObjCName(swiftName = "consumerKey") private var consumerKey: String? = null, - @ObjCName(swiftName = "consumerSecret") private var consumerSecret: String? = null, - @ObjCName(swiftName = "passKey") private var passKey: String? = null, - @ObjCName(swiftName = "darajaEnvironment") private var environment: DarajaEnvironment? = null, + @ObjCName(swiftName = "consumerKey") private var consumerKey: String, + @ObjCName(swiftName = "consumerSecret") private var consumerSecret: String, + @ObjCName(swiftName = "passKey") private var passKey: String, + @ObjCName(swiftName = "darajaEnvironment") private var environment: DarajaEnvironment = DarajaEnvironment.SANDBOX_ENVIRONMENT, ) { /**Provides [consumerKey] provided by Daraja API * @@ -101,10 +93,10 @@ class Daraja( fun setPassKey(passKey: String) = apply { this.passKey = passKey } /**Set Daraja API environment to Sandbox/Testing mode*/ - fun isSandbox() = apply { this.environment = DarajaEnvironment.SANDBOX_ENVIRONMENT } + fun setSandboxEnvironment() = apply { this.environment = DarajaEnvironment.SANDBOX_ENVIRONMENT } /**Set Daraja API environment to Production/Live mode*/ - fun isProduction() = apply { this.environment = DarajaEnvironment.PRODUCTION_ENVIRONMENT } + fun setProductionEnvironment() = apply { this.environment = DarajaEnvironment.PRODUCTION_ENVIRONMENT } /**Create an instance of [Daraja] object with [consumerKey], [consumerSecret] and [passKey] provided*/ @ObjCName(swiftName = "init") @@ -122,11 +114,9 @@ class Daraja( DarajaApiService( httpClient = darajaHttpClientFactory, consumerKey = - consumerKey - ?: throw DarajaException(errorMessage = "Consumer key is null"), + consumerKey, consumerSecret = - consumerSecret - ?: throw DarajaException(errorMessage = "Consumer secret is null"), + consumerSecret, ) /**Request access token that is used to authenticate to Daraja APIs @@ -168,7 +158,7 @@ class Daraja( val darajaPassword = getDarajaPassword( shortCode = businessShortCode, - passkey = passKey ?: throw DarajaException(errorMessage = "Pass key is null"), + passkey = passKey, timestamp = timestamp, ) @@ -205,7 +195,7 @@ class Daraja( val darajaPassword = getDarajaPassword( shortCode = businessShortCode, - passkey = passKey ?: "", + passkey = passKey, timestamp = timestamp, ) @@ -261,93 +251,4 @@ class Daraja( darajaApiService.generateDynamicQr(dynamicQrRequest = dynamicQrRequest) } - - /**Request the status of an Mpesa payment transaction - * - * @param [businessShortCode] This is organizations shortcode (Paybill or Buy Goods - A 5 to 7 digit account number) used to identify an organization and receive the transaction. - * @param [checkoutRequestID] This is a global unique identifier of the processed checkout transaction request. - * - * @return [DarajaTransactionResponse] - * */ - @ObjCName(swiftName = "transactionStatus") - internal fun transactionStatus( - businessShortCode: String, - checkoutRequestID: String, - ): DarajaResult = - runBlocking(Dispatchers.IO) { - val timestamp = Clock.System.now().getDarajaTimestamp() - val darajaPassword = - getDarajaPassword( - shortCode = businessShortCode, - passkey = passKey ?: throw DarajaException(errorMessage = "Pass key is null"), - timestamp = timestamp, - ) - - val darajaTransactionRequest = - DarajaTransactionRequest( - businessShortCode = businessShortCode, - password = darajaPassword, - timestamp = timestamp, - checkoutRequestID = checkoutRequestID, - ) - - darajaApiService.queryTransaction(darajaTransactionRequest) - } - - /**Transact between a phone number registered on M-Pesa to an M-Pesa shortcode - * - * @param [businessShortCode] A unique number is tagged to an M-PESA pay bill/till number of the organization. - * @param [confirmationURL] This is the URL that receives the confirmation request from API upon payment completion. - * @param [validationURL] This is the URL that receives the validation request from the API upon payment submission. The validation URL is only called if the external validation on the registered shortcode is enabled. (By default External Validation is disabled). - * @param [responseType] This parameter specifies what is to happen if for any reason the validation URL is not reachable. Note that, this is the default action value that determines what M-PESA will do in the scenario that your endpoint is unreachable or is unable to respond on time. Only two values are allowed: Completed or Cancelled. Completed means M-PESA will automatically complete your transaction, whereas Cancelled means M-PESA will automatically cancel the transaction, in the event M-PESA is unable to reach your Validation URL. - * - * @return [C2BRegistrationResponse] - * */ - fun c2bRegistration( - businessShortCode: String, - confirmationURL: String, - validationURL: String, - responseType: C2BResponseType = C2BResponseType.COMPLETED, - ): DarajaResult = - runBlocking(Dispatchers.IO) { - val c2BRegistrationRequest = - C2BRegistrationRequest( - confirmationURL = confirmationURL, - validationURL = validationURL, - responseType = - if (validationURL.isEmpty()) { - C2BResponseType.COMPLETED.name.capitalize() - } else { - responseType.name.capitalize() - }, - shortCode = businessShortCode, - ) - - darajaApiService.c2bRegistration(c2bRegistrationRequest = c2BRegistrationRequest) - } - - fun c2b( - amount: Int, - billReferenceNumber: String? = null, - transactionType: DarajaTransactionType, - phoneNumber: String, - businessShortCode: String, - ): DarajaResult = - runBlocking(Dispatchers.IO) { - val c2bRequest = - C2BRequest( - amount = amount, - billReferenceNumber = - if (transactionType.name == DarajaTransactionType.CustomerPayBillOnline.name) { - billReferenceNumber - } else { - null - }, - commandID = transactionType.name, - phoneNumber = phoneNumber.getDarajaPhoneNumber().toLong(), - shortCode = businessShortCode, - ) - - darajaApiService.c2b(c2bRequest = c2bRequest) - } } diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt index 70388cf..379768d 100644 --- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt +++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt @@ -54,7 +54,7 @@ internal class DarajaApiService( private val consumerSecret: String, private val inMemoryCache: Cache = Cache.Builder() - .expireAfterWrite(3600.seconds).build(), + .expireAfterWrite(3000.seconds).build(), ) { /** Initiate API call using the [httpClient] provided by Ktor to fetch Daraja API access token * of type [DarajaToken]*/ diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt index 8d61c97..e8f9767 100644 --- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt +++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt @@ -30,7 +30,7 @@ import io.ktor.util.network.UnresolvedAddressException * an instance of [DarajaResult] with exception of type [DarajaException] on failure * * @return [DarajaResult] Returns data of type [T] on success - * @throws DarajaException Throws expception of type [DarajaException] on failure + * @throws DarajaException Throws exception of type [DarajaException] on failure * */ internal suspend fun darajaSafeApiCall(apiCall: suspend () -> T): DarajaResult = try { @@ -52,6 +52,11 @@ internal suspend fun darajaSafeApiCall(apiCall: suspend () -> T): Dara DarajaResult.Failure(exception = error) } +fun some() = + runCatching { + }.map { + } + /**Generate [DarajaException] from network or system error when making network calls * * @throws [DarajaException] diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRegistrationRequest.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRegistrationRequest.kt index 0225064..5805276 100644 --- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRegistrationRequest.kt +++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRegistrationRequest.kt @@ -28,7 +28,7 @@ import kotlin.native.ObjCName * @param [responseType] This parameter specifies what is to happen if for any reason the validation URL is not reachable. Note that, this is the default action value that determines what M-PESA will do in the scenario that your endpoint is unreachable or is unable to respond on time. Only two values are allowed: `Completed` or `Cancelled`. `Completed` means M-PESA will automatically complete your transaction, whereas `Cancelled` means M-PESA will automatically cancel the transaction, in the event M-PESA is unable to reach your Validation URL. * @param [shortCode] A unique number is tagged to an M-PESA pay bill/till number of the organization. * */ -data class C2BRegistrationRequest( +internal data class C2BRegistrationRequest( @SerialName("ConfirmationURL") val confirmationURL: String, @SerialName("ValidationURL") diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRequest.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRequest.kt index 9825b6c..f4885bd 100644 --- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRequest.kt +++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRequest.kt @@ -23,7 +23,7 @@ import kotlin.native.ObjCName @ObjCName(swiftName = "C2BRequest") @Serializable /**Request C2B M-Pesa payment*/ -data class C2BRequest( +internal data class C2BRequest( @SerialName("Amount") val amount: Int, @SerialName("BillRefNumber") diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BResponse.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BResponse.kt index 04da16b..e959d10 100644 --- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BResponse.kt +++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BResponse.kt @@ -27,7 +27,7 @@ import kotlin.native.ObjCName * @param [responseCode] It indicates whether Mobile Money accepts the request or not. * @param [responseDescription] This is the status of the request. * */ -data class C2BResponse( +internal data class C2BResponse( @SerialName("OriginatorCoversationID") val originatorConversationId: String, @SerialName("ResponseCode") diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt index 1445ff7..da0cef0 100644 --- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt +++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt @@ -40,7 +40,7 @@ enum class DarajaEnvironment { SANDBOX_ENVIRONMENT, } -enum class C2BResponseType { +internal enum class C2BResponseType { CANCELED, COMPLETED, }