diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..659323d1d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,22 @@ +version: 2 +jobs: + build: + working_directory: ~/code + docker: + - image: circleci/android:api-25-alpha + environment: + JVM_OPTS: -Xmx3200m + steps: + - checkout + - restore_cache: + key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} + - run: + name: Download Dependencies + command: ./gradlew androidDependencies + - save_cache: + paths: + - ~/.gradle + key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} + - run: + name: Run Tests + command: ./gradlew test \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..b3c80cb18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Smartphone (please complete the following information):** + - Device: [e.g. Nexus 5X] + - OS: [e.g. Android 5.0] + - Version [e.g. 0.1] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..066b2d920 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..027d76500 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +## Proposed changes + +Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. + +## Types of changes + +What types of changes does your code introduce to frames-android? +_Put an `x` in the boxes that apply_ + +* [ ] Bugfix (non-breaking change which fixes an issue) +* [ ] New feature (non-breaking change which adds functionality) +* [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Checklist + +_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ + +* [ ] Lint and unit tests pass locally with my changes +* [ ] I have added tests that prove my fix is effective or that my feature works +* [ ] I have added necessary documentation (if appropriate) + +## Further comments + +If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..18b3b814f --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +*.iml +.gradle +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild + +/demos/examples/.gradle +/demos/examples/local.properties +/demos/examples/.idea/libraries +/demos/examples/.idea/modules.xml +/demos/examples/.idea/workspace.xml +/demos/examples/.DS_Store +/demos/examples/build +/demos/examples/captures +/demos/examples/.externalNativeBuild + +/demos/googlepay_example/.gradle +/demos/googlepay_example/local.properties +/demos/googlepay_example/.idea/libraries +/demos/googlepay_example/.idea/modules.xml +/demos/googlepay_example/.idea/workspace.xml +/demos/googlepay_example/.DS_Store +/demos/googlepay_example/build +/demos/googlepay_example/captures +/demos/googlepay_example/.externalNativeBuild + +/demos/saved_cards_example/.gradle +/demos/saved_cards_example/local.properties +/demos/saved_cards_example/.idea/libraries +/demos/saved_cards_example/.idea/modules.xml +/demos/saved_cards_example/.idea/workspace.xml +/demos/saved_cards_example/.DS_Store +/demos/saved_cards_example/build +/demos/saved_cards_example/captures +/demos/saved_cards_example/.externalNativeBuild diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 000000000..6f2612655 Binary files /dev/null and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..30aa626c2 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 000000000..9716d2748 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..b6b87e186 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 000000000..dc4117c26 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + frames-android + Project frames-android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 000000000..e8895216f --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..cb1ac5604 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,42 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at integration@checkout.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..e5c61e014 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing Guidelines + +This document contains information and guidelines about contributing to this project. +Please read it before you start participating. + +**Topics** + +* [Asking Questions](#asking-questions) +* [Reporting Security Issues](#reporting-security-issues) +* [Reporting Issues](#reporting-other-issues) +* [Developers Certificate of Origin](#developers-certificate-of-origin) +* [Code of Conduct](#code-of-conduct) + +## [Code of Conduct](./.github/CODE_OF_CONDUCT.md) + +Checkout has adopted the Contributor Covenant Code of Conduct for this project. +Please read the text so that you understand how to conduct while contributing to this project. + +## Semantic Versioning + +frames-andoid use [SemVer](http://semver.org/) for versioning. + +## Reporting Issues + +A great way to contribute to the project +is to send a detailed issue when you encounter a problem. +We always appreciate a well-written, thorough bug report. + +Check that the project issues database +doesn't already include that problem or suggestion before submitting an issue. +If you find a match, add a quick "+1" or "I have this problem too." +Doing this helps prioritize the most common problems and requests. + +When reporting issues, please include the following: + +* The version of Android Studio you're using +* The version of Android or Java you're targeting +* The full output of any stack trace or compiler error +* A code snippet that reproduces the described behavior, if applicable +* Any other details that would be useful in understanding the problem + +This information will help us review and fix your issue faster. + +## Sending a Pull Request + +**Before submitting a pull request,** please make sure the following is done: + +1. Fork [the repository](https://github.com/checkout/frames-android) and create your branch from `master`. +2. If you've added code that should be tested, add tests! +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Format your code. +6. Make sure your code lints. + +### License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..91c60e860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Checkout.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..e4e43299b --- /dev/null +++ b/README.md @@ -0,0 +1,309 @@ +# Frames Android +[![CircleCI](https://circleci.com/gh/checkout/frames-android/tree/master.svg?style=svg)](https://circleci.com/gh/checkout/frames-android/tree/master) +[![](https://jitpack.io/v/checkout/frames-android.svg)](https://jitpack.io/#checkout/frames-android) + +## Requirements +- Android minimum SDK 16 + +## Demo + + + +## Installation + +```gradle +// project gradle file +allprojects { + repositories { + ... + maven { url 'https://jitpack.io' } + } +} +``` +```gradle +// module gradle file +dependencies { + implementation 'com.android.support:design:27.1.1' + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.android.volley:volley:1.0.0' + implementation 'com.github.checkout:frames-android:v2.0.0' +} +``` + +> You can find more about the installation [here](https://jitpack.io/#checkout/frames-android/v2.0.0) + +> Please keep in mind that the Jitpack repository should to be added to the project gradle file while the dependency should be added in the module gradle file. [(see more about gradle files)](https://developer.android.com/studio/build) + +## Usage + +### For using the module's UI you need to do the following: +
+ +**Step1** Add the module to your XML layout. +```xml + +``` + +**Step2** Include the module in your class. +```java + private PaymentForm mPaymentForm; // include the payment form + private CheckoutAPIClient mCheckoutAPIClient; // include the API client +``` + +**Step3** Create a callback for when the Payment form is submitted. +```java + OnSubmitForm mSubmitListener = new OnSubmitForm() { + @Override + public void onSubmit(CardTokenisationRequest request) { + mCheckoutAPIClient.generateToken(request); // send the request to generate the token + } + }; +``` + +**Step4** Create a callback for the tokenisation request +```java + CheckoutAPIClient.OnTokenGenerated mTokenListener = new CheckoutAPIClient.OnTokenGenerated() { + @Override + public void onTokenGenerated(CardTokenisationResponse token) { + // your token + } + @Override + public void onError(CardTokenisationFail error) { + // your error + } + }; +``` + +**Step5** Initialise the module +```java + // initialise the payment from + mPaymentForm = findViewById(R.id.checkout_card_form); + mPaymentForm.setSubmitListener(mSubmitListener); // set the callback for the form submission + + // initialise the api client + mCheckoutAPIClient = new CheckoutAPIClient( + this, // context + "pk_XXXXX", // your public key + Environment.SANDBOX + ); + mCheckoutAPIClient.setTokenListener(mTokenListener); // set the callback for tokenisation +``` + + +
+ +### For using the module's without the UI you need to do the following: +
+ +**Step1** Include the module in your class. +```java + private CheckoutAPIClient mCheckoutAPIClient; // include the module +``` + +**Step2** Create a callback. +```java + CheckoutAPIClient.OnTokenGenerated mTokenListener = new CheckoutAPIClient.OnTokenGenerated() { + @Override + public void onTokenGenerated(CardTokenisationResponse token) { + // your token + } + @Override + public void onError(CardTokenisationFail error) { + // your error + } + }; +``` + +**Step3** Initialise the module and pass the card details. +```java + mCheckoutAPIClient = new CheckoutAPIClient( + this, // context + "pk_XXXXX", // your public key + Environment.SANDBOX // the environment + ); + mCheckoutAPIClient.setTokenListener(mTokenListener); // pass the callback + + + // Pass the paylod and generate the token + mCheckoutAPIClient.generateToken( + new CardTokenisationRequest( + "4242424242424242", + "name", + "06", + "25", + "100", + new BillingModel( + "address line 1", + "address line 2", + "postcode", + "UK", + "city", + "state", + new PhoneModel( + "+44", + "07123456789" + ) + ) + ) + ); + +``` + +## Customisation Options +The module extends a **Frame Layout** so you can use XML attributes like: +```java + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@colors/your_color" +``` + +Moreover, the module inherits the **Theme.AppCompat.Light.DarkActionBar** style so if you want to customise the look of the payment form you can define you own style and theme the module with it: +```xml + + ... + +``` + +If you would like to allow users to input their billing details when completing the payment details you can simply use the folllowing method: +```java + mPaymentForm.includeBilling(true); // false value will hide the option +``` + +If you want to display only certain accepted card types you can select then in the following way: +```java + mPaymentForm.setAcceptedCard(new Cards[]{VISA, MASTERCARD}); +``` + +## Handle 3D Secure + +The module allows you to handle 3DSecure URLs within your mobile app. Here are the steps: + +> When you send a 3D secure charge request from your server you will get back a 3D Secure URL. You can then pass the 3D Secure URL to the module, to handle the verification. + +**Step1** Create a callback. +```java + PaymentForm.On3DSFinished m3DSecureListener = + new PaymentForm.On3DSFinished() { + + @Override + public void onSuccess(String token) { + // success + } + + @Override + public void onError(String errorMessage) { + // fail + } + }; +``` + +**Step2** Pass the callback to the module and handle 3D Secure +```java + mPaymentForm = findViewById(R.id.checkout_card_form); + mPaymentForm.set3DSListener(m3DSecureListener); // pass the callback + + mPaymentForm.handle3DS( + "https://sandbox.checkout.com/api2/v2/3ds/acs/687805", // the 3D Secure URL + "http://example.com/success", // the Redirection URL + "http://example.com/fail" // the Redirection Fail URL + ); +``` +> Keep in mind that the Redirection and Redirection Fail URLs are set in the Checkout Hub, but they can be overwritten in the charge request sent from your server. Keep in mind to provide the correct URLs to ensure a successful interaction. + +## Handle Google Pay + +The module allows you to handle a Google Pay token payload and retrieve a token, that can be used to create a charge from your backend. + +**Step1** Create a callback. +```java + + CheckoutAPIClient.OnGooglePayTokenGenerated mGooglePayListener = + new CheckoutAPIClient.OnGooglePayTokenGenerated() { + @Override + public void onTokenGenerated(GooglePayTokenisationResponse response) { + // success + } + + @Override + public void onError(GooglePayTokenisationFail error) { + // fail + } + }; +``` +**Step2** Pass the callback to the module and generate the token +```java + mCheckoutAPIClient = new CheckoutAPIClient( + context, // activity context + "pk_XXXXX", // your public key + Environment.SANDBOX // the environment + ); + mCheckoutAPIClient.setGooglePayListener(mGooglePayListener); // pass the callback + + mCheckoutAPIClient.generateGooglePayToken(payload); // the payload is the JSON string generated by GooglePay +``` + +## Objects found in callbacks +#### When deling with actions like generating a card token the callback will include the following objects. + +**For success -> CardTokenisationResponse** +
+This has the following getters: +```java + response.getId(); // the card token + response.getLiveMode(); // environment mode + response.getCreated(); // timestamp of creation + response.getUsed(); // show usage + response.getCard(); // card object containing card information and billing details +``` + +**For error -> CardTokenisationResponse** +
+This has the following getters: +```java + error.getEventId(); // a unique identifier for the event + error.getMessage(); // the error message + error.getErrorCode(); // the error code + error.getErrorMessageCodes(); // an array or strings with all error codes + error.getErrors(); // an array or strings with all error messages +``` + +#### When deling with actions like generating a token for a Google Pay payload the callback will include the following objects. + +**For success -> GooglePayTokenisationResponse** +
+This has the following getters: +```java + response.getToken(); // the token + response.getExpiration(); // the token exiration + response.getType(); // the token type +``` + +**For error -> GooglePayTokenisationFail** +
+This has the following getters: +```java + error.getRequestId(); // a unique identifier for the request + error.getErrorType(); // the error type + error.getErrorCodes(); // an array of strings with all the error codes +``` + +## License + +frames-android is released under the MIT license. diff --git a/android-sdk/.classpath b/android-sdk/.classpath new file mode 100644 index 000000000..eb19361b5 --- /dev/null +++ b/android-sdk/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/android-sdk/.gitignore b/android-sdk/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/android-sdk/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android-sdk/.project b/android-sdk/.project new file mode 100644 index 000000000..47f91f356 --- /dev/null +++ b/android-sdk/.project @@ -0,0 +1,23 @@ + + + android-sdk + Project android-sdk created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android-sdk/.settings/org.eclipse.buildship.core.prefs b/android-sdk/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 000000000..b1886adb4 --- /dev/null +++ b/android-sdk/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir=.. +eclipse.preferences.version=1 diff --git a/android-sdk/build.gradle b/android-sdk/build.gradle new file mode 100644 index 000000000..230c531b1 --- /dev/null +++ b/android-sdk/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.library' +apply plugin: 'android-maven' +group = 'com.checkout' +version = '1.0' + + +android { + compileSdkVersion 27 + defaultConfig { + minSdkVersion 16 + targetSdkVersion 27 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support:appcompat-v7:27.1.1' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation 'com.android.support:design:27.1.1' + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.android.volley:volley:1.0.0' +} diff --git a/android-sdk/proguard-rules.pro b/android-sdk/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/android-sdk/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android-sdk/src/androidTest/java/com/example/android_sdk/ExampleInstrumentedTest.java b/android-sdk/src/androidTest/java/com/example/android_sdk/ExampleInstrumentedTest.java new file mode 100644 index 000000000..d92481fde --- /dev/null +++ b/android-sdk/src/androidTest/java/com/example/android_sdk/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.android_sdk; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.example.android_sdk", appContext.getPackageName()); + } +} diff --git a/android-sdk/src/main/AndroidManifest.xml b/android-sdk/src/main/AndroidManifest.xml new file mode 100644 index 000000000..179ce0ddc --- /dev/null +++ b/android-sdk/src/main/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/CheckoutAPIClient.java b/android-sdk/src/main/java/com/checkout/android_sdk/CheckoutAPIClient.java new file mode 100644 index 000000000..c9af3b5cc --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/CheckoutAPIClient.java @@ -0,0 +1,144 @@ +package com.checkout.android_sdk; + +import android.content.Context; + +import com.checkout.android_sdk.Request.CardTokenisationRequest; +import com.checkout.android_sdk.Request.GooglePayTokenisationRequest; +import com.checkout.android_sdk.Response.CardTokenisationFail; +import com.checkout.android_sdk.Response.CardTokenisationResponse; +import com.checkout.android_sdk.Response.GooglePayTokenisationFail; +import com.checkout.android_sdk.Response.GooglePayTokenisationResponse; +import com.checkout.android_sdk.Utils.Environment; +import com.checkout.android_sdk.Utils.HttpUtils; +import com.google.gson.Gson; + +import org.json.JSONException; +import org.json.JSONObject; + +public class CheckoutAPIClient { + + private String key; + + /** + * This is interface used as a callback for when the card token is generated + */ + public interface OnTokenGenerated { + void onTokenGenerated(CardTokenisationResponse response); + + void onError(CardTokenisationFail error); + } + + /** + * This is interface used as a callback for when the google pay token is generated + */ + public interface OnGooglePayTokenGenerated { + void onTokenGenerated(GooglePayTokenisationResponse response); + + void onError(GooglePayTokenisationFail error); + } + + private Context mContext; + private Environment mEnvironment = Environment.SANDBOX; + private CheckoutAPIClient.OnTokenGenerated mTokenListener; + private CheckoutAPIClient.OnGooglePayTokenGenerated mGooglePayTokenListener; + + + public CheckoutAPIClient(Context context, String key, Environment environment) { + this.mContext = context; + this.key = key; + this.mEnvironment = environment; + } + + public CheckoutAPIClient(Context context, String key) { + this.mContext = context; + this.key = key; + } + + /** + * This method is used to generate a card token. + *

