diff --git a/.github/workflows/Enforce_Labels.yml b/.github/workflows/Enforce_Labels.yml new file mode 100644 index 0000000..5d9bcfd --- /dev/null +++ b/.github/workflows/Enforce_Labels.yml @@ -0,0 +1,14 @@ +name: Enforce PR labels + +on: + pull_request: + types: [labeled, unlabeled, opened, edited, synchronize] +jobs: + enforce-label: + runs-on: ubuntu-latest + steps: + - uses: yogevbd/enforce-label-action@2.1.0 + with: + REQUIRED_LABELS_ANY: "bug,enhancement,skip-changelog" + REQUIRED_LABELS_ANY_DESCRIPTION: "Select at least one label ['bug','enhancement','skip-changelog']" + BANNED_LABELS: "banned" diff --git a/.github/workflows/SonarCloud.yml b/.github/workflows/SonarCloud.yml new file mode 100644 index 0000000..61298a5 --- /dev/null +++ b/.github/workflows/SonarCloud.yml @@ -0,0 +1,37 @@ +name: Sonar Cloud +on: + schedule: + - cron: '0 0 * * *' + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew JacocoTestReport sonarqube --scan + diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d47ed..78dd761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,28 @@ # Change Log -Last updated Sep 5, 2016 +Last updated May 05, 2018 (from http://keepachangelog.com/) All notable changes to this project will be documented in this file. + +## Released v3.0.12 on May 08, 2019 +- Update dependencies + +## Released v3.0.11 on May 05, 2018 +- fix - Handle auth=pin + +## Released v3.0.10 on Apr 10, 2018 +- Allow adding JSONObject to metadata + +## Released v3.0.8 on Nov 4, 2017 +- Resolved issue that caused build to fail when using AAPT2 + +## Released v3.0.7 on Oct 27, 2017 +- Invalid card details will force us to show a popup that allows the user correct their card entry +- Updated all dependencies +- In debug mode, an assertion has been added to force the developer to choose either remote or local +transaction initialization exclusively. +- Added utility method to strip card of all non-numeric characters + ## Released v3.0.5 on Jun 20, 2017 - Fixed a bug that made some verve cards get bounced by the validator - Other minor bug fixes diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..30cec81 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,207 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.3) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.1.1) + aws-partitions (1.448.0) + aws-sdk-core (3.114.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.43.0) + aws-sdk-core (~> 3, >= 3.112.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.94.0) + aws-sdk-core (~> 3, >= 3.112.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.3) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + declarative (0.0.20) + digest-crc (0.6.3) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.6) + emoji_regex (3.2.2) + excon (0.80.1) + faraday (1.4.1) + faraday-excon (~> 1.1) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + multipart-post (>= 1.2, < 3) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-excon (1.1.0) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.1.0) + faraday_middleware (1.0.0) + faraday (~> 1.0) + fastimage (2.2.3) + fastlane (2.181.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.37.0, < 0.39.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + naturally (~> 2.2) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + gh_inspector (1.1.3) + google-api-client (0.38.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-apis-core (0.3.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.14) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + rexml + signet (~> 0.14) + webrick + google-apis-iamcredentials_v1 (0.3.0) + google-apis-core (~> 0.1) + google-apis-storage_v1 (0.3.0) + google-apis-core (~> 0.1) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.5.0) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.1.0) + google-cloud-storage (1.31.0) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.2) + googleauth (~> 0.9) + mini_mime (~> 1.0) + googleauth (0.16.1) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.14) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.4.0) + json (2.5.1) + jwt (2.2.3) + memoist (0.16.2) + mini_magick (4.11.0) + mini_mime (1.1.0) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + naturally (2.2.1) + os (1.1.1) + plist (3.6.0) + public_suffix (4.0.6) + rake (13.0.3) + representable (3.1.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby2_keywords (0.0.4) + rubyzip (2.3.0) + security (0.1.3) + signet (0.15.0) + addressable (~> 2.3) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.1) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + webrick (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.19.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + x86_64-darwin-19 + x86_64-linux + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.2.15 diff --git a/HISTORY.md b/HISTORY.md index 030ae92..2a1b807 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,19 @@ # History -Last updated Apr 7, 2017 +Last updated Nov 4, 2017 + +## Commits on Nov 4, 2017 +- Resolve issue that caused build to fail when using AAPT2 + +## Commits on Oct 27, 2017 +- Invalid card details will force us to show a popup that allows the user correct their card entry +- Updated all dependencies +- In debug mode, an assertion has been added to force the developer to choose either remote or local +transaction initialization exclusively. +- Added utility method to strip card of all non-numeric characters + +## Commits on Jun 20, 2017 +- Fixed a bug that made some verve cards get bounced by the validator +- Other minor bug fixes ## Commits on Apr 7, 2017 - `chargeCard` now resumes a transaction initialized by server diff --git a/README.md b/README.md index 8ce2b91..74594be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -[![API](https://img.shields.io/badge/API-16%2B-blue.svg?style=plastic)](https://android-arsenal.com/api?level=16) +[![Maven Central](https://img.shields.io/maven-central/v/co.paystack.android/paystack.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22co.paystack.android%22%20AND%20a:%22paystack%22) +[![Min API](https://img.shields.io/badge/API-16%2B-blue.svg?style=plastic)](https://android-arsenal.com/api?level=16) +[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/dashboard?id=PaystackHQ_paystack-android) @@ -12,21 +14,17 @@ card data directly to our servers. ## Summarized flow -1 Time to pay (user has provided card details on your app) +1. Collect user's card details -2 OPTION 1: Backend starts transaction (recommended) +2. Initialize the transaction + - App prompts your backend to initialize a transaction + - Your backend returns `access_code` we return when it calls the [Initialize Transaction](https://paystack.com/docs/api/#transaction-initialize) endpoint + - App provides the `access_code` and card details to our SDK's `chargeCard` function via `Charge` object -a. App prompts backend to initialize a transaction, backend returns `access_code`. Backend will use this endpoint: https://developers.paystack.co/reference#initialize-a-transaction -b. Provide `access_code` and card details to our SDK's `chargeCard` function via `Charge` object +3. SDK prompts user for PIN, OTP or Bank authentication as required -2 OPTION 2: App starts transaction - -a. Provide transaction parameters and card details to our SDK's `chargeCard` function via `Charge` object - -3 SDK will prompt user for PIN, OTP or Bank authentication as required - -4 Once successful, we will send event to your webhook url and call onSuccess callback +4. Once successful, we'll send an event to your webhook URL and call `onSuccess` callback. You should give value via webhook. ## Requirements - Android SDKv16 (Android 4.1 "Jelly Bean") - This is the first SDK version that includes @@ -36,11 +34,29 @@ API 16 will not be available. ## Installation ### Android Studio (using Gradle) -You do not need to clone this repository or download the files. Just add the following lines to your app's `build.gradle`: +You do not need to clone this repository or download the files. The latest build is available on Maven Central +Add the following lines to your app's `build.gradle`: ```gradle dependencies { - compile 'co.paystack.android:paystack:3.0.5' + implementation 'co.paystack.android:paystack:3.1.3' +} +``` +>From version `3.0.18`, the Pinpad library comes as part of this library and does not need to be explicitly included in your dependencies. + +You should also add Java 8 support in your `build.gradle`: +```gradle +android { + // ... Other configuration code + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + // For kotlin codebases, include + kotlinOptions { + jvmTarget = "1.8" + } } ``` @@ -48,12 +64,12 @@ dependencies { To use this library with Eclipse, you need to: 1. Clone the repository. -2. Import the **Paystack** folder into your [Eclipse](http://help.eclipse.org/juno/topic/org.eclipse.platform.doc.user/tasks/tasks-importproject.htm) project -3. In your project settings, add the **Paystack** project under the Libraries section of the Android category. +2. Import the **paystack** folder into your [Eclipse](http://help.eclipse.org/juno/topic/org.eclipse.platform.doc.user/tasks/tasks-importproject.htm) project +3. In your project settings, add the **paystack** project under the Libraries section of the Android category. ## Usage -### 0. Prepare for use +### 0) Prepare for use To prepare for use, you must ensure that your app has internet permissions by making sure the `uses-permission` line below is present in the AndroidManifest.xml. @@ -61,7 +77,7 @@ To prepare for use, you must ensure that your app has internet permissions by ma ``` -### 1. initializeSdk +### 1) Initialize SDK To use the Paystack Android SDK, you need to first initialize it using the `PaystackSdk` class. @@ -78,19 +94,20 @@ public class App extends Application{ Make sure to call this method in the `onCreate` method of your Fragment or Activity or Application. -### 2. Set your Public Key +### 2) Set your Public Key -Before you can charge a card with the `PaystackSdk` class, you need to set your public key. The library provides two approaches, +Before you can charge a card with the `PaystackSdk` class, you need to set your public key. The library provides two approaches: -#### a. Add the following lines to the `` tag of your AndroidManifest.xml +#### a) Add the following lines to the `` tag of your AndroidManifest.xml ```xml + android:value="pk_your_public_key"/> ``` +> You can obtain your public key from your [Paystack dashboard](https://dashboard.paystack.co/#/settings/developer). -#### b. Set the public key by code +#### b) Set the public key by code This can be done anytime in your code. Just be sure to initialize before calling `chargeCard`. @@ -101,7 +118,7 @@ class Bootstrap { } } ``` -### 3. Collect and validate card details +### 3) Collect and validate card details At this time, we expect you to provide fields on your activity that collect the card details. Our `Card` class allows you collect and verify these. The library provides validation methods to validate the fields of the card. @@ -117,11 +134,30 @@ Method checks if the expiry date (combination of year and month) is valid. #### card.isValid Method to check if the card is valid. Always do this check, before charging the card. + #### card.getType This method returns an estimate of the string representation of the card type. +```java +public class MainActivity extends AppCompatActivity { -### 4. chargeCard + // This sets up the card and check for validity + // This is a test card from paystack + String cardNumber = "4084084084084081"; + int expiryMonth = 11; //any month in the future + int expiryYear = 18; // any year in the future. '2018' would work also! + String cvv = "408"; // cvv of the test card + + Card card = new Card(cardNumber, expiryMonth, expiryYear, cvv); + if (card.isValid()) { + // charge card + } else { + //do something + } +} +``` + +### 4) Charge Card Charging with the PaystackSdk is quite straightforward. #### Parameters for the chargeCard function @@ -134,13 +170,13 @@ open Activity is just fine. `chargeCard`, you should do a `charge.setCard(card)`. The charge can then be used in either of 2 ways - **Resume an initialized transaction**: If employing this flow, you would send all required parameters for the transaction from your backend to the Paystack API via the `transaction/initialize` call - - documented [here](https://developers.paystack.co/reference#initialize-a-transaction).. The + documented [here](https://paystack.com/docs/api/#transaction-initialize). The response of the call includes an `access_code`. This can be used to charge the card by doing `charge.setAccessCode({value from backend})`. Once an access code is set, the only other parameter relevant to the transaction is the card. Others will be ignored. - **Initiate a fresh transaction on Paystack**: Using the functions: `setCurrency`, `setPlan`, `setSubaccount`, `setTransactionCharge`, `setAmount`, `setEmail`, `setReference`, `setBearer`, - `putMetadata`, `putCustomField`, you can set up a fresh transaction direct from the SDK. + `putMetadata`, `putCustomField`, you can set up a fresh transaction directly from the SDK. Documentation for these parameters are same as for `transaction/initialize`. @@ -155,10 +191,15 @@ the methods available in the callback you provided. ```java public class MainActivity extends AppCompatActivity { - + + // This is the subroutine you will call after creating the charge // adding a card and setting the access_code public void performCharge(){ + //create a Charge object + Charge charge = new Charge(); + charge.setCard(card); //sets the card to charge + PaystackSdk.chargeCard(MainActivity.this, charge, new Paystack.TransactionCallback() { @Override public void onSuccess(Transaction transaction) { @@ -173,6 +214,12 @@ public class MainActivity extends AppCompatActivity { // Save reference so you may send to server. If // error occurs with OTP, you should still verify on server. } + + @Override + public void showLoading(Boolean isProcessing) { + // This is called whenever the SDK makes network requests. + // Use this to display loading indicators in your application UI + } @Override public void onError(Throwable error, Transaction transaction) { @@ -184,15 +231,14 @@ public class MainActivity extends AppCompatActivity { } ``` -Note that once `chargeCard` is called, depending on settings agreed with Paystack's Customer Success team, the SDK _may_ prompt the user to provide their PIN, an OTP or conclude Bank Authentication. These +> Note that once `chargeCard` is called, the SDK _may_ prompt the user to provide their PIN, an OTP or conclude Bank Authentication. These are currently being managed entirely by the SDK. Your app will only be notified via the `beforeValidate` function of the callback when OTP or Bank Authentication is about to start. -### 5. Verifying the transaction +### 5) Verifying the transaction Send the reference to your backend and verify by calling our REST API. An authorization will be returned which -will let you know if its code is reusable. You can learn more about our verify call - [here](https://developers.paystack.co/reference#verifying-transactions). +will let you know if its code is reusable. You can learn more about our [Verify Transaction](https://paystack.com/docs/api/#transaction-verify) endpoint. Below is a sample authorization object returned along with the transaction details: ```json @@ -265,13 +311,13 @@ Below is a sample authorization object returned along with the transaction detai 2. Confirm that the authorization is reusable by checking `data.authorization.reusable` which is true in this case. Once both pass, you can save the authorization code against the customer's email. -### 6. Charging a card authorization from your server in future -To charge an authorization saved from concluding chargeCard, you need its authorization code and the custmer's email. The `charge_authorization` endpoint is documented [here](https://developers.paystack.co/docs/charging-returning-customers). +### 6) Charging a card authorization from your server in future +To charge an authorization saved from concluding `chargeCard`, you need its authorization code and the customer's email. Our [Recurring Charge](https://paystack.com/docs/payments/recurring-charges) documentation provides more information about charging an authorization. ## Testing your implementation You can (and should) test your implementation of the Paystack Android library in your Android app. You need the details of an actual debit/credit card to do this, so we provide ##_test cards_## for your use instead of using your own debit/credit cards. -You may find test cards on [this Paystack documentation page](https://developers.paystack.co/docs/test-cards). +Kindly reference our [Test Payments](https://paystack.com/docs/payments/test-payments) documentation for test cards. To try out the OTP flow, we have provided a test "verve" card: diff --git a/build.gradle b/build.gradle index af52b0d..e04e25b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,32 +1,95 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.jacocoVersion = '0.8.7' + ext.versions = [ + 'kotlin' : '1.7.22', + 'coroutines' : '1.4.2', + 'pusher' : '2.2.1', + 'robolectric' : '4.9.2', + 'mockito' : '3.8.0', + 'mockito_kotlin': '2.2.0', + 'junit_ext' : '1.1.2', + ] + repositories { - jcenter() + mavenCentral() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.1' + classpath 'com.android.tools.build:gradle:7.4.0' classpath 'org.robolectric:robolectric-gradle-plugin:1.1.0' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } +plugins { + id "org.sonarqube" version "3.5.0.2730" +} + +sonarqube { + properties { + property "sonar.projectKey", "PaystackHQ_paystack-android" + property "sonar.organization", "paystackhq" + property "sonar.host.url", "https://sonarcloud.io" + property 'sonar.core.codeCoveragePlugin', 'jacoco' + property "sonar.coverage.jacoco.xmlReportPaths", "${project.projectDir}/paystack/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml" + property("sonar.sources", "src/main") + property("sonar.tests", "src/test") + } +} + allprojects { repositories { + mavenCentral() + google() jcenter() } + + configurations.all { + resolutionStrategy { + force 'org.objenesis:objenesis:2.6' + eachDependency { details -> + if (details.requested.group == 'org.jacoco') { + details.useVersion jacocoVersion + } + } + } + } } ext { - compileSdkVersion = 25 + compileSdkVersion = 29 minSdkVersion = 16 - targetSdkVersion = 25 - versionCode = 16 + targetSdkVersion = 29 + versionCode = 23 - buildToolsVersion = "25.0.1" - supportLibraryVersion = "25.3.1" - versionName = "3.0.5" -} \ No newline at end of file + buildToolsVersion = "29.0.2" + versionName = "3.3.2" +} + +Object getEnvOrDefault(String propertyName, Object defaultValue) { + // Check 'local.properties' first + String propertyValue + + def propFile = file('local.properties') + if (propFile.exists()) { + Properties localProps = new Properties() + localProps.load(propFile.newDataInputStream()) + propertyValue = localProps.getProperty(propertyName) + if (propertyValue != null) { + return propertyValue + } + } + + propertyValue = project.properties[propertyName] + + if (propertyValue == null) { + logger.error("Build property named {} not defined. Falling back to default value.", propertyName) + return defaultValue + } + + return propertyValue +} diff --git a/example/build.gradle b/example/build.gradle index 50628ce..3c8ee60 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,9 +1,37 @@ apply plugin: 'com.android.application' //apply plugin: 'org.robolectric' +apply plugin: 'jacoco' + +jacoco { + toolVersion = "$jacocoVersion" +} + +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true + jacoco.excludes = ['jdk.internal.*'] +} + +task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + + reports { + xml.enabled = true + html.enabled = true + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] + def debugTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug/classes", excludes: fileFilter) + def mainSrc = "$project.projectDir/src/main/java" + + sourceDirectories.setFrom(files([mainSrc])) + classDirectories.setFrom(files([debugTree])) + executionData.setFrom(fileTree(dir: project.buildDir, includes: [ + '**/*.exec', '**/*.ec' + ])) +} + android { compileSdkVersion rootProject.ext.compileSdkVersion - buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { applicationId "co.paystack.example" @@ -13,20 +41,34 @@ android { versionName rootProject.ext.versionName } buildTypes { + debug { + testCoverageEnabled false + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + testOptions { + unitTests.all { + jacoco { + includeNoLocationClasses = true + } + } + } } dependencies { - compile "com.android.support:appcompat-v7:$rootProject.ext.supportLibraryVersion" - compile fileTree(include: ['*.jar'], dir: 'libs') - testCompile 'junit:junit:4.12' - testCompile 'org.assertj:assertj-core:3.5.2' - testCompile 'org.robolectric:robolectric:3.1.2' - testCompile 'org.mockito:mockito-core:1.10.19' -// compile project(':paystack') - compile "co.paystack.android:paystack:$rootProject.ext.versionName" + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation fileTree(include: ['*.jar'], dir: 'libs') + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.assertj:assertj-core:3.12.2' + testImplementation 'org.robolectric:robolectric:4.5.1' + testImplementation 'org.mockito:mockito-core:3.8.0' + implementation project(':paystack') +// implementation "co.paystack.android:paystack:$rootProject.ext.versionName" } diff --git a/example/src/main/java/co/paystack/example/MainActivity.java b/example/src/main/java/co/paystack/example/MainActivity.java index 41303e5..ec7e989 100644 --- a/example/src/main/java/co/paystack/example/MainActivity.java +++ b/example/src/main/java/co/paystack/example/MainActivity.java @@ -3,7 +3,7 @@ import android.app.ProgressDialog; import android.os.AsyncTask; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import android.util.Log; import android.view.Menu; import android.view.View; import android.widget.Button; @@ -11,9 +11,14 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; + +import org.json.JSONException; + import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; +import java.util.Calendar; import co.paystack.android.Paystack; import co.paystack.android.PaystackSdk; @@ -30,10 +35,10 @@ public class MainActivity extends AppCompatActivity { // Step 2. Click "Deploy to heroku" // Step 3. Login with your heroku credentials or create a free heroku account // Step 4. Provide your secret key and an email with which to start all test transactions - // Step 5. Copy the url generated by heroku (format https://some-url.heroku-app.com) into the space below - String backend_url = ""; + // Step 5. Copy the url generated by heroku (format https://some-url.herokuapp.com) into the space below + String backend_url = "https://charge-sample-service.onrender.com"; // Set this to a public key that matches the secret key you supplied while creating the heroku instance - String paystack_public_key = ""; + String paystack_public_key = "pk_test_9eb0263ed776c4c892e0281348aee4136cd0dd52"; EditText mEditCardNum; EditText mEditCVC; @@ -43,13 +48,11 @@ public class MainActivity extends AppCompatActivity { TextView mTextError; TextView mTextBackendMessage; - Card card; - ProgressDialog dialog; private TextView mTextReference; private Charge charge; private Transaction transaction; - + private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -65,16 +68,17 @@ protected void onCreate(Bundle savedInstanceState) { PaystackSdk.setPublicKey(paystack_public_key); - mEditCardNum = (EditText) findViewById(R.id.edit_card_number); - mEditCVC = (EditText) findViewById(R.id.edit_cvc); - mEditExpiryMonth = (EditText) findViewById(R.id.edit_expiry_month); - mEditExpiryYear = (EditText) findViewById(R.id.edit_expiry_year); + mEditCardNum = findViewById(R.id.edit_card_number); + mEditCVC = findViewById(R.id.edit_cvc); + mEditExpiryMonth = findViewById(R.id.edit_expiry_month); + mEditExpiryYear = findViewById(R.id.edit_expiry_year); - Button mButtonPerformTransaction = (Button) findViewById(R.id.button_perform_transaction); + Button mButtonPerformTransaction = findViewById(R.id.button_perform_transaction); + Button mButtonPerformLocalTransaction = findViewById(R.id.button_perform_local_transaction); - mTextError = (TextView) findViewById(R.id.textview_error); - mTextBackendMessage = (TextView) findViewById(R.id.textview_backend_message); - mTextReference = (TextView) findViewById(R.id.textview_reference); + mTextError = findViewById(R.id.textview_error); + mTextBackendMessage = findViewById(R.id.textview_backend_message); + mTextReference = findViewById(R.id.textview_reference); //initialize sdk PaystackSdk.initialize(getApplicationContext()); @@ -83,113 +87,100 @@ protected void onCreate(Bundle savedInstanceState) { mButtonPerformTransaction.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - //validate form - validateCardForm(); - //check card validity - if (card != null && card.isValid()) { - dialog = new ProgressDialog(MainActivity.this); - dialog.setMessage("Performing transaction... please wait"); - dialog.setCancelable(true); - dialog.setCanceledOnTouchOutside(true); - - dialog.show(); - - try { - startAFreshCharge(); - } catch (Exception e) { - MainActivity.this.mTextError.setText(String.format("An error occured hwile charging card: %s %s", e.getClass().getSimpleName(), e.getMessage())); - } + try { + startAFreshCharge(false); + } catch (Exception e) { + MainActivity.this.mTextError.setText(String.format("An error occurred while charging card: %s %s", e.getClass().getSimpleName(), e.getMessage())); + + } } + }); + mButtonPerformLocalTransaction.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + try { + startAFreshCharge(true); + } catch (Exception e) { + MainActivity.this.mTextError.setText(String.format("An error occurred while charging card: %s %s", e.getClass().getSimpleName(), e.getMessage())); + + } } }); } - private void startAFreshCharge() { + private void startAFreshCharge(boolean local) { // initialize the charge charge = new Charge(); - // Perform transaction/initialize on our server to get an access code - // documentation: https://developers.paystack.co/reference#initialize-a-transaction + charge.setCard(loadCardFromForm()); - new fetchAccessCodeFromServer().execute(backend_url+"/new-access-code"); + dialog = new ProgressDialog(MainActivity.this); + dialog.setMessage("Performing transaction... please wait"); + dialog.show(); + if (local) { + // Set transaction params directly in app (note that these params + // are only used if an access_code is not set. In debug mode, + // setting them after setting an access code would throw an exception + + charge.setAmount(2000); + charge.setEmail("customer@email.com"); + charge.setReference("ChargedFromAndroid_" + Calendar.getInstance().getTimeInMillis()); + try { + charge.putCustomField("Charged From", "Android SDK"); + } catch (JSONException e) { + e.printStackTrace(); + } + chargeCard(); + } else { + // Perform transaction/initialize on our server to get an access code + // documentation: https://developers.paystack.co/reference#initialize-a-transaction + new fetchAccessCodeFromServer().execute(backend_url + "/new-access-code"); + } } /** * Method to validate the form, and set errors on the edittexts. */ - private void validateCardForm() { + private Card loadCardFromForm() { //validate fields - String cardNum = mEditCardNum.getText().toString().trim(); + Card card; - if (isEmpty(cardNum)) { - mEditCardNum.setError("Empty card number"); - return; - } + String cardNum = mEditCardNum.getText().toString().trim(); //build card object with ONLY the number, update the other fields later card = new Card.Builder(cardNum, 0, 0, "").build(); - if (!card.validNumber()) { - mEditCardNum.setError("Invalid card number"); - return; - } - - //validate cvc String cvc = mEditCVC.getText().toString().trim(); - if (isEmpty(cvc)) { - mEditCVC.setError("Empty cvc"); - return; - } //update the cvc field of the card card.setCvc(cvc); - //check that it's valid - if (!card.validCVC()) { - mEditCVC.setError("Invalid cvc"); - return; - } - //validate expiry month; String sMonth = mEditExpiryMonth.getText().toString().trim(); - int month = -1; + int month = 0; try { month = Integer.parseInt(sMonth); } catch (Exception ignored) { } - if (month < 1) { - mEditExpiryMonth.setError("Invalid month"); - return; - } - card.setExpiryMonth(month); String sYear = mEditExpiryYear.getText().toString().trim(); - int year = -1; + int year = 0; try { year = Integer.parseInt(sYear); } catch (Exception ignored) { } - - if (year < 1) { - mEditExpiryYear.setError("invalid year"); - return; - } - card.setExpiryYear(year); - //validate expiry - if (!card.validExpiryDate()) { - mEditExpiryMonth.setError("Invalid expiry"); - mEditExpiryYear.setError("Invalid expiry"); - } + return card; } @Override public void onPause() { super.onPause(); - if ((dialog != null) && dialog.isShowing()){ + if ((dialog != null) && dialog.isShowing()) { dialog.dismiss(); } dialog = null; @@ -221,13 +212,21 @@ public void beforeValidate(Transaction transaction) { updateTextViews(); } + @Override + public void showLoading(Boolean isProcessing) { + Log.i(TAG, "Paystack SDK loading: " + isProcessing); + if (isProcessing) { + Toast.makeText(MainActivity.this, "Processing...", Toast.LENGTH_LONG).show(); + } + } + @Override public void onError(Throwable error, Transaction transaction) { // If an access code has expired, simply ask your server for a new one // and restart the charge instead of displaying error MainActivity.this.transaction = transaction; if (error instanceof ExpiredAccessCodeException) { - MainActivity.this.startAFreshCharge(); + MainActivity.this.startAFreshCharge(false); MainActivity.this.chargeCard(); return; } @@ -281,7 +280,6 @@ protected void onPostExecute(String result) { super.onPostExecute(result); if (result != null) { charge.setAccessCode(result); - charge.setCard(card); chargeCard(); } else { MainActivity.this.mTextBackendMessage.setText(String.format("There was a problem getting a new access code form the backend: %s", error)); diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml index bde499d..5acc722 100644 --- a/example/src/main/res/layout/activity_main.xml +++ b/example/src/main/res/layout/activity_main.xml @@ -64,7 +64,8 @@ android:inputType="number" android:maxEms="3" android:maxLength="2" - android:text=""/> + android:text="" + android:layout_toStartOf="@id/divider_horizontal"/> + android:text="" + android:layout_toEndOf="@id/divider_horizontal"/>