+ * It takes a {@link CardTokenisationRequest} as the argument and it will perform a + * HTTP Post request to generate the token. it is important to you select an environment and + * provide your public key before calling tis method. Moreover it is important to set a callback + * {@link CheckoutAPIClient.OnTokenGenerated} so you can receive the token back. + *

+ * If you are using the UI of the SDK this method will be called automatically, but you still + * need to provide the callback, key and environment when initialising this class + * + * @param request Custom request body to be used in the HTTP call. + */ + public void generateToken(CardTokenisationRequest request) { + + // Initialise the HTTP utility class + HttpUtils http = new HttpUtils(mContext); + + // Provide a callback for when the token request is completed + http.setTokenListener(mTokenListener); + + // Using Gson to convert the custom request object into a JSON string for use in the HTTP call + Gson gson = new Gson(); + String jsonBody = gson.toJson(request); + + try { + http.generateToken(key, mEnvironment.token, jsonBody); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + /** + * This method used to create a token that can be used in a server environment to create a + * charge. It will take a GooglePay payload in JSON string format. The payload is usually generated in + * the handlePaymentSuccess method shown in the Google Pay example from Google (token.getToken()) + * + * @param payload Google Pay Payload + */ + public void generateGooglePayToken(String payload) throws JSONException { + + JSONObject googlePayToken = new JSONObject(payload); + + // Initialise the HTTP utility class + HttpUtils http = new HttpUtils(mContext); + + // Provide a callback for when the token request is completed + http.setGooglePayTokenListener(mGooglePayTokenListener); + + GooglePayTokenisationRequest gPay = new GooglePayTokenisationRequest(); + + gPay + .setSignature(googlePayToken.getString("signature")) + .setProtocolVersion(googlePayToken.getString("protocolVersion")) + .setSignedMessage(googlePayToken.getString("signedMessage")); + + // Using Gson to convert the custom request object into a JSON string for use in the HTTP call + Gson gson = new Gson(); + String jsonBody = gson.toJson(gPay); + + try { + http.generateGooglePayToken(key, mEnvironment.googlePay, jsonBody); + } catch (JSONException e) { + e.printStackTrace(); + } + + } + + /** + * This method used to set a callback for 3D Secure handling. + * + * @return CheckoutAPIClient to allow method chaining + */ + public CheckoutAPIClient setTokenListener(OnTokenGenerated listener) { + this.mTokenListener = listener; + return this; + } + + /** + * This method used to set a callback for Google Pay handling. + * + * @return CheckoutAPIClient to allow method chaining + */ + public CheckoutAPIClient setGooglePayListener(OnGooglePayTokenGenerated listener) { + this.mGooglePayTokenListener = listener; + return this; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/AddressOneInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/AddressOneInput.java new file mode 100755 index 000000000..2eac907e1 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/AddressOneInput.java @@ -0,0 +1,76 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +/** + * A custom EdiText with validation and handling of address input + */ +public class AddressOneInput extends android.support.v7.widget.AppCompatEditText { + + public interface AddressOneListener { + void onAddressOneInputFinish(String number); + + void clearAddressOneError(); + } + + private @Nullable + AddressOneInput.AddressOneListener mAddressOneListener; + private Context mContext; + + public AddressOneInput(Context context) { + this(context, null); + } + + public AddressOneInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + private void init() { + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + // Save state + if (mAddressOneListener != null) { + mAddressOneListener.onAddressOneInputFinish(s.toString()); + } + } + }); + + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + performClick(); + // Clear error if the user starts typing + if (mAddressOneListener != null) { + mAddressOneListener.clearAddressOneError(); + } + @Nullable InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT); + } + } + } + }); + } + + public void setAddressOneListener(AddressOneInput.AddressOneListener listener) { + this.mAddressOneListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/BillingInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/BillingInput.java new file mode 100755 index 000000000..744137b80 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/BillingInput.java @@ -0,0 +1,99 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; + +import com.checkout.android_sdk.R; +import com.checkout.android_sdk.Store.DataStore; + +import java.util.ArrayList; +import java.util.List; + +/** + * A custom Spinner with handling of billing input + */ +public class BillingInput extends android.support.v7.widget.AppCompatSpinner { + + public interface BillingListener { + void onGoToBilling(); + } + + private @Nullable + BillingInput.BillingListener mBillingListener; + private Context mContext; + + public BillingInput(Context context) { + this(context, null); + } + + public BillingInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element as well as setting up appropriate listeners + */ + private void init() { + + // Options needed for focus context switching + setFocusable(true); + setFocusableInTouchMode(true); + + // Populate the spinner values + populateSpinner(); + + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + performClick(); + @Nullable InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } + } + }); + } + + /** + * This method populates the spinner with some default values + */ + private void populateSpinner() { + List billingElement = new ArrayList<>(); + + billingElement.add(getResources().getString(R.string.select_billing_details)); + billingElement.add(getResources().getString(R.string.billing_details_add)); + + ArrayAdapter dataAdapter = new ArrayAdapter<>(mContext, + android.R.layout.simple_spinner_item, billingElement); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + setAdapter(dataAdapter); + } + + /** + * This method is used to redirect the user tot he billing page is they chose the ADD option + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mBillingListener != null && this.getSelectedItemPosition() == 1) { + mBillingListener.onGoToBilling(); + } + super.onLayout(changed, l, t, r, b); + } + + /** + * Used to set the callback listener for when the address input is completed + */ + public void setBillingListener(BillingInput.BillingListener listener) { + this.mBillingListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/CardInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/CardInput.java new file mode 100755 index 000000000..8c187b77d --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/CardInput.java @@ -0,0 +1,162 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; + +import com.checkout.android_sdk.Store.DataStore; +import com.checkout.android_sdk.Utils.CardUtils; + +/** + *

CardInput class

+ * The CardInput class has the purpose extending an AppCompatEditText and provide validation + * and formatting for the user's card details. + *

+ * This class will validate on the "afterTextChanged" event and display a card icon on the right + * side based on the users input. It will also span spaces following the {@link CardUtils} details. + */ +public class CardInput extends android.support.v7.widget.AppCompatEditText { + /** + * An interface needed to communicate with the parent once the field is successfully completed + */ + public interface Listener { + void onCardInputFinish(String number); + + void onCardError(); + + void onClearCardError(); + } + + private @Nullable + CardInput.Listener mCardInputListener; + Context mContext; + DataStore mDataStore = DataStore.getInstance(); + final CardUtils mCardUtils = new CardUtils(); + + public CardInput(Context context) { + this(context, null); + } + + public CardInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element as well as setting up appropriate listeners + */ + private void init() { + + // Add listener for text input + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + // Remove error if the user is typing + if (mCardInputListener != null) { + mCardInputListener.onClearCardError(); + } + // Remove Spaces + String initial = sanitizeEntry(s.toString()); + // Save State + mDataStore.setCardNumber(s.toString()); + // Format number + String formatted = mCardUtils.getFormattedCardNumber(initial); + // Get Card type + CardUtils.Cards cardType = mCardUtils.getType(initial); + // Set the CardInput maximum length based on the type of card + setFilters(new InputFilter[]{new InputFilter.LengthFilter(cardType.maxCardLength)}); + // Set the CardInput icon based on the type of card + setCardTypeIcon(cardType); + + // Update only is the formatted number is different from the initial input + if (!s.toString().equals(formatted)) { + s.replace(0, s.toString().length(), formatted); + } + checkIfCardIsValid(initial, cardType); + } + }); + + // Add listener for focus + + // When the CardInput loses focus check if the card number is not valid and trigger an error + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (!hasFocus) { + if (mCardInputListener != null && !mCardUtils.isValidCard(mDataStore.getCardNumber())) { + mCardInputListener.onCardError(); + } + } else { + // Clear the error message until the field loses focus + if (mCardInputListener != null) { + mCardInputListener.onClearCardError(); + } + } + } + }); + } + + /** + * This method is used to validate the card number + */ + public void checkIfCardIsValid(String number, CardUtils.Cards cardType) { + boolean hasDesiredLength = false; + for (int i : cardType.cardLength) { + if (i == number.length()) { + hasDesiredLength = true; + break; + } + } + if (mCardUtils.isValidCard(number) && hasDesiredLength) { + if (mCardInputListener != null) { + mCardInputListener.onCardInputFinish(sanitizeEntry(number)); + } + mDataStore.setCvvLength(cardType.maxCvvLength); + } + } + + /** + * This method will display a card icon associated to the specific card scheme + */ + public void setCardTypeIcon(CardUtils.Cards type) { + Drawable img; + if (type.resourceId != 0) { + img = getContext().getResources().getDrawable(type.resourceId); + img.setBounds(0, 0, 68, 68); + setCompoundDrawables(null, null, img, null); + setCompoundDrawablePadding(5); + } else { + setCompoundDrawables(null, null, null, null); + } + } + + /** + * This method will clear the whitespace in a number string + */ + public static String sanitizeEntry(String entry) { + return entry.replaceAll("\\D", ""); + } + + /** + * Used to set the callback listener for when the card input is completed + */ + public void setCardListener(Listener listener) { + this.mCardInputListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/CountryInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/CountryInput.java new file mode 100755 index 000000000..d4698ca61 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/CountryInput.java @@ -0,0 +1,123 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import com.checkout.android_sdk.R; +import com.checkout.android_sdk.Utils.PhoneUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Locale; + +/** + * A custom Spinner with handling of city input + */ +public class CountryInput extends android.support.v7.widget.AppCompatSpinner { + + public interface CountryListener { + void onCountryInputFinish(String country, String prefix); + } + + private @Nullable + CountryInput.CountryListener mCountryListener; + private Context mContext; + + public CountryInput(Context context) { + this(context, 0); + } + + public CountryInput(Context context, int mode) { + this(context, null); + } + + public CountryInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element as well as setting up appropriate listeners + */ + private void init() { + + setFocusable(true); + setFocusableInTouchMode(true); + + populateSpinner(); + + // Based on the country selected save teh ISO2 and set a prefix for teh phone number + setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (mCountryListener != null && getSelectedItemPosition() > 0) { + Locale[] locale = Locale.getAvailableLocales(); + String country; + for (Locale loc : locale) { + country = loc.getDisplayCountry(); + if (country.equals(getSelectedItem().toString())) { + mCountryListener.onCountryInputFinish(loc.getCountry(), PhoneUtils.getPrefix(loc.getCountry())); + } + } + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + performClick(); + @Nullable InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } + } + }); + + // Remove extra padding left + setPadding(0, this.getPaddingTop(), this.getPaddingRight(), this.getPaddingBottom()); + } + + /** + * Populate the Spinner with all country regions + */ + private void populateSpinner() { + Locale[] locale = Locale.getAvailableLocales(); + ArrayList countries = new ArrayList<>(); + String country; + countries.add(getResources().getString(R.string.placeholder_country)); + + for (Locale loc : locale) { + country = loc.getDisplayCountry(); + if (country.length() > 0 && !countries.contains(country)) { + countries.add(country); + } + } + Collections.sort(countries, String.CASE_INSENSITIVE_ORDER); + + ArrayAdapter adapter = new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_dropdown_item, countries); + setAdapter(adapter); + } + + /** + * Used to set the callback listener for when the country input is completed + */ + public void setCountryListener(CountryInput.CountryListener listener) { + this.mCountryListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/CvvInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/CvvInput.java new file mode 100755 index 000000000..c225abda4 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/CvvInput.java @@ -0,0 +1,28 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.util.AttributeSet; + +import com.checkout.android_sdk.Store.DataStore; + +/** + * A custom EdiText with validation and handling of cvv input + */ +public class CvvInput extends DefaultInput { + + private DataStore mDataStore = DataStore.getInstance(); + + public CvvInput(Context context) { + this(context, null); + } + + public CvvInput(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + + + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/DefaultInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/DefaultInput.java new file mode 100644 index 000000000..9934e72bd --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/DefaultInput.java @@ -0,0 +1,77 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; + +public class DefaultInput extends android.support.v7.widget.AppCompatEditText { + public interface Listener { + void onInputFinish(String value); + + void clearInputError(); + } + + private @Nullable + DefaultInput.Listener mListener; + Context mContext; + + public DefaultInput(Context context) { + this(context, 0); + } + + public DefaultInput(Context context, int mode) { + this(context, null); + } + + + public DefaultInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element as well as setting up appropriate listeners + */ + private void init() { + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + // Save state + if (mListener != null) { + mListener.onInputFinish(s.toString()); + mListener.clearInputError(); + } + } + }); + + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (mListener != null && hasFocus) { + mListener.clearInputError(); + } + } + }); + } + + /** + * Used to set the callback listener for when the zip input is completed + */ + public void setListener(DefaultInput.Listener listener) { + this.mListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/MonthInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/MonthInput.java new file mode 100755 index 000000000..3b2597a96 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/MonthInput.java @@ -0,0 +1,148 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import com.checkout.android_sdk.Store.DataStore; + +import java.util.ArrayList; +import java.util.List; + +/** + * A custom Spinner with handling of card expiration month input + */ +public class MonthInput extends android.support.v7.widget.AppCompatSpinner { + + public interface MonthListener { + void onMonthInputFinish(String month); + } + + // enum with month is different formats + public enum Months { + JANUARY("JAN", 1, "01"), + FEBRUARY("FEB", 2, "02"), + MARCH("MAR", 3, "03"), + APRIL("APR", 4, "04"), + MAY("MAY", 5, "05"), + JUNE("JUN", 6, "06"), + JULY("JUL", 7, "07"), + AUGUST("AUG", 8, "08"), + SEPTEMBER("SEP", 9, "09"), + OCTOBER("OCT", 10, "10"), + NOVEMBER("NOV", 11, "11"), + DECEMBER("DEC", 12, "12"); + + public final String name; + public final int number; + public final String numberString; + + + Months(String name, int number, String numberString) { + this.name = name; + this.number = number; + this.numberString = numberString; + } + + } + + private @Nullable + MonthInput.MonthListener mMonthInputListener; + private Context mContext; + private DataStore mDatastore = DataStore.getInstance(); + + public MonthInput(Context context) { + this(context, 0); + } + + public MonthInput(Context context, int mode) { + this(context, null); + } + + + public MonthInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element as well as setting up appropriate listeners + */ + private void init() { + // Options needed for focus context switching + setFocusable(true); + setFocusableInTouchMode(true); + + populateSpinner(); + + setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + MonthInput.Months[] months = MonthInput.Months.values(); + + mDatastore.setCardMonth(months[position].numberString); + + for (int i = 0; i < 12; i++) { + if (mMonthInputListener != null && months[i].number - 1 == position) { + mMonthInputListener.onMonthInputFinish(months[i].numberString); + break; + } + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + performClick(); + @Nullable InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } + } + }); + + // Remove extra padding left + setPadding(0, this.getPaddingTop(), this.getPaddingRight(), this.getPaddingBottom()); + + } + + /** + * Populate the spinner with all the month of the year + */ + public void populateSpinner() { + MonthInput.Months[] months = MonthInput.Months.values(); + + List monthElements = new ArrayList<>(); + + for (int i = 0; i < 12; i++) { + monthElements.add(months[i].name + " - " + months[i].numberString); + } + + ArrayAdapter dataAdapter = new ArrayAdapter<>(mContext, + android.R.layout.simple_spinner_item, monthElements); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + setAdapter(dataAdapter); + } + + /** + * Used to set the callback listener for when the month input is completed + */ + public void setMonthListener(MonthInput.MonthListener listener) { + this.mMonthInputListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/PhoneInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/PhoneInput.java new file mode 100755 index 000000000..e502d33ef --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/PhoneInput.java @@ -0,0 +1,74 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; + +/** + * A custom EdiText with validation and handling of phone number input + */ +public class PhoneInput extends android.support.v7.widget.AppCompatEditText { + + public interface PhoneListener { + void onPhoneInputFinish(String phone); + + void clearPhoneError(); + } + + private @Nullable + PhoneInput.PhoneListener mPhoneListener; + + public PhoneInput(Context context) { + this(context, null); + } + + public PhoneInput(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element as well as setting up appropriate listeners + */ + private void init() { + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + // Save state + if (mPhoneListener != null) { + mPhoneListener.onPhoneInputFinish(s.toString()); + } + } + }); + + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (mPhoneListener != null && hasFocus) { + setSelection(getText().toString().length()); + mPhoneListener.clearPhoneError(); + } + } + }); + } + + /** + * Used to set the callback listener for when the phone input is completed + */ + public void setPhoneListener(PhoneInput.PhoneListener listener) { + this.mPhoneListener = listener; + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Input/YearInput.java b/android-sdk/src/main/java/com/checkout/android_sdk/Input/YearInput.java new file mode 100755 index 000000000..2398ac576 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Input/YearInput.java @@ -0,0 +1,110 @@ +package com.checkout.android_sdk.Input; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +/** + * A custom Spinner with handling of card expiration year input + */ +public class YearInput extends android.support.v7.widget.AppCompatSpinner { + + public interface YearListener { + void onYearInputFinish(String month); + } + + private @Nullable + YearInput.YearListener mYearInputListener; + Context mContext; + + public YearInput(Context context) { + this(context, 0); + } + + public YearInput(Context context, int mode) { + this(context, null); + } + + + public YearInput(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element as well as setting up appropriate listeners + */ + private void init() { + // Options needed for focus context switching + setFocusable(true); + setFocusableInTouchMode(true); + + populateYears(); + + setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + performClick(); + @Nullable InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } + } + }); + + setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (mYearInputListener != null) { + mYearInputListener.onYearInputFinish(getSelectedItem().toString()); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + // Remove extra padding left + setPadding(0, this.getPaddingTop(), this.getPaddingRight(), this.getPaddingBottom()); + + } + + /** + * Populate the spinner with the next 15 year + */ + private void populateYears() { + + List yearElements = new ArrayList<>(); + + for (int i = Calendar.getInstance().get(Calendar.YEAR); i < Calendar.getInstance().get(Calendar.YEAR) + 15; i++) { + yearElements.add(String.valueOf(i)); + } + + ArrayAdapter dataAdapter = new ArrayAdapter<>(mContext, + android.R.layout.simple_spinner_item, yearElements); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + setAdapter(dataAdapter); + } + + /** + * Used to set the callback listener for when the year input is completed + */ + public void setYearListener(YearInput.YearListener listener) { + this.mYearInputListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Models/BillingModel.java b/android-sdk/src/main/java/com/checkout/android_sdk/Models/BillingModel.java new file mode 100755 index 000000000..33b40a920 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Models/BillingModel.java @@ -0,0 +1,54 @@ +package com.checkout.android_sdk.Models; + +/** + * Http request billing details object model + */ +public class BillingModel { + + private String addressLine1; + private String addressLine2; + private String postcode; + private String country; + private String city; + private String state; + private PhoneModel phone; + + public BillingModel(String addressLine1, String addressLine2, String postcode, String country, + String city, String state, PhoneModel phone) { + this.addressLine1 = addressLine1; + this.addressLine2 = addressLine2; + this.postcode = postcode; + this.country = country; + this.city = city; + this.state = state; + this.phone = phone; + } + + public String getAddressLine1() { + return addressLine1; + } + + public String getAddressLine2() { + return addressLine2; + } + + public String getPostcode() { + return postcode; + } + + public String getCountry() { + return country; + } + + public String getCity() { + return city; + } + + public String getState() { + return state; + } + + public PhoneModel getPhone() { + return phone; + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Models/CardModel.java b/android-sdk/src/main/java/com/checkout/android_sdk/Models/CardModel.java new file mode 100755 index 000000000..959e23cd0 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Models/CardModel.java @@ -0,0 +1,48 @@ +package com.checkout.android_sdk.Models; + +/** + * Http request card details object model + */ +public class CardModel { + + private String expiryMonth; + private String expiryYear; + private BillingModel billingDetails; + private String id; + private String last4; + private String bin; + private String paymentMethod; + private String name; + + public String getExpiryMonth() { + return expiryMonth; + } + + public String getExpiryYear() { + return expiryYear; + } + + public BillingModel getBillingDetails() { + return billingDetails; + } + + public String getId() { + return id; + } + + public String getLast4() { + return last4; + } + + public String getBin() { + return bin; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public String getName() { + return name; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Models/GooglePayModel.java b/android-sdk/src/main/java/com/checkout/android_sdk/Models/GooglePayModel.java new file mode 100755 index 000000000..550dadc04 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Models/GooglePayModel.java @@ -0,0 +1,35 @@ +package com.checkout.android_sdk.Models; + +/** + * Http request Google Pay object model + */ +public class GooglePayModel { + + private String signature; + private String protocolVersion; + private String signedMessage; + + public String getSignature() { + return signature; + } + + public String getProtocolVersion() { + return protocolVersion; + } + + public String getSignedMessage() { + return signedMessage; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public void setProtocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public void setSignedMessage(String signedMessage) { + this.signedMessage = signedMessage; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Models/PhoneModel.java b/android-sdk/src/main/java/com/checkout/android_sdk/Models/PhoneModel.java new file mode 100755 index 000000000..8cda2bab0 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Models/PhoneModel.java @@ -0,0 +1,23 @@ +package com.checkout.android_sdk.Models; + +/** + * Http request Phone object model + */ +public class PhoneModel { + + private String countryCode; + private String number; + + public PhoneModel(String countryCode, String number) { + this.countryCode = countryCode; + this.number = number; + } + + public String getCountryCode() { + return countryCode; + } + + public String getNumber() { + return number; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/PaymentForm.java b/android-sdk/src/main/java/com/checkout/android_sdk/PaymentForm.java new file mode 100755 index 000000000..2f11dc065 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/PaymentForm.java @@ -0,0 +1,269 @@ +package com.checkout.android_sdk; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; + +import com.checkout.android_sdk.Models.BillingModel; +import com.checkout.android_sdk.Models.PhoneModel; +import com.checkout.android_sdk.Request.CardTokenisationRequest; +import com.checkout.android_sdk.Store.DataStore; +import com.checkout.android_sdk.Utils.CardUtils; +import com.checkout.android_sdk.Utils.CustomAdapter; +import com.checkout.android_sdk.Utils.Environment; +import com.checkout.android_sdk.View.BillingDetailsView; +import com.checkout.android_sdk.View.CardDetailsView; + +/** + * Contains helper methods dealing with the tokenisation or payment from customisation + *

+ * Most of the methods that include interaction with the Checkout.com API rely on + * callbacks to communicate outcomes. Please make sure you set the key/environment + * and appropriate callbacks to a ensure successful interaction + */ +public class PaymentForm extends FrameLayout { + + /** + * This is interface used as a callback for when the 3D secure functionality is used + */ + public interface On3DSFinished { + void onSuccess(String token); + + void onError(String errorMessage); + } + + /** + * This is interface used as a callback for when the form is completed and the user pressed the + * pay button. You can use this to potentially display a loader. + */ + public interface OnSubmitForm { + void onSubmit(CardTokenisationRequest request); + } + + // Indexes for the pages + private static int CARD_DETAILS_PAGE_INDEX = 0; + private static int BILLING_DETAILS_PAGE_INDEX = 1; + + /** + * This is a callback used to generate a payload with the user details and pass them to the + * mSubmitFormListener so the user can act upon them. The next step will most likely include using + * this payload to generate a token in the CheckoutAPIClient + */ + private final CardDetailsView.DetailsCompleted mDetailsCompletedListener = new CardDetailsView.DetailsCompleted() { + @Override + public void onDetailsCompleted() { + mSubmitFormListener.onSubmit(generateRequest()); + } + }; + + /** + * This is a callback used to go back to the card details view from the billing page + * and based on the action used decide is the billing spinner will be updated + */ + private BillingDetailsView.Listener mBillingListener = new BillingDetailsView.Listener() { + @Override + public void onBillingCompleted() { + customAdapter.updateBillingSpinner(); + mPager.setCurrentItem(CARD_DETAILS_PAGE_INDEX); + } + + @Override + public void onBillingCanceled() { + customAdapter.clearBillingSpinner(); + mPager.setCurrentItem(CARD_DETAILS_PAGE_INDEX); + } + }; + + /** + * This is a callback used to navigate to the billing details page + */ + private CardDetailsView.GoToBillingListener mCardListener = new CardDetailsView.GoToBillingListener() { + @Override + public void onGoToBillingPressed() { + mPager.setCurrentItem(BILLING_DETAILS_PAGE_INDEX); + } + }; + + + private Context mContext; + public On3DSFinished m3DSecureListener; + public OnSubmitForm mSubmitFormListener; + public CheckoutAPIClient.OnTokenGenerated mTokenListener; + + private CustomAdapter customAdapter; + private ViewPager mPager; + private AttributeSet attrs; + private DataStore mDataStore = DataStore.getInstance(); + + /** + * This is the constructor used when the module is used without the UI. + */ + public PaymentForm(@NonNull Context context) { + this(context, null); + } + + public PaymentForm(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + this.attrs = attrs; + initView(); + } + + /** + * This method is used to initialise the UI of the module + */ + private void initView() { + // Set up the layout + inflate(mContext, R.layout.payment_form, this); + + mPager = findViewById(R.id.view_pager); + // Use a custom adapter for the viewpager + customAdapter = new CustomAdapter(mContext); + // Set up the callbacks + customAdapter.setCardDetailsListener(mCardListener); + customAdapter.setBillingListener(mBillingListener); + customAdapter.setTokenDetailsCompletedListener(mDetailsCompletedListener); + mPager.setAdapter(customAdapter); + mPager.setEnabled(false); + } + + /** + * This method is used set the accepted card schemes + * + * @param cards array of accepted cards + */ + public PaymentForm setAcceptedCard(CardUtils.Cards[] cards) { + mDataStore.setAcceptedCards(cards); + return this; + } + + /** + * This method is used to handle 3D Secure URLs. + *

+ * It wil programmatically generate a WebView and listen for when the url changes + * in either the success url or the fail url. + * + * @param url the 3D Secure url + * @param successUrl the Redirection url set up in the Checkout.com HUB + * @param failsUrl the Redirection Fail url set up in the Checkout.com HUB + */ + public void handle3DS(String url, final String successUrl, final String failsUrl) { + if (mPager != null) { + mPager.setVisibility(GONE); // dismiss the card form UI + } + WebView web = new WebView(mContext); + web.loadUrl(url); + web.getSettings().setJavaScriptEnabled(true); + web.setWebViewClient(new WebViewClient() { + // Listen for when teh URL changes and match t with either the success of fail url + @Override + public void onPageFinished(WebView view, String url) { + if (url.contains(successUrl)) { + Uri uri = Uri.parse(url); + String paymentToken = uri.getQueryParameter("cko-payment-token"); + m3DSecureListener.onSuccess(paymentToken); + } else if (url.contains(failsUrl)) { + Uri uri = Uri.parse(url); + String paymentToken = uri.getQueryParameter("cko-payment-token"); + m3DSecureListener.onError(paymentToken); + } + } + }); + // Make WebView fill the layout + web.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + addView(web); + } + + /** + * This method used to generate a {@link CardTokenisationRequest} with the details + * completed by the user in the payment from + * displayed in the payment form. + * + * @return CardTokenisationRequest + */ + private CardTokenisationRequest generateRequest() { + CardTokenisationRequest request; + if (mDataStore.isBillingCompleted()) { + request = new CardTokenisationRequest( + sanitizeEntry(mDataStore.getCardNumber()), + mDataStore.getCustomerName(), + mDataStore.getCardMonth(), + mDataStore.getCardYear(), + mDataStore.getCardCvv(), + new BillingModel( + mDataStore.getCustomerAddress1(), + mDataStore.getCustomerAddress2(), + mDataStore.getCustomerZipcode(), + mDataStore.getCustomerCountry(), + mDataStore.getCustomerCity(), + mDataStore.getCustomerState(), + new PhoneModel( + mDataStore.getCustomerPhonePrefix(), + mDataStore.getCustomerPhone() + ) + ) + ); + } else { + request = new CardTokenisationRequest( + sanitizeEntry(mDataStore.getCardNumber()), + mDataStore.getCustomerName(), + mDataStore.getCardMonth(), + mDataStore.getCardYear(), + mDataStore.getCardCvv(), + null + ); + } + + return request; + } + + /** + * This method used to decide if the billing details option will be + * displayed in the payment form. + * + * @param include boolean showing if the billing should be used + */ + public void includeBilling(Boolean include) { + if (!include) { + mDataStore.setShowBilling(false); + } else { + mDataStore.setShowBilling(true); + } + } + + /** + * Returns a String without any spaces + *

+ * This method used to take a card number input String and return a + * String that simply removed all whitespace, keeping only digits. + * + * @param entry the String value of a card number + */ + private String sanitizeEntry(String entry) { + return entry.replaceAll("\\D", ""); + } + + /** + * This method used to set a callback for when the 3D Secure handling. + */ + public PaymentForm set3DSListener(On3DSFinished listener) { + this.m3DSecureListener = listener; + return this; + } + + /** + * This method used to set a callback for when the form is submitted + */ + public PaymentForm setSubmitListener(OnSubmitForm listener) { + this.mSubmitFormListener = listener; + return this; + } + +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Request/CardTokenisationRequest.java b/android-sdk/src/main/java/com/checkout/android_sdk/Request/CardTokenisationRequest.java new file mode 100755 index 000000000..33068de13 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Request/CardTokenisationRequest.java @@ -0,0 +1,34 @@ +package com.checkout.android_sdk.Request; + +import com.checkout.android_sdk.Models.BillingModel; + +/** + * The request model object for the card tokenisation request + */ +public class CardTokenisationRequest { + + private String number; + private String name; + private String expiryMonth; + private String expiryYear; + private String cvv; + + private BillingModel billingDetails; + + public CardTokenisationRequest(String number, String name, String expiryMonth, String expiryYear, String cvv, BillingModel billingDetails) { + this.number = number; + this.name = name; + this.expiryMonth = expiryMonth; + this.expiryYear = expiryYear; + this.cvv = cvv; + this.billingDetails = billingDetails; + } + + public CardTokenisationRequest(String number, String name, String expiryMonth, String expiryYear, String cvv) { + this.number = number; + this.name = name; + this.expiryMonth = expiryMonth; + this.expiryYear = expiryYear; + this.cvv = cvv; + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Request/GooglePayTokenisationRequest.java b/android-sdk/src/main/java/com/checkout/android_sdk/Request/GooglePayTokenisationRequest.java new file mode 100755 index 000000000..48995fab2 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Request/GooglePayTokenisationRequest.java @@ -0,0 +1,28 @@ +package com.checkout.android_sdk.Request; + +/** + * The request model object for the Google Pay tokenisation request + */ + +import com.checkout.android_sdk.Models.GooglePayModel; + +public class GooglePayTokenisationRequest { + + private String type = "googlepay"; + private GooglePayModel token_data = new GooglePayModel(); + + public GooglePayTokenisationRequest setSignature(String signature) { + this.token_data.setSignature(signature); + return this; + } + + public GooglePayTokenisationRequest setProtocolVersion(String protocolVersion) { + this.token_data.setProtocolVersion(protocolVersion); + return this; + } + + public GooglePayTokenisationRequest setSignedMessage(String signedMessage) { + this.token_data.setSignedMessage(signedMessage); + return this; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Response/CardTokenisationFail.java b/android-sdk/src/main/java/com/checkout/android_sdk/Response/CardTokenisationFail.java new file mode 100755 index 000000000..41cfe0c45 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Response/CardTokenisationFail.java @@ -0,0 +1,33 @@ +package com.checkout.android_sdk.Response; + +/** + * The response model object for the card tokenisation error + */ +public class CardTokenisationFail { + + private String eventId; + private String errorCode; + private String message; + private String[] errorMessageCodes; + private String[] errors; + + public String getEventId() { + return eventId; + } + + public String getErrorCode() { + return errorCode; + } + + public String getMessage() { + return message; + } + + public String[] getErrorMessageCodes() { + return errorMessageCodes; + } + + public String[] getErrors() { + return errors; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Response/CardTokenisationResponse.java b/android-sdk/src/main/java/com/checkout/android_sdk/Response/CardTokenisationResponse.java new file mode 100755 index 000000000..2348d3c3b --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Response/CardTokenisationResponse.java @@ -0,0 +1,35 @@ +package com.checkout.android_sdk.Response; + +import com.checkout.android_sdk.Models.CardModel; + +/** + * The response model object for the card tokenisation response + */ +public class CardTokenisationResponse { + + private String id; + private String liveMode; + private String created; + private String used; + private CardModel card; + + public String getId() { + return id; + } + + public String getLiveMode() { + return liveMode; + } + + public String getCreated() { + return created; + } + + public String getUsed() { + return used; + } + + public CardModel getCard() { + return card; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Response/GooglePayTokenisationFail.java b/android-sdk/src/main/java/com/checkout/android_sdk/Response/GooglePayTokenisationFail.java new file mode 100755 index 000000000..a89781c33 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Response/GooglePayTokenisationFail.java @@ -0,0 +1,38 @@ +package com.checkout.android_sdk.Response; + +/** + * The response model object for the Google Pay tokenisation error + */ +public class GooglePayTokenisationFail { + + private String request_id; + private String error_type; + private String[] error_codes; + + public String getRequestId() { + return request_id; + } + + public GooglePayTokenisationFail setRequestId(String request_id) { + this.request_id = request_id; + return this; + } + + public String getErrorType() { + return error_type; + } + + public GooglePayTokenisationFail setErrorType(String error_type) { + this.error_type = error_type; + return this; + } + + public String[] getErrorCodes() { + return error_codes; + } + + public GooglePayTokenisationFail setErrorCodes(String[] error_codes) { + this.error_codes = error_codes; + return this; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Response/GooglePayTokenisationResponse.java b/android-sdk/src/main/java/com/checkout/android_sdk/Response/GooglePayTokenisationResponse.java new file mode 100755 index 000000000..8f34f6027 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Response/GooglePayTokenisationResponse.java @@ -0,0 +1,23 @@ +package com.checkout.android_sdk.Response; + +/** + * The response model object for the Google Pay tokenisation response + */ +public class GooglePayTokenisationResponse { + + private String type; + private String token; + private String expires_on; + + public String getType() { + return type; + } + + public String getToken() { + return token; + } + + public String getExpiration() { + return expires_on; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Store/DataStore.java b/android-sdk/src/main/java/com/checkout/android_sdk/Store/DataStore.java new file mode 100755 index 000000000..f7d68812c --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Store/DataStore.java @@ -0,0 +1,246 @@ +package com.checkout.android_sdk.Store; + +import com.checkout.android_sdk.Utils.CardUtils; + +/** + * The DataStore + *

+ * Used to contain state within the SDK for easy communication between custom components. + * It is also used preserve and restore state when in case the device orientation changes. + */ +public class DataStore { + + private static DataStore INSTANCE = null; + private String mCardNumber; + private String mCardMonth; + private String mCardYear; + private String mCardCvv; + private int mCvvLength = 4; + + private CardUtils.Cards[] acceptedCards; + + private String mSuccessUrl; + private String mFailUrl; + + private boolean IsValidCardNumber = false; + private boolean IsValidCardMonth = false; + private boolean IsValidCardYear = false; + private boolean IsValidCardCvv = false; + + private String mCustomerName = ""; + private String mCustomerCountry = ""; + private String mCustomerAddress1 = ""; + private String mCustomerAddress2 = ""; + private String mCustomerCity = ""; + private String mCustomerState = ""; + private String mCustomerZipcode = ""; + private String mCustomerPhonePrefix = ""; + private String mCustomerPhone = ""; + + private boolean showBilling = true; + private boolean billingCompleted = false; + + protected DataStore() { + } + + public static DataStore getInstance() { + if (INSTANCE == null) { + INSTANCE = new DataStore(); + } + return (INSTANCE); + } + + public String getSuccessUrl() { + return mSuccessUrl; + } + + public void setSuccessUrl(String successUrl) { + mSuccessUrl = successUrl; + } + + public String getFailUrl() { + return mFailUrl; + } + + public void setFailUrl(String failUrl) { + mFailUrl = failUrl; + } + + public String getCardNumber() { + return mCardNumber; + } + + public void setCardNumber(String cardNumber) { + mCardNumber = cardNumber; + } + + public String getCardMonth() { + return mCardMonth; + } + + public void setCardMonth(String mCardMonth) { + this.mCardMonth = mCardMonth; + } + + public String getCardYear() { + return mCardYear; + } + + public void setCardYear(String cardYear) { + mCardYear = cardYear; + } + + public String getCardCvv() { + return mCardCvv; + } + + public void setCardCvv(String cardCvv) { + mCardCvv = cardCvv; + } + + public int getCvvLength() { + return mCvvLength; + } + + public void setCvvLength(int cvvLength) { + mCvvLength = cvvLength; + } + + public boolean isValidCardNumber() { + return IsValidCardNumber; + } + + public void setValidCardNumber(boolean validCardNumber) { + IsValidCardNumber = validCardNumber; + } + + public boolean isValidCardMonth() { + return IsValidCardMonth; + } + + public void setValidCardMonth(boolean validCardMonth) { + IsValidCardMonth = validCardMonth; + } + + public boolean isValidCardYear() { + return IsValidCardYear; + } + + public void setValidCardYear(boolean validCardYear) { + IsValidCardYear = validCardYear; + } + + public boolean isValidCardCvv() { + return IsValidCardCvv; + } + + public void setValidCardCvv(boolean validCardCvv) { + IsValidCardCvv = validCardCvv; + } + + public String getCustomerCountry() { + return mCustomerCountry; + } + + public void setCustomerCountry(String customerCountry) { + mCustomerCountry = customerCountry; + } + + public String getCustomerPhonePrefix() { + return mCustomerPhonePrefix; + } + + public void setCustomerPhonePrefix(String customerPhonePrefix) { + mCustomerPhonePrefix = customerPhonePrefix; + } + + public String getCustomerAddress1() { + return mCustomerAddress1; + } + + public void setCustomerAddress1(String customerAddress1) { + mCustomerAddress1 = customerAddress1; + } + + public String getCustomerAddress2() { + return mCustomerAddress2; + } + + public void setCustomerAddress2(String customerAddress2) { + mCustomerAddress2 = customerAddress2; + } + + public String getCustomerCity() { + return mCustomerCity; + } + + public void setCustomerCity(String customerCity) { + mCustomerCity = customerCity; + } + + public String getCustomerState() { + return mCustomerState; + } + + public void setCustomerState(String customerState) { + mCustomerState = customerState; + } + + public String getCustomerZipcode() { + return mCustomerZipcode; + } + + public void setCustomerZipcode(String customerZipcode) { + mCustomerZipcode = customerZipcode; + } + + public String getCustomerPhone() { + return mCustomerPhone; + } + + public void setCustomerPhone(String customerPhone) { + mCustomerPhone = customerPhone; + } + + public boolean getBillingVisibility() { + return showBilling; + } + + public void setShowBilling(boolean showBilling) { + this.showBilling = showBilling; + } + + public String getCustomerName() { + return mCustomerName; + } + + public void setCustomerName(String customerName) { + mCustomerName = customerName; + } + + public boolean isBillingCompleted() { + return billingCompleted; + } + + public void setBillingCompleted(boolean billingCompleted) { + this.billingCompleted = billingCompleted; + } + + public void cleanBillingData() { + DataStore.getInstance().setCustomerCountry(""); + DataStore.getInstance().setCustomerAddress1(""); + DataStore.getInstance().setCustomerAddress2(""); + DataStore.getInstance().setCustomerCity(""); + DataStore.getInstance().setCustomerState(""); + DataStore.getInstance().setCustomerZipcode(""); + DataStore.getInstance().setCustomerPhone(""); + } + + public CardUtils.Cards[] getAcceptedCards() { + return acceptedCards; + } + + public void setAcceptedCards(CardUtils.Cards[] acceptedCards) { + this.acceptedCards = acceptedCards; + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Utils/CardUtils.java b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/CardUtils.java new file mode 100755 index 000000000..f1ee2a116 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/CardUtils.java @@ -0,0 +1,256 @@ +package com.checkout.android_sdk.Utils; + +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.checkout.android_sdk.R; + +import java.util.Calendar; + +/** + * Provide information about different card types. + */ +public class CardUtils { + + /** + * An enum that hold information about the different card types. + * The sported card types are: VISA, AMEX, DISCOVER, UNIONPAY, JCB, + * LASER, DINERSCLUB, MASTERCARD, MAESTRO and a DEFAULT abstract card. + */ + public enum Cards { + VISA("visa", R.drawable.visa, "^4\\d*$", "^4[0-9]{12}(?:[0-9]{3})?$", new int[]{13, 16}, 19, 3, new int[]{4, 9, 14}, true), + AMEX("amex", R.drawable.amex, "^3[47]\\d*$", "/(\\d{1,4})(\\d{1,6})?(\\d{1,5})?/", new int[]{15}, 18, 4, new int[]{4, 6}, true), + DISCOVER("discover", R.drawable.discover, "^(6011|65|64[4-9])\\d*$", "^6(?:011|5[0-9]{2})[0-9]{12}$", new int[]{16}, 23, 3, new int[]{4, 9, 14}, true), + UNIONPAY("unionpay", R.drawable.unionpay, "^(((620|(621(?!83|88|98|99))|622(?!06|018)|62[3-6]|627[02,06,07]|628(?!0|1)|629[1,2]))\\d*|622018\\d{12})$", "^6(?:011|5[0-9]{2})[0-9]{12}$", new int[]{16, 17, 18, 19}, 23, 3, new int[]{4, 6, 14}, false), + JCB("jcb", R.drawable.jcb, "^(2131|1800|35)\\d*$", "^(?:2131|1800|35[0-9]{3})[0-9]{11}$", new int[]{16}, 23, 3, new int[]{4, 9, 14}, true), + DINERSCLUB("dinersclub", R.drawable.dinersclub, "^3(0[0-5]|[689])\\d*$", "^3(?:0[0-5]|[68][0-9])?[0-9]{11}$", new int[]{14}, 23, 3, new int[]{4, 6}, true), + MASTERCARD("mastercard", R.drawable.mastercard, "^(5[1-5]|222[1-9]|22[3-9]|2[3-6]|27[0-1]|2720)\\d*$", "^5[1-5][0-9]{14}$", new int[]{16, 17}, 19, 3, new int[]{4, 9, 14}, true), + MAESTRO("maestro", R.drawable.maestro, "^(?:5[06789]\\d\\d|(?!6011[0234])(?!60117[4789])(?!60118[6789])(?!60119)(?!64[456789])(?!65)6\\d{3})\\d{8,15}$", "^(5[06-9]|6[37])[0-9]{10,17}$", new int[]{12, 13, 14, 15, 16, 17, 18, 19}, 23, 3, new int[]{4, 9, 14}, true), + DEFAULT("default", 0, "", "", new int[]{16}, 19, 3, new int[]{4, 9, 14}, false); + + public final String name; + public final int resourceId; + private final String pattern; + public final String regex; + public final int[] cardLength; + public final int maxCardLength; + public final int maxCvvLength; + public final int[] gaps; + private final boolean luhn; + + /** + * The {@link Cards} constructor + *

+ * + * @param name card name + * @param pattern pattern used to determine card type early + * @param regex full regex of a full card + * @param maxCardLength the max length a card of a type can have + * @param maxCvvLength the max CVV a card of a type can have + * @param gaps the positions of the spaces spans ina formatted card + * @see Cards + */ + Cards(String name, int resourceId, String pattern, String regex, int[] cardLength, int maxCardLength, int maxCvvLength, int[] gaps, boolean luhn) { + this.name = name; + this.resourceId = resourceId; + this.pattern = pattern; + this.regex = regex; + this.cardLength = cardLength; + this.maxCardLength = maxCardLength; + this.maxCvvLength = maxCvvLength; + this.gaps = gaps; + this.luhn = luhn; + } + } + + /** + * Returns a Cards object can be used to identify the card type and + * information about: regex, card/cvv maximum length, space separation + * The number argument must specify as a String. + *

+ * This method iterates a Cards enum, and determines if the the function + * argument matches any pattern. Based on the verification, a Cards object + * will be returned. + * + * @param number the String value of a card number + * @return Cards object for the given type found + * @see Cards + */ + public static Cards getType(String number) { + + // Remove spaces from The number String + number = sanitizeEntry(number); + CardUtils.Cards[] cards = CardUtils.Cards.values(); + + // Iterate over the card card types and check what pattern matches + if (!number.equals("")) { + for (Cards card : cards) { + if (number.matches(card.pattern)) { + return card; + } + } + } + + // Return a default card if no card type is matched + return Cards.DEFAULT; + } + + /** + * Returns a boolean showing is the card String is a valid card number. + *

+ * This method is using the regex in {@link Cards} as well as the Luhn algorithm to + * the terms the validity of a card number + * + * @param number the String value of a card number + * @return If the card number is valid or not + */ + public static boolean isValidCard(@Nullable String number) { + if (number == null || number.equals("")) { + return false; + } + + number = sanitizeEntry(number); + Cards type = getType(number); + // If the card is not on the available card list return false + if (type == Cards.DEFAULT) { + return false; + } + + // Check if the length of the card matches the valid card lengths for the specific type + boolean isValidLength = false; + for (int length : type.cardLength) { + if (number.length() == length) { + isValidLength = true; + } + } + + // If the card length is valid and luhn is available check luhn, otherwise consider card valid + if (isValidLength && type.luhn) { + return checkLuhn(number); + } else if (isValidLength && !type.luhn) { + return true; + } + + return false; + } + + /** + * Returns a boolean showing is the card String is a valid card number. + *

+ * This is using Luhn validation to determine the card validity + * + * @param number the String value of a card number + * @return If the card number passes Luhn validation + */ + private static boolean checkLuhn(String number) { + final String rev = new StringBuffer(number).reverse().toString(); + final int len = rev.length(); + int oddSum = 0; + int evenSum = 0; + for (int i = 0; i < len; i++) { + final char c = rev.charAt(i); + final int digit = Character.digit(c, 10); + if (i % 2 == 0) { + oddSum += digit; + } else { + evenSum += digit / 5 + (2 * digit) % 10; + } + } + return (oddSum + evenSum) % 10 == 0; + } + + /** + * f + * Returns a String without any spaces + *

+ * This method used to take a card number input String and return a + * String that simply removed all whitespace, keeping only digits. + * + * @param entry the String value of a card number + * @return Cards object for the given type found + */ + private static String sanitizeEntry(String entry) { + return entry.replaceAll("\\D", ""); + } + + /** + * The card formatting method + *

+ * Used to take a card number String and provide formatting (span space characters) + * + * @param number card number in string format + * @return processedCard + */ + public static String getFormattedCardNumber(String number) { + + // Remove spaces form the card String + String processedCard = sanitizeEntry(number); + + CardUtils.Cards cardType = getType(number); + + // If the card is an AMEX or DINERSCLUB we iterate and span spaces at specific positions + if (cardType.name.equals("amex") || cardType.name.equals("dinersclub") || cardType.name.equals("unionpay")) { + for (int i = 0; i < cardType.gaps.length; i++) { + processedCard = processedCard.replaceFirst("(\\d{" + cardType.gaps[i] + "})(?=\\d)", "$1 "); + } + // If the card is on any other kind we span a space after every group of 4 digits + } else { + processedCard = processedCard.replaceAll("(\\d{4})(?=\\d)", "$1 "); + } + + return processedCard; + } + + /** + * Returns a boolean showing is the date is valid + *

+ * Used to take a card number String and provide formatting (span space characters) + * + * @param month the card expiration month as a string + * @param year the card expiration year as a string + * @return boolean representing validity + */ + public static boolean isValidDate(String month, String year) { + if (month.equals("") || year.equals("")) { + return false; + } + if (TextUtils.isDigitsOnly(sanitizeEntry(month)) && + TextUtils.isDigitsOnly(sanitizeEntry(year))) { + + if (Integer.valueOf(month) < 1 || Integer.valueOf(month) > 12) + return false; + + // Get current year and month + Calendar calendar = Calendar.getInstance(); + int calendarYear = calendar.get(Calendar.YEAR); + int calendarMonth = calendar.get(Calendar.MONTH); + + if (Integer.valueOf(year) < calendarYear) + return false; + if (Integer.valueOf(year) == calendarYear && + Integer.valueOf(month) < calendarMonth) + return false; + + return true; + } + return false; + } + + /** + * Returns a boolean showing is the cvv is valid in relation to the card type + *

+ * Used to take a card number String and provide formatting (span space characters) + * + * @param cvv the card cvv + * @param card the card object + * @return boolean representing validity + */ + public static boolean isValidCvv(String cvv, Cards card) { + if (TextUtils.isDigitsOnly(sanitizeEntry(cvv)) && + card != null) { + if (card.maxCvvLength == cvv.length()) + return true; + } + return false; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Utils/CustomAdapter.java b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/CustomAdapter.java new file mode 100755 index 000000000..f81f7c331 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/CustomAdapter.java @@ -0,0 +1,124 @@ +package com.checkout.android_sdk.Utils; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.PagerAdapter; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.checkout.android_sdk.View.BillingDetailsView; +import com.checkout.android_sdk.View.CardDetailsView; + +import java.util.ArrayList; +import java.util.List; + +/** + * The adapter of the viewpager used to have the 2 pages {@link CardDetailsView} + * and {@link BillingDetailsView} + *

+ * This class handles interaction initialisation and interaction between the to pages + */ +public class CustomAdapter extends PagerAdapter { + + private Context mContext; + private CardDetailsView cardDetailsView; + private List mViews = new ArrayList<>(); + private CardDetailsView.GoToBillingListener mCardDetailsListener; + private BillingDetailsView billingDetailsView; + private BillingDetailsView.Listener mBillingListener; + private CardDetailsView.DetailsCompleted mDetailsCompletedListener; + + public CustomAdapter(Context context) { + mContext = context; + } + + /** + * Pass the callback to go to the billing page + */ + public void setCardDetailsListener(CardDetailsView.GoToBillingListener listener) { + mCardDetailsListener = listener; + } + + /** + * Pass the callback to go to the card details page + */ + public void setBillingListener(BillingDetailsView.Listener listener) { + mBillingListener = listener; + } + + /** + * Pass the callback for when the card toke is generated + */ + public void setTokenDetailsCompletedListener(CardDetailsView.DetailsCompleted listener) { + mDetailsCompletedListener = listener; + } + + /** + * Indicate the {@link CardDetailsView} need to update the billing spinner + */ + public void updateBillingSpinner() { + cardDetailsView.updateBillingSpinner(); + } + + /** + * Indicate the {@link CardDetailsView} need to clear the billing spinner + */ + public void clearBillingSpinner() { + cardDetailsView.clearBillingSpinner(); + } + + /** + * Instantiation function + */ + @NonNull + @Override + public LinearLayout instantiateItem(@NonNull ViewGroup container, int position) { + maybeInstantiateViews(container); + return mViews.get(position); + } + + /** + * Indicate there viewpager position + */ + @Nullable + @Override + public CharSequence getPageTitle(int position) { + return "page " + position; + } + + /** + * Indicate there is only a 2 level depth in the viewpager + */ + @Override + public int getCount() { + return 2; + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return view == object; + } + + /** + * Instantiates teh viewpager and adds the 2 pages: {@link CardDetailsView} + * and {@link BillingDetailsView} + */ + private void maybeInstantiateViews(ViewGroup container) { + if (mViews.isEmpty()) { + cardDetailsView = new CardDetailsView(mContext); + cardDetailsView.setGoToBillingListener(mCardDetailsListener); + cardDetailsView.setDetailsCompletedListener(mDetailsCompletedListener); + + billingDetailsView = new BillingDetailsView(mContext); + billingDetailsView.setGoToCardDetailsListener(mBillingListener); + + mViews.add(cardDetailsView); + mViews.add(billingDetailsView); + + container.addView(cardDetailsView); + container.addView(billingDetailsView); + } + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Utils/Environment.java b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/Environment.java new file mode 100644 index 000000000..1ab1a1876 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/Environment.java @@ -0,0 +1,14 @@ +package com.checkout.android_sdk.Utils; + +public enum Environment { + SANDBOX("https://sandbox.checkout.com/api2/v2/tokens/card/", "https://sandbox.checkout.com/api2/tokens"), + LIVE("https://api2.checkout.com/v2/tokens/card/", "https://api2.checkout.com/tokens"); + + public final String token; + public final String googlePay; + + Environment(String token, String googlePay) { + this.token = token; + this.googlePay = googlePay; + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Utils/HttpUtils.java b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/HttpUtils.java new file mode 100755 index 000000000..ecb4b620e --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/HttpUtils.java @@ -0,0 +1,171 @@ +package com.checkout.android_sdk.Utils; + +import android.content.Context; +import android.support.annotation.Nullable; + +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import com.checkout.android_sdk.CheckoutAPIClient; +import com.checkout.android_sdk.Response.CardTokenisationFail; +import com.checkout.android_sdk.Response.CardTokenisationResponse; +import com.checkout.android_sdk.Response.GooglePayTokenisationFail; +import com.checkout.android_sdk.Response.GooglePayTokenisationResponse; +import com.google.gson.Gson; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Helper class used to create HTTP calls + *

+ * This class handles interaction with the Checkout.com API + */ +public class HttpUtils { + + private @Nullable + CheckoutAPIClient.OnTokenGenerated mTokenListener; + private @Nullable + CheckoutAPIClient.OnGooglePayTokenGenerated mGooglePayTokenListener; + private Context mContext; + + public HttpUtils(Context context) { + //empty constructor + mContext = context; + } + + /** + * Used to do a HTTP call with the card details + *

+ * This method will perform an HTTP POST request to the Checkout.com API. + * The API call is async so it will us the callback to communicate the result + * This method is used to generate a card token + * + * @param key the public key of the customer + * @param url the request URL + * @param body the body of the request as a JSON String + */ + public void generateGooglePayToken(final String key, String url, String body) throws JSONException { + + RequestQueue queue = Volley.newRequestQueue(mContext); + + JsonObjectRequest portRequest = new JsonObjectRequest(Request.Method.POST, url, new JSONObject(body), + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + // Create a response object and populate it + GooglePayTokenisationResponse responseBody = new Gson().fromJson(response.toString(), GooglePayTokenisationResponse.class); + // Use the callback to send the response + if (mGooglePayTokenListener != null) { + mGooglePayTokenListener.onTokenGenerated(responseBody); + } + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + NetworkResponse networkResponse = error.networkResponse; + if (networkResponse != null && networkResponse.data != null) { + try { + JSONObject jsonError = new JSONObject(new String(networkResponse.data)); + GooglePayTokenisationFail errorBody = new Gson().fromJson(jsonError.toString(), GooglePayTokenisationFail.class); + if (mGooglePayTokenListener != null) { + mGooglePayTokenListener.onError(errorBody); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + } + ) { + @Override + public Map getHeaders() { + // Add the Authorisation headers + Map params = new HashMap<>(); + params.put("Authorization", key); + return params; + } + }; + // Enable retry policy since is not enabled by default in the Volley + portRequest.setRetryPolicy(new DefaultRetryPolicy(10000, 10, 1.0f)); + queue.add(portRequest); + } + + /** + * Used to do a HTTP call with the Google Pay's payload + *

+ * This method will perform an HTTP POST request to the Checkout.com API. + * The API call is async so it will us the callback to communicate the result. + * This method is used to generate a token for Google Pay + * + * @param key the public key of the customer + * @param url the request URL + * @param body the body of the request as a JSON String + */ + public void generateToken(final String key, String url, String body) throws JSONException { + + RequestQueue queue = Volley.newRequestQueue(mContext); + + JsonObjectRequest portRequest = new JsonObjectRequest(Request.Method.POST, url, new JSONObject(body), + new Response.Listener() { + @Override + public void onResponse(JSONObject response) { + if (mTokenListener != null) { + CardTokenisationResponse responseBody = new Gson().fromJson(response.toString(), CardTokenisationResponse.class); + mTokenListener.onTokenGenerated(responseBody); + } + + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + NetworkResponse networkResponse = error.networkResponse; + if (networkResponse != null && networkResponse.data != null) { + try { + JSONObject jsonError = new JSONObject(new String(networkResponse.data)); + CardTokenisationFail cardTokenisationFail = new Gson().fromJson(jsonError.toString(), CardTokenisationFail.class); + if (mTokenListener != null) { + mTokenListener.onError(cardTokenisationFail); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + } + ) { + @Override + public Map getHeaders() { + Map params = new HashMap<>(); + params.put("Authorization", key); + return params; + } + }; + portRequest.setRetryPolicy(new DefaultRetryPolicy(10000, 10, 1.0f)); + queue.add(portRequest); + } + + /** + * Used to set the callback listener for when the card token is generated + */ + public void setTokenListener(CheckoutAPIClient.OnTokenGenerated listener) { + mTokenListener = listener; + } + + /** + * Used to set the callback listener for when the token for Google Pay is generated + */ + public void setGooglePayTokenListener(CheckoutAPIClient.OnGooglePayTokenGenerated listener) { + mGooglePayTokenListener = listener; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Utils/PhoneUtils.java b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/PhoneUtils.java new file mode 100755 index 000000000..23b3439c8 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/PhoneUtils.java @@ -0,0 +1,267 @@ +package com.checkout.android_sdk.Utils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Helper class used to determine the phone prefix + *

+ * This class is used to take an ISO2 of a country and determine the phone prefix + */ +public class PhoneUtils { + + private static Map mCountryPhonePrefix = new HashMap<>(); + + static { + mCountryPhonePrefix.put("AF", "+93"); + mCountryPhonePrefix.put("AL", "+355"); + mCountryPhonePrefix.put("DZ", "+213"); + mCountryPhonePrefix.put("AD", "+376"); + mCountryPhonePrefix.put("AO", "+244"); + mCountryPhonePrefix.put("AG", "+1-268"); + mCountryPhonePrefix.put("AR", "+54"); + mCountryPhonePrefix.put("AM", "+374"); + mCountryPhonePrefix.put("AU", "+61"); + mCountryPhonePrefix.put("AT", "+43"); + mCountryPhonePrefix.put("AZ", "+994"); + mCountryPhonePrefix.put("BS", "+1-242"); + mCountryPhonePrefix.put("BH", "+973"); + mCountryPhonePrefix.put("BD", "+880"); + mCountryPhonePrefix.put("BB", "+1-246"); + mCountryPhonePrefix.put("BY", "+375"); + mCountryPhonePrefix.put("BE", "+32"); + mCountryPhonePrefix.put("BZ", "+501"); + mCountryPhonePrefix.put("BJ", "+229"); + mCountryPhonePrefix.put("BT", "+975"); + mCountryPhonePrefix.put("BO", "+591"); + mCountryPhonePrefix.put("BA", "+387"); + mCountryPhonePrefix.put("BW", "+267"); + mCountryPhonePrefix.put("BR", "+55"); + mCountryPhonePrefix.put("BN", "+673"); + mCountryPhonePrefix.put("BG", "+359"); + mCountryPhonePrefix.put("BF", "+226"); + mCountryPhonePrefix.put("BI", "+257"); + mCountryPhonePrefix.put("KH", "+855"); + mCountryPhonePrefix.put("CM", "+237"); + mCountryPhonePrefix.put("CA", "+1"); + mCountryPhonePrefix.put("CV", "+238"); + mCountryPhonePrefix.put("CF", "+236"); + mCountryPhonePrefix.put("TD", "+235"); + mCountryPhonePrefix.put("CL", "+56"); + mCountryPhonePrefix.put("CN", "+86"); + mCountryPhonePrefix.put("CO", "+57"); + mCountryPhonePrefix.put("KM", "+269"); + mCountryPhonePrefix.put("CD", "+243"); + mCountryPhonePrefix.put("CG", "+242"); + mCountryPhonePrefix.put("CR", "+506"); + mCountryPhonePrefix.put("CI", "+225"); + mCountryPhonePrefix.put("HR", "+385"); + mCountryPhonePrefix.put("CU", "+53"); + mCountryPhonePrefix.put("CY", "+357"); + mCountryPhonePrefix.put("CZ", "+420"); + mCountryPhonePrefix.put("DK", "+45"); + mCountryPhonePrefix.put("DJ", "+253"); + mCountryPhonePrefix.put("DM", "+1-767"); + mCountryPhonePrefix.put("DO", "+1-809and1-829"); + mCountryPhonePrefix.put("EC", "+593"); + mCountryPhonePrefix.put("EG", "+20"); + mCountryPhonePrefix.put("SV", "+503"); + mCountryPhonePrefix.put("GQ", "+240"); + mCountryPhonePrefix.put("ER", "+291"); + mCountryPhonePrefix.put("EE", "+372"); + mCountryPhonePrefix.put("ET", "+251"); + mCountryPhonePrefix.put("FJ", "+679"); + mCountryPhonePrefix.put("FI", "+358"); + mCountryPhonePrefix.put("FR", "+33"); + mCountryPhonePrefix.put("GA", "+241"); + mCountryPhonePrefix.put("GM", "+220"); + mCountryPhonePrefix.put("GE", "+995"); + mCountryPhonePrefix.put("DE", "+49"); + mCountryPhonePrefix.put("GH", "+233"); + mCountryPhonePrefix.put("GR", "+30"); + mCountryPhonePrefix.put("GD", "+1-473"); + mCountryPhonePrefix.put("GT", "+502"); + mCountryPhonePrefix.put("GN", "+224"); + mCountryPhonePrefix.put("GW", "+245"); + mCountryPhonePrefix.put("GY", "+592"); + mCountryPhonePrefix.put("HT", "+509"); + mCountryPhonePrefix.put("HN", "+504"); + mCountryPhonePrefix.put("HU", "+36"); + mCountryPhonePrefix.put("IS", "+354"); + mCountryPhonePrefix.put("IN", "+91"); + mCountryPhonePrefix.put("ID", "+62"); + mCountryPhonePrefix.put("IR", "+98"); + mCountryPhonePrefix.put("IQ", "+964"); + mCountryPhonePrefix.put("IE", "+353"); + mCountryPhonePrefix.put("IL", "+972"); + mCountryPhonePrefix.put("IT", "+39"); + mCountryPhonePrefix.put("JM", "+1-876"); + mCountryPhonePrefix.put("JP", "+81"); + mCountryPhonePrefix.put("JO", "+962"); + mCountryPhonePrefix.put("KZ", "+7"); + mCountryPhonePrefix.put("KE", "+254"); + mCountryPhonePrefix.put("KI", "+686"); + mCountryPhonePrefix.put("KP", "+850"); + mCountryPhonePrefix.put("KR", "+82"); + mCountryPhonePrefix.put("KW", "+965"); + mCountryPhonePrefix.put("KG", "+996"); + mCountryPhonePrefix.put("LA", "+856"); + mCountryPhonePrefix.put("LV", "+371"); + mCountryPhonePrefix.put("LB", "+961"); + mCountryPhonePrefix.put("LS", "+266"); + mCountryPhonePrefix.put("LR", "+231"); + mCountryPhonePrefix.put("LY", "+218"); + mCountryPhonePrefix.put("LI", "+423"); + mCountryPhonePrefix.put("LT", "+370"); + mCountryPhonePrefix.put("LU", "+352"); + mCountryPhonePrefix.put("MK", "+389"); + mCountryPhonePrefix.put("MG", "+261"); + mCountryPhonePrefix.put("MW", "+265"); + mCountryPhonePrefix.put("MY", "+60"); + mCountryPhonePrefix.put("MV", "+960"); + mCountryPhonePrefix.put("ML", "+223"); + mCountryPhonePrefix.put("MT", "+356"); + mCountryPhonePrefix.put("MH", "+692"); + mCountryPhonePrefix.put("MR", "+222"); + mCountryPhonePrefix.put("MU", "+230"); + mCountryPhonePrefix.put("MX", "+52"); + mCountryPhonePrefix.put("FM", "+691"); + mCountryPhonePrefix.put("MD", "+373"); + mCountryPhonePrefix.put("MC", "+377"); + mCountryPhonePrefix.put("MN", "+976"); + mCountryPhonePrefix.put("ME", "+382"); + mCountryPhonePrefix.put("MA", "+212"); + mCountryPhonePrefix.put("MZ", "+258"); + mCountryPhonePrefix.put("MM", "+95"); + mCountryPhonePrefix.put("NA", "+264"); + mCountryPhonePrefix.put("NR", "+674"); + mCountryPhonePrefix.put("NP", "+977"); + mCountryPhonePrefix.put("NL", "+31"); + mCountryPhonePrefix.put("NZ", "+64"); + mCountryPhonePrefix.put("NI", "+505"); + mCountryPhonePrefix.put("NE", "+227"); + mCountryPhonePrefix.put("NG", "+234"); + mCountryPhonePrefix.put("NO", "+47"); + mCountryPhonePrefix.put("OM", "+968"); + mCountryPhonePrefix.put("PK", "+92"); + mCountryPhonePrefix.put("PW", "+680"); + mCountryPhonePrefix.put("PA", "+507"); + mCountryPhonePrefix.put("PG", "+675"); + mCountryPhonePrefix.put("PY", "+595"); + mCountryPhonePrefix.put("PE", "+51"); + mCountryPhonePrefix.put("PH", "+63"); + mCountryPhonePrefix.put("PL", "+48"); + mCountryPhonePrefix.put("PT", "+351"); + mCountryPhonePrefix.put("QA", "+974"); + mCountryPhonePrefix.put("RO", "+40"); + mCountryPhonePrefix.put("RU", "+7"); + mCountryPhonePrefix.put("RW", "+250"); + mCountryPhonePrefix.put("KN", "+1-869"); + mCountryPhonePrefix.put("LC", "+1-758"); + mCountryPhonePrefix.put("VC", "+1-784"); + mCountryPhonePrefix.put("WS", "+685"); + mCountryPhonePrefix.put("SM", "+378"); + mCountryPhonePrefix.put("ST", "+239"); + mCountryPhonePrefix.put("SA", "+966"); + mCountryPhonePrefix.put("SN", "+221"); + mCountryPhonePrefix.put("RS", "+381"); + mCountryPhonePrefix.put("SC", "+248"); + mCountryPhonePrefix.put("SL", "+232"); + mCountryPhonePrefix.put("SG", "+65"); + mCountryPhonePrefix.put("SK", "+421"); + mCountryPhonePrefix.put("SI", "+386"); + mCountryPhonePrefix.put("SB", "+677"); + mCountryPhonePrefix.put("SO", "+252"); + mCountryPhonePrefix.put("ZA", "+27"); + mCountryPhonePrefix.put("ES", "+34"); + mCountryPhonePrefix.put("LK", "+94"); + mCountryPhonePrefix.put("SD", "+249"); + mCountryPhonePrefix.put("SR", "+597"); + mCountryPhonePrefix.put("SZ", "+268"); + mCountryPhonePrefix.put("SE", "+46"); + mCountryPhonePrefix.put("CH", "+41"); + mCountryPhonePrefix.put("SY", "+963"); + mCountryPhonePrefix.put("TJ", "+992"); + mCountryPhonePrefix.put("TZ", "+255"); + mCountryPhonePrefix.put("TH", "+66"); + mCountryPhonePrefix.put("TL", "+670"); + mCountryPhonePrefix.put("TG", "+228"); + mCountryPhonePrefix.put("TO", "+676"); + mCountryPhonePrefix.put("TT", "+1-868"); + mCountryPhonePrefix.put("TN", "+216"); + mCountryPhonePrefix.put("TR", "+90"); + mCountryPhonePrefix.put("TM", "+993"); + mCountryPhonePrefix.put("TV", "+688"); + mCountryPhonePrefix.put("UG", "+256"); + mCountryPhonePrefix.put("UA", "+380"); + mCountryPhonePrefix.put("AE", "+971"); + mCountryPhonePrefix.put("GB", "+44"); + mCountryPhonePrefix.put("US", "+1"); + mCountryPhonePrefix.put("UY", "+598"); + mCountryPhonePrefix.put("UZ", "+998"); + mCountryPhonePrefix.put("VU", "+678"); + mCountryPhonePrefix.put("VA", "+379"); + mCountryPhonePrefix.put("VE", "+58"); + mCountryPhonePrefix.put("VN", "+84"); + mCountryPhonePrefix.put("YE", "+967"); + mCountryPhonePrefix.put("ZM", "+260"); + mCountryPhonePrefix.put("ZW", "+263"); + mCountryPhonePrefix.put("TW", "+886"); + mCountryPhonePrefix.put("CX", "+61"); + mCountryPhonePrefix.put("CC", "+61"); + mCountryPhonePrefix.put("NF", "+672"); + mCountryPhonePrefix.put("NC", "+687"); + mCountryPhonePrefix.put("PF", "+689"); + mCountryPhonePrefix.put("YT", "+262"); + mCountryPhonePrefix.put("PM", "+508"); + mCountryPhonePrefix.put("WF", "+681"); + mCountryPhonePrefix.put("CK", "+682"); + mCountryPhonePrefix.put("NU", "+683"); + mCountryPhonePrefix.put("TK", "+690"); + mCountryPhonePrefix.put("GG", "+44"); + mCountryPhonePrefix.put("IM", "+44"); + mCountryPhonePrefix.put("JE", "+44"); + mCountryPhonePrefix.put("AI", "+1-264"); + mCountryPhonePrefix.put("BM", "+1-441"); + mCountryPhonePrefix.put("IO", "+246"); + mCountryPhonePrefix.put("VG", "+1-284"); + mCountryPhonePrefix.put("KY", "+1-345"); + mCountryPhonePrefix.put("FK", "+500"); + mCountryPhonePrefix.put("GI", "+350"); + mCountryPhonePrefix.put("MS", "+1-664"); + mCountryPhonePrefix.put("SH", "+290"); + mCountryPhonePrefix.put("TC", "+1-649"); + mCountryPhonePrefix.put("MP", "+1-670"); + mCountryPhonePrefix.put("PR", "+1-787and1-939"); + mCountryPhonePrefix.put("AS", "+1-684"); + mCountryPhonePrefix.put("GU", "+1-671"); + mCountryPhonePrefix.put("VI", "+1-340"); + mCountryPhonePrefix.put("HK", "+852"); + mCountryPhonePrefix.put("MO", "+853"); + mCountryPhonePrefix.put("FO", "+298"); + mCountryPhonePrefix.put("GL", "+299"); + mCountryPhonePrefix.put("GF", "+594"); + mCountryPhonePrefix.put("GP", "+590"); + mCountryPhonePrefix.put("MQ", "+596"); + mCountryPhonePrefix.put("RE", "+262"); + mCountryPhonePrefix.put("AX", "+358-18"); + mCountryPhonePrefix.put("AW", "+297"); + mCountryPhonePrefix.put("AN", "+599"); + mCountryPhonePrefix.put("SJ", "+47"); + mCountryPhonePrefix.put("AC", "+247"); + mCountryPhonePrefix.put("TA", "+290"); + mCountryPhonePrefix.put("CS", "+381"); + mCountryPhonePrefix.put("PS", "+970"); + mCountryPhonePrefix.put("EH", "+212"); + } + + /** + * This method is used to determine the phone prefix based on the country ISO2 + * + * @param iso ISO2 of the country + * @return phone prefix as String + */ + public static String getPrefix(String iso) { + return mCountryPhonePrefix.get(iso); + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/Utils/TouchlessViewPager.java b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/TouchlessViewPager.java new file mode 100755 index 000000000..be98165b7 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/Utils/TouchlessViewPager.java @@ -0,0 +1,40 @@ +package com.checkout.android_sdk.Utils; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.MotionEvent; + +/** + * Custom ViewPager that is not scrollable + */ +public class TouchlessViewPager extends ViewPager { + + public TouchlessViewPager(@NonNull Context context) { + this(context, null); + } + + public TouchlessViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + /** + * Prevent swiping + */ + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + // Never allow swiping to switch between pages + return false; + } + + /** + * Prevent swiping + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + // Never allow swiping to switch between pages + return false; + } +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/View/BillingDetailsView.java b/android-sdk/src/main/java/com/checkout/android_sdk/View/BillingDetailsView.java new file mode 100755 index 000000000..2ce39d572 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/View/BillingDetailsView.java @@ -0,0 +1,481 @@ +package com.checkout.android_sdk.View; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.checkout.android_sdk.Input.AddressOneInput; +import com.checkout.android_sdk.Input.CountryInput; +import com.checkout.android_sdk.Input.DefaultInput; +import com.checkout.android_sdk.Input.PhoneInput; +import com.checkout.android_sdk.R; +import com.checkout.android_sdk.Store.DataStore; + +import java.util.Locale; + +/** + * The controller of the billing details view page + *

+ * This class handles interaction with the custom inputs in the billing details form. + * The state of the view is handled here, so are action like focus changes, full form + * validation, listeners, persistence over orientation. + */ +public class BillingDetailsView extends LinearLayout { + /** + * The callback used to indicate is the billing details were completed + *

+ * After the user completes their details and the form is valid this callback will + * be used to communicate to the parent that teh focus needs to change + */ + public interface Listener { + void onBillingCompleted(); + + void onBillingCanceled(); + } + + /** + * The callback is used to communicate with the name input + *

+ * The custom {@link DefaultInput} takes care takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final DefaultInput.Listener mNameListener = new DefaultInput.Listener() { + @Override + public void onInputFinish(String value) { + mDatastore.setCustomerName(value); + } + + @Override + public void clearInputError() { + mNameLayout.setError(null); + mNameLayout.setErrorEnabled(false); + } + + }; + + /** + * The callback is used to communicate with the country input + *

+ * The custom {@link CountryInput} takes care of populating the values in the spinner + * and will trigger this callback when the user selects a new option. State is update + * accordingly. Moreover, the phone prefix is added bade on the country selected. + */ + private final CountryInput.CountryListener mCountryListener = new CountryInput.CountryListener() { + @Override + public void onCountryInputFinish(String country, String prefix) { + mDatastore.setCustomerCountry(country); + mDatastore.setCustomerPhonePrefix(prefix); + mPhone.setText(prefix + " "); + mAddressOne.requestFocus(); + mAddressOne.performClick(); + } + }; + + /** + * The callback is used to communicate with the address one input + *

+ * The custom {@link AddressOneInput} takes care takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final AddressOneInput.AddressOneListener mAddressOneListener = new AddressOneInput.AddressOneListener() { + + @Override + public void onAddressOneInputFinish(String value) { + mDatastore.setCustomerAddress1(value); + } + + @Override + public void clearAddressOneError() { + mAddressOneLayout.setError(null); + mAddressOneLayout.setErrorEnabled(false); + } + }; + + /** + * The callback is used to communicate with the address two input + *

+ * The custom {@link DefaultInput} takes care takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final DefaultInput.Listener mAddressTwoListener = new DefaultInput.Listener() { + @Override + public void onInputFinish(String value) { + mDatastore.setCustomerAddress2(value); + } + + @Override + public void clearInputError() { + mAddressTwoLayout.setError(null); + mAddressTwoLayout.setErrorEnabled(false); + } + }; + + /** + * The callback is used to communicate with the city input + *

+ * The custom {@link DefaultInput} takes care takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final DefaultInput.Listener mCityListener = new DefaultInput.Listener() { + @Override + public void onInputFinish(String value) { + mDatastore.setCustomerCity(value); + } + + @Override + public void clearInputError() { + mCityLayout.setError(null); + mCityLayout.setErrorEnabled(false); + } + }; + + /** + * The callback is used to communicate with the state input + *

+ * The custom {@link DefaultInput} takes care takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final DefaultInput.Listener mStateListener = new DefaultInput.Listener() { + @Override + public void onInputFinish(String value) { + mDatastore.setCustomerState(value); + } + + @Override + public void clearInputError() { + mStateLayout.setError(null); + mStateLayout.setErrorEnabled(false); + } + + }; + + /** + * The callback is used to communicate with the zip input + *

+ * The custom {@link DefaultInput} takes care takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final DefaultInput.Listener mZipListener = new DefaultInput.Listener() { + @Override + public void onInputFinish(String value) { + mDatastore.setCustomerZipcode(value); + } + + @Override + public void clearInputError() { + mZipLayout.setError(null); + mZipLayout.setErrorEnabled(false); + } + }; + + /** + * The callback is used to communicate with the phone input + *

+ * The custom {@link PhoneInput} takes care takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final PhoneInput.PhoneListener mPhoneListener = new PhoneInput.PhoneListener() { + @Override + public void onPhoneInputFinish(String phone) { + mDatastore.setCustomerPhone(phone.replace(mDatastore.getCustomerPhonePrefix(), "")); + } + + @Override + public void clearPhoneError() { + mPhoneLayout.setError(null); + mPhoneLayout.setErrorEnabled(false); + } + }; + + private @Nullable + BillingDetailsView.Listener mListener; + private @Nullable + Context mContext; + private Button mDone; + private Button mClear; + private android.support.v7.widget.Toolbar mToolbar; + private DefaultInput mName; + private TextInputLayout mNameLayout; + private CountryInput mCountryInput; + private AddressOneInput mAddressOne; + private DefaultInput mAddressTwo; + private DefaultInput mCity; + private DefaultInput mState; + private DefaultInput mZip; + private PhoneInput mPhone; + private DataStore mDatastore = DataStore.getInstance(); + private TextInputLayout mAddressOneLayout; + private TextInputLayout mAddressTwoLayout; + private TextInputLayout mCityLayout; + private TextInputLayout mStateLayout; + private TextInputLayout mZipLayout; + private TextInputLayout mPhoneLayout; + + public BillingDetailsView(Context context) { + this(context, null); + } + + public BillingDetailsView(@Nullable Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element and pass callbacks as well as setting up appropriate listeners + */ + private void init() { + inflate(mContext, R.layout.blling_details, this); + mToolbar = findViewById(R.id.my_toolbar); + + setFocusableInTouchMode(true); + + mAddressOneLayout = findViewById(R.id.address_one_input_layout); + mAddressTwoLayout = findViewById(R.id.address_two_input_layout); + mCityLayout = findViewById(R.id.city_input_layout); + mStateLayout = findViewById(R.id.state_input_layout); + mZipLayout = findViewById(R.id.zipcode_input_layout); + mPhoneLayout = findViewById(R.id.phone_input_layout); + + // trigger focus change to the card details view on the toolbar back button press + mToolbar.setNavigationOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mListener != null) { + mListener.onBillingCanceled(); + } + } + }); + + mName = findViewById(R.id.name_input); + mNameLayout = findViewById(R.id.name_input_layout); + mName.setListener(mNameListener); + + mCountryInput = findViewById(R.id.country_input); + mCountryInput.setCountryListener(mCountryListener); + + mAddressOne = findViewById(R.id.address_one_input); + mAddressOne.setAddressOneListener(mAddressOneListener); + + mAddressTwo = findViewById(R.id.address_two_input); + mAddressTwo.setListener(mAddressTwoListener); + + mCity = findViewById(R.id.city_input); + mCity.setListener(mCityListener); + + mState = findViewById(R.id.state_input); + mState.setListener(mStateListener); + + mZip = findViewById(R.id.zipcode_input); + mZip.setListener(mZipListener); + + mPhone = findViewById(R.id.phone_input); + mPhone.setPhoneListener(mPhoneListener); + + mClear = findViewById(R.id.clear_button); + + mDone = findViewById(R.id.done_button); + + // Used to restore state on orientation changes + repopulateFields(); + + // Clear the state and the fields if the user chooses to press the clear button + mClear.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + resetFields(); + mDatastore.cleanBillingData(); + if (mListener != null) { + mListener.onBillingCanceled(); + } + mDatastore.setBillingCompleted(false); + } + }); + + // Is the form is valid indicate the billing was completed using the callback + // so the billing spinner can be updated adn teh focus can be changes + mDone.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (isValidForm()) { + if (mListener != null) { + mDatastore.setBillingCompleted(true); + mListener.onBillingCompleted(); + } + } + } + }); + + requestFocus(); + } + + /** + * Used to restore state on orientation changes + *

+ * The method will repopulate all the card input fields with the latest state they were in + * if the device orientation changes, and therefore avoiding the text inputs to be cleared. + */ + private void repopulateFields() { + // Repopulate name + mName.setText(mDatastore.getCustomerName()); + + // Repopulate country + Locale[] locale = Locale.getAvailableLocales(); + String country; + + for (Locale loc : locale) { + country = loc.getDisplayCountry(); + if (loc.getCountry().equals(mDatastore.getCustomerCountry())) { + mCountryInput.setSelection(((ArrayAdapter) mCountryInput.getAdapter()) + .getPosition(country)); + } + } + + // Repopulate address line 1 + mAddressOne.setText(mDatastore.getCustomerAddress1()); + + // Repopulate address line 1 + mAddressTwo.setText(mDatastore.getCustomerAddress2()); + + // Repopulate city + mCity.setText(mDatastore.getCustomerCity()); + + // Repopulate state + mState.setText(mDatastore.getCustomerState()); + + // Repopulate zip/post code + mZip.setText(mDatastore.getCustomerZipcode()); + + // Repopulate phone + mPhone.setText(mDatastore.getCustomerPhone()); + } + + /** + * Used to indicate the validity of the billing details from + *

+ * The method will check if the inputs are valid. + * This method will also populate the field error accordingly + * + * @return boolean abut form validity + */ + private boolean isValidForm() { + boolean result = true; + + if (mName.length() < 3) { + mNameLayout.setError(getResources().getString(R.string.error_name)); + result = false; + } + + if (mCountryInput.getSelectedItemPosition() == 0) { + ((TextView) mCountryInput.getSelectedView()).setError(getResources().getString(R.string.error_country)); + result = false; + } + if (mAddressOne.length() < 3) { + mAddressOneLayout.setError(getResources().getString(R.string.error_address_one)); + result = false; + } + + if (mCity.length() < 2) { + mCityLayout.setError(getResources().getString(R.string.error_city)); + result = false; + } + + if (mState.length() < 3) { + mStateLayout.setError(getResources().getString(R.string.error_state)); + result = false; + } + + if (mZip.length() < 3) { + mZipLayout.setError(getResources().getString(R.string.error_postcode)); + result = false; + } + + if (mPhone.length() < 3) { + mPhoneLayout.setError(getResources().getString(R.string.error_phone)); + result = false; + } + + return result; + } + + /** + * Used to clear the text from teh fields + */ + private void resetFields() { + mName.setText(""); + mNameLayout.setError(null); + mNameLayout.setErrorEnabled(false); + mCountryInput.setSelection(0); + ((TextView) mCountryInput.getSelectedView()).setError(null); + mAddressOne.setText(""); + mAddressOneLayout.setError(null); + mAddressOneLayout.setErrorEnabled(false); + mAddressTwo.setText(""); + mAddressTwoLayout.setError(null); + mAddressTwoLayout.setErrorEnabled(false); + mCity.setText(""); + mCityLayout.setError(null); + mCityLayout.setErrorEnabled(false); + mState.setText(""); + mStateLayout.setError(null); + mStateLayout.setErrorEnabled(false); + mZip.setText(""); + mZipLayout.setError(null); + mZipLayout.setErrorEnabled(false); + mPhone.setText(""); + mPhoneLayout.setError(null); + mPhoneLayout.setErrorEnabled(false); + } + + // Move to previous view on back button pressed + @Override + public boolean dispatchKeyEventPreIme(KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + // Prevent back button to trigger the mListener is any is focused + if (mListener != null && + !mAddressOne.hasFocus() && + !mName.hasFocus() && + !mAddressTwo.hasFocus() && + !mCity.hasFocus() && + !mState.hasFocus() && + !mZip.hasFocus() && + !mPhone.hasFocus()) { + mListener.onBillingCanceled(); + return true; + } else { + requestFocus(); + return false; + } + } + + return super.dispatchKeyEventPreIme(event); + } + + + /** + * Used to set the callback listener for when the card details page is requested + */ + public void setGoToCardDetailsListener(BillingDetailsView.Listener listener) { + mListener = listener; + } + +} diff --git a/android-sdk/src/main/java/com/checkout/android_sdk/View/CardDetailsView.java b/android-sdk/src/main/java/com/checkout/android_sdk/View/CardDetailsView.java new file mode 100755 index 000000000..893c1c361 --- /dev/null +++ b/android-sdk/src/main/java/com/checkout/android_sdk/View/CardDetailsView.java @@ -0,0 +1,441 @@ +package com.checkout.android_sdk.View; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; +import android.support.v7.widget.Toolbar; +import android.text.InputFilter; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.checkout.android_sdk.Input.BillingInput; +import com.checkout.android_sdk.Input.CardInput; +import com.checkout.android_sdk.Input.DefaultInput; +import com.checkout.android_sdk.Input.MonthInput; +import com.checkout.android_sdk.Input.YearInput; +import com.checkout.android_sdk.R; +import com.checkout.android_sdk.Store.DataStore; +import com.checkout.android_sdk.Utils.CardUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The controller of the card details view page + *

+ * This class handles interaction with the custom inputs in the card details form. + * The state of the view is handled here, so are action like focus changes, full form + * validation, listeners, persistence over orientation. + */ +public class CardDetailsView extends LinearLayout { + + /** + * The callback used to indicate the form submission + *

+ * After the user completes their details and the form is valid this callback will + * be used to communicate to the parent and start the necessary API call(s). + */ + public interface DetailsCompleted { + void onDetailsCompleted(); + } + + /** + * The callback used to indicate the view needs to moved to the billing details page + *

+ * When the user selects the option to add billing details this callback will be used + * to communicate to the parent the focus change is requested + */ + public interface GoToBillingListener { + void onGoToBillingPressed(); + } + + /** + * The callback is used to communicate with the card input + *

+ * The custom {@link CardInput} takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final CardInput.Listener mCardInputListener = new CardInput.Listener() { + @Override + public void onCardInputFinish(String number) { + mDataStore.setValidCardNumber(true); + } + + @Override + public void onCardError() { + mCardLayout.setError(getResources().getString(R.string.error_card_number)); + mDataStore.setValidCardNumber(false); + } + + @Override + public void onClearCardError() { + mCardLayout.setError(null); + mCardLayout.setErrorEnabled(false); + } + }; + + /** + * The callback is used to communicate with the month input + *

+ * The custom {@link MonthInput} takes care of populating the values in the spinner + * and will trigger this callback when the user selects a new option. State is update + * accordingly. + */ + private final MonthInput.MonthListener mMonthInputListener = new MonthInput.MonthListener() { + @Override + public void onMonthInputFinish(String month) { + mDataStore.setCardMonth(month); + mDataStore.setValidCardMonth(true); + } + }; + + /** + * The callback is used to communicate with the year input + *

+ * The custom {@link YearInput} takes care of populating the values in the spinner + * and will trigger this callback when the user selects a new option. State is update + * accordingly. + */ + private final YearInput.YearListener mYearInputListener = new YearInput.YearListener() { + @Override + public void onYearInputFinish(String year) { + mDataStore.setCardYear(year); + mDataStore.setValidCardYear(true); + ((TextView) mMonthInput.getSelectedView()).setError(null); + } + }; + + /** + * The callback is used to communicate with the cvv input + *

+ * The custom {@link DefaultInput} takes care of the validation and it uses a callback + * to indicate this controller if there is any error or if the error state needs to + * be cleared. State is also updates based on the outcome of the input. + */ + private final DefaultInput.Listener mCvvInputListener = new DefaultInput.Listener() { + @Override + public void onInputFinish(String value) { + mDataStore.setCardCvv(value); + if (value.length() == mDataStore.getCvvLength()) { + mDataStore.setValidCardCvv(true); + } else { + mDataStore.setValidCardCvv(false); + } + } + + @Override + public void clearInputError() { + mCvvLayout.setError(null); + mCvvLayout.setErrorEnabled(false); + } + }; + + /** + * The callback is used to trigger the focus change to the billing page + */ + private final BillingInput.BillingListener mBillingInputListener = new BillingInput.BillingListener() { + @Override + public void onGoToBilling() { + if (mGotoBillingListener != null) { + mGotoBillingListener.onGoToBillingPressed(); + } + } + }; + + DataStore mDataStore = DataStore.getInstance(); + private @Nullable + CardDetailsView.GoToBillingListener mGotoBillingListener; + private @Nullable + CardDetailsView.DetailsCompleted mDetailsCompletedListener; + private Context mContext; + + private CardInput mCardInput; + private MonthInput mMonthInput; + private YearInput mYearInput; + private BillingInput mGoToBilling; + private DefaultInput mCvvInput; + private TextInputLayout mCardLayout; + private TextInputLayout mCvvLayout; + private Button mPayButton; + private TextView mBillingHelper; + private Toolbar mToolbar; + private LinearLayout mAcceptedCardsView; + private AttributeSet attrs; + + public CardDetailsView(Context context) { + this(context, null); + } + + public CardDetailsView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(); + } + + /** + * The UI initialisation + *

+ * Used to initialise element and pass callbacks as well as setting up appropriate listeners + */ + private void init() { + inflate(mContext, R.layout.card_details, this); + + mToolbar = findViewById(R.id.my_toolbar); + + mCardInput = findViewById(R.id.card_input); + mCardLayout = findViewById(R.id.card_input_layout); + mCardInput.setCardListener(mCardInputListener); + + mMonthInput = findViewById(R.id.month_input); + mMonthInput.setMonthListener(mMonthInputListener); + + mYearInput = findViewById(R.id.year_input); + mYearInput.setYearListener(mYearInputListener); + + mCvvInput = findViewById(R.id.cvv_input); + mCvvLayout = findViewById(R.id.cvv_input_layout); + mCvvInput.setListener(mCvvInputListener); + + mBillingHelper = findViewById(R.id.billing_helper_text); + mGoToBilling = findViewById(R.id.go_to_billing); + + // Hide billing details options based on the module initialisation option + if (!mDataStore.getBillingVisibility()) { + mBillingHelper.setVisibility(GONE); + mGoToBilling.setVisibility(GONE); + } else { + mGoToBilling.setBillingListener(mBillingInputListener); + } + + mPayButton = findViewById(R.id.pay_button); + + mPayButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mCvvInput.clearFocus(); + if (mDetailsCompletedListener != null && isValidForm()) { + mDetailsCompletedListener.onDetailsCompleted(); + } + } + }); + + // Restore state in case the orientation changes + repopulateField(); + + // Populate accepted cards + mAcceptedCardsView = findViewById(R.id.card_icons_layout); + setAcceptedCards(); + } + + /** + * Used to restore state on orientation changes + *

+ * The method will repopulate all the card input fields with the latest state they were in + * if the device orientation changes, and therefore avoiding the text inputs to be cleared. + */ + private void repopulateField() { + // Repopulate card number + if (DataStore.getInstance().getCardNumber() != null) { + if (mDataStore.getCardNumber() != null) { + // Get card type based on the saved card number + CardUtils.Cards cardType = CardUtils.getType(DataStore.getInstance().getCardNumber()); + // Set the CardInput maximum length based on the type of card + mCardInput.setFilters(new InputFilter[]{new InputFilter.LengthFilter(cardType.maxCardLength)}); + // Set the CardInput icon based on the type of card + mCardInput.setCardTypeIcon(cardType); + // Check if the card is valid + mCardInput.checkIfCardIsValid(mDataStore.getCardNumber(), cardType); + // Update the card field with the last input value + String formattedCard = CardUtils.getFormattedCardNumber(mDataStore.getCardNumber()); + mCardInput.setText(formattedCard); + // Set the cursor to the end of the input + mCardInput.setSelection(formattedCard.length()); + } + } + + //Repopulate card month + if (DataStore.getInstance().getCardMonth() != null) { + MonthInput.Months[] months = MonthInput.Months.values(); + mMonthInput.setSelection(months[Integer.parseInt(DataStore.getInstance().getCardMonth()) - 1].number - 1); + } + + //Repopulate card year + if (DataStore.getInstance().getCardYear() != null) { + try { + mYearInput.setSelection(((ArrayAdapter) mYearInput.getAdapter()).getPosition(DataStore.getInstance().getCardYear())); + } catch (Exception e) { + e.printStackTrace(); + } + } + + //Repopulate card cvv + if (DataStore.getInstance().getCardCvv() != null) { + // Update the cvv field with the last input value + mCvvInput.setText(mDataStore.getCardCvv()); + } + + //Repopulate billing spinner + updateBillingSpinner(); + } + + /** + * Used to indicate the validity of the full card from + *

+ * The method will check if the inputs are valid and also check the relation between the inputs + * to ensure validity (e.g. month to year relation). + * This method will also populate the field error accordingly + * + * @return boolean abut form validity + */ + private boolean isValidForm() { + + boolean outcome = true; + + checkFullDate(); + + if (!mDataStore.isValidCardMonth()) { + outcome = false; + } + + if (!mDataStore.isValidCardNumber()) { + mCardLayout.setError(getResources().getString(R.string.error_card_number)); + outcome = false; + } + + if (mCvvInput.getText().length() == mDataStore.getCvvLength()) { + mDataStore.setValidCardCvv(true); + } else { + mDataStore.setValidCardCvv(false); + } + + if (!mDataStore.isValidCardCvv()) { + mCvvLayout.setError(getResources().getString(R.string.error_cvv)); + outcome = false; + } else { + mCvvLayout.setError(null); + mCvvLayout.setErrorEnabled(false); + } + + return outcome; + } + + /** + * Used to indicate the validity of the date + *

+ * The method will check if the values from the {@link MonthInput} and {@link YearInput} are + * not representing a date in the past. + * + * @return boolean abut form validity of the date + */ + private boolean checkFullDate() { + + // Check is the state contain the date and if it is check if the current selected + // values are valid. Display error if applicable. + if (mDataStore.getCardYear() != null && + mDataStore.getCardYear() != null && + !CardUtils.isValidDate(mDataStore.getCardMonth(), mDataStore.getCardYear())) { + mDataStore.setValidCardMonth(false); + ((TextView) mMonthInput.getSelectedView()).setError(getResources() + .getString(R.string.error_expiration_date)); + return false; + } + mDataStore.setValidCardMonth(true); + return true; + } + + /** + * Used to clear/reset the billing details spinner + *

+ * The method will be used to clear/reset the billing details spinner in case the user + * has decide to clear their details from the {@link BillingDetailsView} + */ + public void clearBillingSpinner() { + List billingElement = new ArrayList<>(); + + // Set the default value fo the spinner + billingElement.add(getResources().getString(R.string.select_billing_details)); + billingElement.add(getResources().getString(R.string.billing_details_add)); + + ArrayAdapter dataAdapter = new ArrayAdapter<>(mContext, + android.R.layout.simple_spinner_item, billingElement); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mGoToBilling.setAdapter(dataAdapter); + mGoToBilling.setSelection(0); + } + + /** + * Used to populate the billing spinner with the user billing details + *

+ * The method will be called when the user has successfully saved their billing details and + * to visually confirm that, the spinner is populated with the details and the default ADD + * button is replaced by a EDIT button + */ + public void updateBillingSpinner() { + + String address = mDataStore.getCustomerAddress1() + + ", " + mDataStore.getCustomerAddress2() + + ", " + mDataStore.getCustomerCity() + + ", " + mDataStore.getCustomerState(); + + // Avoid updates for there are no values set + if (address.length() > 6) { + List billingElement = new ArrayList<>(); + + billingElement.add(address); + billingElement.add("Edit"); + + ArrayAdapter dataAdapter = new ArrayAdapter<>(mContext, + android.R.layout.simple_spinner_item, billingElement); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mGoToBilling.setAdapter(dataAdapter); + mGoToBilling.setSelection(0); + } + } + + /** + * Used dynamically populate the accepted cards view is the option is used + */ + public void setAcceptedCards() { + + CardUtils.Cards[] allCards = mDataStore.getAcceptedCards() != null + ? mDataStore.getAcceptedCards() + : (CardUtils.Cards[]) Arrays.asList(CardUtils.Cards.values()).toArray(); + + int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 35, getResources().getDisplayMetrics()); + int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()); + + for (CardUtils.Cards card : allCards) { + ImageView image = new ImageView(mContext); + image.setLayoutParams(new android.view.ViewGroup.LayoutParams(size, size)); + image.setImageResource(card.resourceId); + MarginLayoutParams marginParams = new MarginLayoutParams(image.getLayoutParams()); + marginParams.setMargins(0, 0, margin, 0); + + // Adds the view to the layout + mAcceptedCardsView.addView(image); + } + + } + + /** + * Used to set the callback listener for when the form is submitted + */ + public void setDetailsCompletedListener(CardDetailsView.DetailsCompleted listener) { + mDetailsCompletedListener = listener; + } + + /** + * Used to set the callback listener for when the billing details page is requested + */ + public void setGoToBillingListener(GoToBillingListener listener) { + mGotoBillingListener = listener; + } +} diff --git a/android-sdk/src/main/res/drawable-hdpi/amex.png b/android-sdk/src/main/res/drawable-hdpi/amex.png new file mode 100755 index 000000000..036629a17 Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/amex.png differ diff --git a/android-sdk/src/main/res/drawable-hdpi/dinersclub.png b/android-sdk/src/main/res/drawable-hdpi/dinersclub.png new file mode 100755 index 000000000..482a594df Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/dinersclub.png differ diff --git a/android-sdk/src/main/res/drawable-hdpi/discover.png b/android-sdk/src/main/res/drawable-hdpi/discover.png new file mode 100755 index 000000000..1e4ba3bf4 Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/discover.png differ diff --git a/android-sdk/src/main/res/drawable-hdpi/jcb.png b/android-sdk/src/main/res/drawable-hdpi/jcb.png new file mode 100755 index 000000000..d06dc3a06 Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/jcb.png differ diff --git a/android-sdk/src/main/res/drawable-hdpi/maestro.png b/android-sdk/src/main/res/drawable-hdpi/maestro.png new file mode 100755 index 000000000..d09f7a473 Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/maestro.png differ diff --git a/android-sdk/src/main/res/drawable-hdpi/mastercard.png b/android-sdk/src/main/res/drawable-hdpi/mastercard.png new file mode 100755 index 000000000..7cdbfafca Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/mastercard.png differ diff --git a/android-sdk/src/main/res/drawable-hdpi/unionpay.png b/android-sdk/src/main/res/drawable-hdpi/unionpay.png new file mode 100755 index 000000000..b84d1d39b Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/unionpay.png differ diff --git a/android-sdk/src/main/res/drawable-hdpi/visa.png b/android-sdk/src/main/res/drawable-hdpi/visa.png new file mode 100755 index 000000000..2fe950be3 Binary files /dev/null and b/android-sdk/src/main/res/drawable-hdpi/visa.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/amex.png b/android-sdk/src/main/res/drawable-ldpi/amex.png new file mode 100755 index 000000000..253504400 Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/amex.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/dinersclub.png b/android-sdk/src/main/res/drawable-ldpi/dinersclub.png new file mode 100755 index 000000000..d33c2ecb4 Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/dinersclub.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/discover.png b/android-sdk/src/main/res/drawable-ldpi/discover.png new file mode 100755 index 000000000..384e279f8 Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/discover.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/jcb.png b/android-sdk/src/main/res/drawable-ldpi/jcb.png new file mode 100755 index 000000000..31ff30526 Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/jcb.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/maestro.png b/android-sdk/src/main/res/drawable-ldpi/maestro.png new file mode 100755 index 000000000..f5a653c07 Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/maestro.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/mastercard.png b/android-sdk/src/main/res/drawable-ldpi/mastercard.png new file mode 100755 index 000000000..a806a077a Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/mastercard.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/unionpay.png b/android-sdk/src/main/res/drawable-ldpi/unionpay.png new file mode 100755 index 000000000..96dfde339 Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/unionpay.png differ diff --git a/android-sdk/src/main/res/drawable-ldpi/visa.png b/android-sdk/src/main/res/drawable-ldpi/visa.png new file mode 100755 index 000000000..874eb8539 Binary files /dev/null and b/android-sdk/src/main/res/drawable-ldpi/visa.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/amex.png b/android-sdk/src/main/res/drawable-mdpi/amex.png new file mode 100755 index 000000000..020deb2ef Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/amex.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/dinersclub.png b/android-sdk/src/main/res/drawable-mdpi/dinersclub.png new file mode 100755 index 000000000..f97c6d53f Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/dinersclub.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/discover.png b/android-sdk/src/main/res/drawable-mdpi/discover.png new file mode 100755 index 000000000..27d0be98b Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/discover.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/jcb.png b/android-sdk/src/main/res/drawable-mdpi/jcb.png new file mode 100755 index 000000000..8c50ac276 Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/jcb.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/maestro.png b/android-sdk/src/main/res/drawable-mdpi/maestro.png new file mode 100755 index 000000000..a973f9446 Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/maestro.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/mastercard.png b/android-sdk/src/main/res/drawable-mdpi/mastercard.png new file mode 100755 index 000000000..ca2715534 Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/mastercard.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/unionpay.png b/android-sdk/src/main/res/drawable-mdpi/unionpay.png new file mode 100755 index 000000000..a35b9b3b9 Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/unionpay.png differ diff --git a/android-sdk/src/main/res/drawable-mdpi/visa.png b/android-sdk/src/main/res/drawable-mdpi/visa.png new file mode 100755 index 000000000..ff1defa0b Binary files /dev/null and b/android-sdk/src/main/res/drawable-mdpi/visa.png differ diff --git a/android-sdk/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android-sdk/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..c7bd21dbd --- /dev/null +++ b/android-sdk/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android-sdk/src/main/res/drawable-xhdpi/amex.png b/android-sdk/src/main/res/drawable-xhdpi/amex.png new file mode 100755 index 000000000..82ec3ea9b Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/amex.png differ diff --git a/android-sdk/src/main/res/drawable-xhdpi/dinersclub.png b/android-sdk/src/main/res/drawable-xhdpi/dinersclub.png new file mode 100755 index 000000000..e6e12f657 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/dinersclub.png differ diff --git a/android-sdk/src/main/res/drawable-xhdpi/discover.png b/android-sdk/src/main/res/drawable-xhdpi/discover.png new file mode 100755 index 000000000..5e269bfa4 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/discover.png differ diff --git a/android-sdk/src/main/res/drawable-xhdpi/jcb.png b/android-sdk/src/main/res/drawable-xhdpi/jcb.png new file mode 100755 index 000000000..3d0463987 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/jcb.png differ diff --git a/android-sdk/src/main/res/drawable-xhdpi/maestro.png b/android-sdk/src/main/res/drawable-xhdpi/maestro.png new file mode 100755 index 000000000..42ce139b4 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/maestro.png differ diff --git a/android-sdk/src/main/res/drawable-xhdpi/mastercard.png b/android-sdk/src/main/res/drawable-xhdpi/mastercard.png new file mode 100755 index 000000000..eca72aee2 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/mastercard.png differ diff --git a/android-sdk/src/main/res/drawable-xhdpi/unionpay.png b/android-sdk/src/main/res/drawable-xhdpi/unionpay.png new file mode 100755 index 000000000..38874a226 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/unionpay.png differ diff --git a/android-sdk/src/main/res/drawable-xhdpi/visa.png b/android-sdk/src/main/res/drawable-xhdpi/visa.png new file mode 100755 index 000000000..537a819c3 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xhdpi/visa.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/amex.png b/android-sdk/src/main/res/drawable-xxhdpi/amex.png new file mode 100755 index 000000000..d421a25fd Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/amex.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/dinersclub.png b/android-sdk/src/main/res/drawable-xxhdpi/dinersclub.png new file mode 100755 index 000000000..0b274fc44 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/dinersclub.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/discover.png b/android-sdk/src/main/res/drawable-xxhdpi/discover.png new file mode 100755 index 000000000..4827de703 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/discover.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/jcb.png b/android-sdk/src/main/res/drawable-xxhdpi/jcb.png new file mode 100755 index 000000000..169947004 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/jcb.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/maestro.png b/android-sdk/src/main/res/drawable-xxhdpi/maestro.png new file mode 100755 index 000000000..1def56790 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/maestro.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/mastercard.png b/android-sdk/src/main/res/drawable-xxhdpi/mastercard.png new file mode 100755 index 000000000..f169e7fbd Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/mastercard.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/unionpay.png b/android-sdk/src/main/res/drawable-xxhdpi/unionpay.png new file mode 100755 index 000000000..592d48b20 Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/unionpay.png differ diff --git a/android-sdk/src/main/res/drawable-xxhdpi/visa.png b/android-sdk/src/main/res/drawable-xxhdpi/visa.png new file mode 100755 index 000000000..71b4e310e Binary files /dev/null and b/android-sdk/src/main/res/drawable-xxhdpi/visa.png differ diff --git a/android-sdk/src/main/res/drawable/ic_launcher_background.xml b/android-sdk/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..d5fccc538 --- /dev/null +++ b/android-sdk/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-sdk/src/main/res/layout/blling_details.xml b/android-sdk/src/main/res/layout/blling_details.xml new file mode 100644 index 000000000..dbc353d74 --- /dev/null +++ b/android-sdk/src/main/res/layout/blling_details.xml @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +