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 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
+ *
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/.idea/gradle.xml b/demos/examples/.idea/gradle.xml
new file mode 100644
index 000000000..7ac24c777
--- /dev/null
+++ b/demos/examples/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/.idea/misc.xml b/demos/examples/.idea/misc.xml
new file mode 100644
index 000000000..99202cc2d
--- /dev/null
+++ b/demos/examples/.idea/misc.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/.idea/runConfigurations.xml b/demos/examples/.idea/runConfigurations.xml
new file mode 100644
index 000000000..7f68460d8
--- /dev/null
+++ b/demos/examples/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/.gitignore b/demos/examples/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/demos/examples/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/demos/examples/app/build.gradle b/demos/examples/app/build.gradle
new file mode 100644
index 000000000..a03e44777
--- /dev/null
+++ b/demos/examples/app/build.gradle
@@ -0,0 +1,32 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 27
+ defaultConfig {
+ applicationId "checkout.com.demo"
+ 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(dir: 'libs', include: ['*.jar'])
+ implementation 'com.android.support:appcompat-v7:27.1.1'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.2'
+ 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.github.checkout:frames-android:v2.0.0'
+ 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/demos/examples/app/proguard-rules.pro b/demos/examples/app/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/demos/examples/app/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/demos/examples/app/src/main/AndroidManifest.xml b/demos/examples/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..5737552f7
--- /dev/null
+++ b/demos/examples/app/src/main/AndroidManifest.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/java/checkout/com/demo/Demos/CustomFieldsDemo.java b/demos/examples/app/src/main/java/checkout/com/demo/Demos/CustomFieldsDemo.java
new file mode 100644
index 000000000..1623f2100
--- /dev/null
+++ b/demos/examples/app/src/main/java/checkout/com/demo/Demos/CustomFieldsDemo.java
@@ -0,0 +1,122 @@
+package checkout.com.demo.Demos;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+import com.checkout.android_sdk.CheckoutAPIClient;
+import com.checkout.android_sdk.CheckoutAPIClient.OnTokenGenerated;
+import com.checkout.android_sdk.Request.CardTokenisationRequest;
+import com.checkout.android_sdk.Response.CardTokenisationFail;
+import com.checkout.android_sdk.Response.CardTokenisationResponse;
+import com.checkout.android_sdk.Utils.CardUtils;
+import com.checkout.android_sdk.Utils.Environment;
+
+import checkout.com.demo.R;
+
+public class CustomFieldsDemo extends Activity {
+
+ CheckoutAPIClient mCheckoutAPIClient;
+
+ private final CheckoutAPIClient.OnTokenGenerated mTokenListener = new OnTokenGenerated() {
+
+ @Override
+ public void onTokenGenerated(CardTokenisationResponse token) {
+ displayMessage("Success!", token.getId());
+ }
+
+ @Override
+ public void onError(CardTokenisationFail error) {
+ displayMessage("Error!", error.getEventId());
+ }
+ };
+
+ private EditText mName, mCard, mMonth, mYear, mCvv;
+ private Button mPay;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.custom_fields_activity);
+
+ mName = findViewById(R.id.name_input);
+ mCard = findViewById(R.id.card_input);
+ mMonth = findViewById(R.id.month_input);
+ mYear = findViewById(R.id.year_input);
+ mCvv = findViewById(R.id.cvv_input);
+ mPay = findViewById(R.id.pay_button);
+
+ mCheckoutAPIClient = new CheckoutAPIClient(
+ this,
+ "pk_test_6e40a700-d563-43cd-89d0-f9bb17d35e73",
+ Environment.SANDBOX
+ );
+ mCheckoutAPIClient.setTokenListener(mTokenListener); // pass the callback
+
+ mPay.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (formValidationOutcome()) {
+ mCheckoutAPIClient.generateToken(
+ new CardTokenisationRequest(
+ mCard.getText().toString(),
+ mName.getText().toString(),
+ mMonth.getText().toString(),
+ mYear.getText().toString(),
+ mCvv.getText().toString()
+ )
+ );
+ }
+ }
+ });
+ }
+
+ private boolean formValidationOutcome() {
+
+ boolean outcome = true;
+
+ if (!CardUtils.isValidCard(mCard.getText().toString())) {
+ mCard.getBackground().setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
+ outcome = false;
+ } else {
+ mCard.getBackground().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_ATOP);
+ }
+ if (!CardUtils.isValidDate(mMonth.getText().toString(), mYear.getText().toString())) {
+ mMonth.getBackground().setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
+ mYear.getBackground().setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
+ outcome = false;
+ } else {
+ mMonth.getBackground().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_ATOP);
+ mYear.getBackground().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ if (!CardUtils.isValidCvv(mCvv.getText().toString(), CardUtils.getType(mCard.getText().toString()))) {
+ mCvv.getBackground().setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
+ outcome = false;
+ } else {
+ mCvv.getBackground().setColorFilter(Color.GREEN, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ return outcome;
+ }
+
+ private void displayMessage(String title, String message) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(title)
+ .setMessage(message)
+ .setCancelable(false)
+ .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ //do things
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+}
diff --git a/demos/examples/app/src/main/java/checkout/com/demo/Demos/CustomisationDemo.java b/demos/examples/app/src/main/java/checkout/com/demo/Demos/CustomisationDemo.java
new file mode 100644
index 000000000..8a926f60c
--- /dev/null
+++ b/demos/examples/app/src/main/java/checkout/com/demo/Demos/CustomisationDemo.java
@@ -0,0 +1,87 @@
+package checkout.com.demo.Demos;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+
+import com.checkout.android_sdk.CheckoutAPIClient;
+import com.checkout.android_sdk.PaymentForm;
+import com.checkout.android_sdk.Request.CardTokenisationRequest;
+import com.checkout.android_sdk.Response.CardTokenisationFail;
+import com.checkout.android_sdk.Response.CardTokenisationResponse;
+import com.checkout.android_sdk.Utils.CardUtils.Cards;
+import com.checkout.android_sdk.Utils.Environment;
+
+import checkout.com.demo.R;
+
+public class CustomisationDemo extends Activity {
+
+ private PaymentForm mPaymentForm; // include the payment form
+ private CheckoutAPIClient mCheckoutAPIClient; // include the API client
+ private ProgressDialog progress = null;
+
+ PaymentForm.OnSubmitForm mSubmitListener = new PaymentForm.OnSubmitForm() {
+ @Override
+ public void onSubmit(CardTokenisationRequest request) {
+ // Display a loader until the card is tokenised
+ progress = new ProgressDialog(CustomisationDemo.this);
+ progress.setTitle("Loading");
+ progress.setMessage("loading...");
+ progress.setCancelable(false);
+ progress.show();
+ mCheckoutAPIClient.generateToken(request); // send the request to generate the token
+ }
+ };
+
+ private final CheckoutAPIClient.OnTokenGenerated mTokenListener = new CheckoutAPIClient.OnTokenGenerated() {
+
+ @Override
+ public void onTokenGenerated(CardTokenisationResponse token) {
+ progress.dismiss();
+ displayMessage("Success!", token.getId());
+ }
+
+ @Override
+ public void onError(CardTokenisationFail error) {
+ progress.dismiss();
+ displayMessage("Error!", error.getEventId());
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.customisation_activity);
+
+ // initialise the payment from
+ mPaymentForm = findViewById(R.id.checkout_card_form);
+ mPaymentForm
+ .setSubmitListener(mSubmitListener) // set the callback for the form submission
+ .setAcceptedCard(new Cards[]{Cards.VISA, Cards.MASTERCARD})
+ .includeBilling(false);
+
+ // initialise the api client
+ mCheckoutAPIClient = new CheckoutAPIClient(
+ this, // context
+ "pk_test_6e40a700-d563-43cd-89d0-f9bb17d35e73", // your public key
+ Environment.SANDBOX
+ );
+ mCheckoutAPIClient.setTokenListener(mTokenListener); // set the callback for tokenisation
+ }
+
+ private void displayMessage(String title, String message) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(title)
+ .setMessage(message)
+ .setCancelable(false)
+ .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ //do things
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+}
\ No newline at end of file
diff --git a/demos/examples/app/src/main/java/checkout/com/demo/Demos/HeadlessDemo.java b/demos/examples/app/src/main/java/checkout/com/demo/Demos/HeadlessDemo.java
new file mode 100644
index 000000000..967530774
--- /dev/null
+++ b/demos/examples/app/src/main/java/checkout/com/demo/Demos/HeadlessDemo.java
@@ -0,0 +1,87 @@
+package checkout.com.demo.Demos;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+
+import com.checkout.android_sdk.CheckoutAPIClient;
+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.Response.CardTokenisationFail;
+import com.checkout.android_sdk.Response.CardTokenisationResponse;
+import com.checkout.android_sdk.Utils.Environment;
+
+import checkout.com.demo.R;
+
+public class HeadlessDemo extends Activity {
+
+ private CheckoutAPIClient mCheckoutAPIClient; // include the module
+
+ private final CheckoutAPIClient.OnTokenGenerated mTokenListener = new CheckoutAPIClient.OnTokenGenerated() {
+
+ @Override
+ public void onTokenGenerated(CardTokenisationResponse token) {
+ displayMessage("Success!", token.getId());
+ }
+
+ @Override
+ public void onError(CardTokenisationFail error) {
+ displayMessage("Error!", error.getEventId());
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.headless_activity);
+
+
+ mCheckoutAPIClient = new CheckoutAPIClient(
+ this,
+ "pk_test_6e40a700-d563-43cd-89d0-f9bb17d35e73",
+ Environment.SANDBOX
+ );
+ 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"
+ )
+ )
+ )
+ );
+
+ }
+
+ private void displayMessage(String title, String message) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(title)
+ .setMessage(message)
+ .setCancelable(false)
+ .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ //do things
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+}
diff --git a/demos/examples/app/src/main/java/checkout/com/demo/MainActivity.java b/demos/examples/app/src/main/java/checkout/com/demo/MainActivity.java
new file mode 100644
index 000000000..0af833ae4
--- /dev/null
+++ b/demos/examples/app/src/main/java/checkout/com/demo/MainActivity.java
@@ -0,0 +1,53 @@
+package checkout.com.demo;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+import checkout.com.demo.Demos.CustomFieldsDemo;
+import checkout.com.demo.Demos.CustomisationDemo;
+import checkout.com.demo.Demos.HeadlessDemo;
+
+public class MainActivity extends Activity {
+
+ private Button mCustomisationDemo;
+ private Button mCustomFieldsDemo;
+ private Button mHeadlessDemo;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mCustomisationDemo = findViewById(R.id.customisation_demo);
+ mCustomFieldsDemo = findViewById(R.id.custom_fields_demo);
+ mHeadlessDemo = findViewById(R.id.headless_demo);
+
+ mCustomisationDemo.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent myIntent = new Intent(MainActivity.this, CustomisationDemo.class);
+ MainActivity.this.startActivity(myIntent);
+ }
+ });
+
+ mCustomFieldsDemo.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent myIntent = new Intent(MainActivity.this, CustomFieldsDemo.class);
+ MainActivity.this.startActivity(myIntent);
+ }
+ });
+
+ mHeadlessDemo.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent myIntent = new Intent(MainActivity.this, HeadlessDemo.class);
+ MainActivity.this.startActivity(myIntent);
+ }
+ });
+
+ }
+}
diff --git a/demos/examples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demos/examples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..c7bd21dbd
--- /dev/null
+++ b/demos/examples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/examples/app/src/main/res/drawable/ic_launcher_background.xml b/demos/examples/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..d5fccc538
--- /dev/null
+++ b/demos/examples/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/examples/app/src/main/res/layout/activity_main.xml b/demos/examples/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..e37f5ec05
--- /dev/null
+++ b/demos/examples/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/res/layout/custom_fields_activity.xml b/demos/examples/app/src/main/res/layout/custom_fields_activity.xml
new file mode 100644
index 000000000..d26b96022
--- /dev/null
+++ b/demos/examples/app/src/main/res/layout/custom_fields_activity.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/res/layout/customisation_activity.xml b/demos/examples/app/src/main/res/layout/customisation_activity.xml
new file mode 100644
index 000000000..c742d219f
--- /dev/null
+++ b/demos/examples/app/src/main/res/layout/customisation_activity.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/res/layout/headless_activity.xml b/demos/examples/app/src/main/res/layout/headless_activity.xml
new file mode 100644
index 000000000..d8fb3247f
--- /dev/null
+++ b/demos/examples/app/src/main/res/layout/headless_activity.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/res/layout/three_d_activity.xml b/demos/examples/app/src/main/res/layout/three_d_activity.xml
new file mode 100644
index 000000000..a9c4bcbc2
--- /dev/null
+++ b/demos/examples/app/src/main/res/layout/three_d_activity.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demos/examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/demos/examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demos/examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/demos/examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/examples/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/examples/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a2f590828
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/examples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1b5239980
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/examples/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ff10afd6e
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/examples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..115a4c768
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/examples/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dcd3cd808
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/examples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..459ca609d
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/examples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8ca12fe02
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/examples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8e19b410a
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b824ebdd4
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4c19a13c2
Binary files /dev/null and b/demos/examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/demos/examples/app/src/main/res/values/colors.xml b/demos/examples/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..3ab3e9cbc
--- /dev/null
+++ b/demos/examples/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/demos/examples/app/src/main/res/values/strings.xml b/demos/examples/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..b2250a863
--- /dev/null
+++ b/demos/examples/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Demo
+
diff --git a/demos/examples/app/src/main/res/values/styles.xml b/demos/examples/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..b1a8a250b
--- /dev/null
+++ b/demos/examples/app/src/main/res/values/styles.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/examples/build.gradle b/demos/examples/build.gradle
new file mode 100644
index 000000000..b31566e0e
--- /dev/null
+++ b/demos/examples/build.gradle
@@ -0,0 +1,28 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.1'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/demos/examples/gradle.properties b/demos/examples/gradle.properties
new file mode 100644
index 000000000..743d692ce
--- /dev/null
+++ b/demos/examples/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/demos/examples/gradle/wrapper/gradle-wrapper.jar b/demos/examples/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..7a3265ee9
Binary files /dev/null and b/demos/examples/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/demos/examples/gradle/wrapper/gradle-wrapper.properties b/demos/examples/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..40f0e5253
--- /dev/null
+++ b/demos/examples/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Jun 25 16:45:00 BST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/demos/examples/gradlew b/demos/examples/gradlew
new file mode 100755
index 000000000..cccdd3d51
--- /dev/null
+++ b/demos/examples/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/demos/examples/gradlew.bat b/demos/examples/gradlew.bat
new file mode 100644
index 000000000..f9553162f
--- /dev/null
+++ b/demos/examples/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/demos/examples/settings.gradle b/demos/examples/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/demos/examples/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/demos/googlepay_example/.gitignore b/demos/googlepay_example/.gitignore
new file mode 100755
index 000000000..592b3b750
--- /dev/null
+++ b/demos/googlepay_example/.gitignore
@@ -0,0 +1,11 @@
+.gradle
+local.properties
+.idea
+.DS_Store
+build/
+*.aar
+*.iml
+*.jks
+*.apk
+
+*.pem
diff --git a/demos/googlepay_example/CONTRIBUTING.md b/demos/googlepay_example/CONTRIBUTING.md
new file mode 100755
index 000000000..ae319c70a
--- /dev/null
+++ b/demos/googlepay_example/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
diff --git a/demos/googlepay_example/LICENSE b/demos/googlepay_example/LICENSE
new file mode 100755
index 000000000..afdfe50e7
--- /dev/null
+++ b/demos/googlepay_example/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2017 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/demos/googlepay_example/README.md b/demos/googlepay_example/README.md
new file mode 100755
index 000000000..abdcea8dc
--- /dev/null
+++ b/demos/googlepay_example/README.md
@@ -0,0 +1,32 @@
+# Google Pay API sample app
+
+## Overview
+This sample demonstrates basic usage of the Google Pay API.
+
+The Google Pay API can be used to request any credit or debit card stored in
+your customer’s Google account, including their tokenized credentials from
+the Google Pay app.
+
+For more information, visit the following link:
+
+https://developers.google.com/pay/api/overview
+
+## Instructions
+
+Make sure you read the comments in Constants.java and PaymentsUtil.java before
+you continue. These files **must** be modified prior to running the app, as per
+the instructions provided in the comments.
+
+## Requirements
+
+In order to build and run this sample app, make sure you:
+
+- Have Android Studio 3.0 or newer installed.
+- Have a device running Android 4.4 (KitKat) or newer.
+- Have Google Play services version 11.4.0 or newer installed on this device.
+
+To be able to fully test the API, you will also need to:
+
+- [Add a payment method to your Google Account](https://support.google.com/payments/answer/6220309).
+- [Install and add a payment method to the Google Pay app](https://support.google.com/pay/answer/6289372) (optional).
+- Consult your payment processor's documentation to learn about whether they support paying with Google.
diff --git a/demos/googlepay_example/app/.gitignore b/demos/googlepay_example/app/.gitignore
new file mode 100755
index 000000000..796b96d1c
--- /dev/null
+++ b/demos/googlepay_example/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/demos/googlepay_example/app/build.gradle b/demos/googlepay_example/app/build.gradle
new file mode 100755
index 000000000..479166e3d
--- /dev/null
+++ b/demos/googlepay_example/app/build.gradle
@@ -0,0 +1,40 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 27
+ defaultConfig {
+ applicationId "com.google.android.gms.samples.wallet"
+
+ // Google Payment API is not supported in SDK versions lower than 19
+ minSdkVersion 19
+
+ targetSdkVersion 27
+ versionCode 1
+ versionName "1.0"
+ multiDexEnabled true
+ }
+ buildTypes {
+ release {
+ shrinkResources true
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation 'com.google.android.gms:play-services:11.8.0'
+ implementation 'com.android.support:multidex:1.0.3'
+ implementation 'com.android.support:appcompat-v7:27.1.1'
+ implementation 'com.android.support:mediarouter-v7:27.1.1'
+ 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'
+
+ // "wallet" is the only module needed for the Google Payment API and this sample app.
+ // Apps that selectively compile Google Play service APIs need only this dependency.
+ //implementation 'com.google.android.gms:play-services-wallet:11.8.0'
+}
diff --git a/demos/googlepay_example/app/src/main/AndroidManifest.xml b/demos/googlepay_example/app/src/main/AndroidManifest.xml
new file mode 100755
index 000000000..e75c2a6fd
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/CheckoutActivity.java b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/CheckoutActivity.java
new file mode 100755
index 000000000..0cc9b529e
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/CheckoutActivity.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.gms.samples.wallet;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.checkout.android_sdk.CheckoutAPIClient;
+import com.checkout.android_sdk.Response.GooglePayTokenisationFail;
+import com.checkout.android_sdk.Response.GooglePayTokenisationResponse;
+import com.checkout.android_sdk.Utils.Environment;
+import com.google.android.gms.common.api.ApiException;
+import com.google.android.gms.common.api.Status;
+import com.google.android.gms.tasks.OnCompleteListener;
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.wallet.AutoResolveHelper;
+import com.google.android.gms.wallet.PaymentData;
+import com.google.android.gms.wallet.PaymentDataRequest;
+import com.google.android.gms.wallet.PaymentMethodToken;
+import com.google.android.gms.wallet.PaymentsClient;
+import com.google.android.gms.wallet.TransactionInfo;
+
+import org.json.JSONException;
+
+public class CheckoutActivity extends Activity {
+
+ CheckoutAPIClient mCheckoutAPIClient;
+
+ CheckoutAPIClient.OnGooglePayTokenGenerated mGooglePayListener =
+ new CheckoutAPIClient.OnGooglePayTokenGenerated() {
+ @Override
+ public void onTokenGenerated(GooglePayTokenisationResponse response) {
+ displayMessage("Success!", response.getToken());
+ }
+
+ @Override
+ public void onError(GooglePayTokenisationFail error) {
+ displayMessage("Success!", error.getRequestId());
+ }
+ };
+
+ // Arbitrarily-picked result code.
+ private static final int LOAD_PAYMENT_DATA_REQUEST_CODE = 991;
+
+ private PaymentsClient mPaymentsClient;
+
+ private View mGooglePayButton;
+ private TextView mGooglePayStatusText;
+
+ private ItemInfo mBikeItem = new ItemInfo("Simple Bike", 300 * 1000000, R.drawable.bike);
+ private long mShippingCost = 90 * 1000000;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_checkout);
+
+ // Set up the mock information for our item in the UI.
+ initItemUI();
+
+ mGooglePayButton = findViewById(R.id.googlepay_button);
+ mGooglePayStatusText = findViewById(R.id.googlepay_status);
+
+ mGooglePayButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ requestPayment(view);
+ }
+ });
+
+ // It's recommended to create the PaymentsClient object inside of the onCreate method.
+ mPaymentsClient = PaymentsUtil.createPaymentsClient(this);
+ checkIsReadyToPay();
+ }
+
+ private void checkIsReadyToPay() {
+ // The call to isReadyToPay is asynchronous and returns a Task. We need to provide an
+ // OnCompleteListener to be triggered when the result of the call is known.
+ PaymentsUtil.isReadyToPay(mPaymentsClient).addOnCompleteListener(
+ new OnCompleteListener() {
+ public void onComplete(Task task) {
+ try {
+ boolean result = task.getResult(ApiException.class);
+ setGooglePayAvailable(result);
+ } catch (ApiException exception) {
+ // Process error
+ Log.w("isReadyToPay failed", exception);
+ }
+ }
+ });
+ }
+
+ private void setGooglePayAvailable(boolean available) {
+ // If isReadyToPay returned true, show the button and hide the "checking" text. Otherwise,
+ // notify the user that Pay with Google is not available.
+ // Please adjust to fit in with your current user flow. You are not required to explicitly
+ // let the user know if isReadyToPay returns false.
+ if (available) {
+ mGooglePayStatusText.setVisibility(View.GONE);
+ mGooglePayButton.setVisibility(View.VISIBLE);
+ } else {
+ mGooglePayStatusText.setText(R.string.googlepay_status_unavailable);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case LOAD_PAYMENT_DATA_REQUEST_CODE:
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ PaymentData paymentData = PaymentData.getFromIntent(data);
+ try {
+ handlePaymentSuccess(paymentData);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ break;
+ case Activity.RESULT_CANCELED:
+ // Nothing to here normally - the user simply cancelled without selecting a
+ // payment method.
+ break;
+ case AutoResolveHelper.RESULT_ERROR:
+ Status status = AutoResolveHelper.getStatusFromIntent(data);
+ handleError(status.getStatusCode());
+ break;
+ }
+
+ // Re-enables the Pay with Google button.
+ mGooglePayButton.setClickable(true);
+ break;
+ }
+ }
+
+ private void handlePaymentSuccess(PaymentData paymentData) throws JSONException {
+ // PaymentMethodToken contains the payment information, as well as any additional
+ // requested information, such as billing and shipping address.
+ //
+ // Refer to your processor's documentation on how to proceed from here.
+ PaymentMethodToken token = paymentData.getPaymentMethodToken();
+
+ // getPaymentMethodToken will only return null if PaymentMethodTokenizationParameters was
+ // not set in the PaymentRequest.
+ if (token != null) {
+ // If the gateway is set to example, no payment information is returned - instead, the
+ // token will only consist of "examplePaymentMethodToken".
+ mCheckoutAPIClient = new CheckoutAPIClient(
+ this, // activity context
+ "pk_test_6e40a700-d563-43cd-89d0-f9bb17d35e73", // your public key
+ Environment.SANDBOX // the environment
+ );
+ mCheckoutAPIClient.setGooglePayListener(mGooglePayListener); // pass the callback
+
+ mCheckoutAPIClient.generateGooglePayToken(token.getToken()); // the payload is the JSON string generated by GooglePay
+
+ // Use token.getToken() to get the token string.
+ Log.d("PaymentData", "PaymentMethodToken received");
+ }
+ }
+
+ private void handleError(int statusCode) {
+ // At this stage, the user has already seen a popup informing them an error occurred.
+ // Normally, only logging is required.
+ // statusCode will hold the value of any constant from CommonStatusCode or one of the
+ // WalletConstants.ERROR_CODE_* constants.
+ Log.w("loadPaymentData failed", String.format("Error code: %d", statusCode));
+ }
+
+ // This method is called when the Pay with Google button is clicked.
+ public void requestPayment(View view) {
+ // Disables the button to prevent multiple clicks.
+ mGooglePayButton.setClickable(false);
+
+ // The price provided to the API should include taxes and shipping.
+ // This price is not displayed to the user.
+ String price = PaymentsUtil.microsToString(mBikeItem.getPriceMicros() + mShippingCost);
+
+ TransactionInfo transaction = PaymentsUtil.createTransaction(price);
+ PaymentDataRequest request = PaymentsUtil.createPaymentDataRequest(transaction);
+ Task futurePaymentData = mPaymentsClient.loadPaymentData(request);
+
+ // Since loadPaymentData may show the UI asking the user to select a payment method, we use
+ // AutoResolveHelper to wait for the user interacting with it. Once completed,
+ // onActivityResult will be called with the result.
+ AutoResolveHelper.resolveTask(futurePaymentData, this, LOAD_PAYMENT_DATA_REQUEST_CODE);
+ }
+
+ private void initItemUI() {
+ TextView itemName = findViewById(R.id.text_item_name);
+ ImageView itemImage = findViewById(R.id.image_item_image);
+ TextView itemPrice = findViewById(R.id.text_item_price);
+
+ itemName.setText(mBikeItem.getName());
+ itemImage.setImageResource(mBikeItem.getImageResourceId());
+ itemPrice.setText(PaymentsUtil.microsToString(mBikeItem.getPriceMicros()));
+ }
+
+ private void displayMessage(String title, String message) {
+ android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(this);
+ builder.setTitle(title)
+ .setMessage(message)
+ .setCancelable(false)
+ .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ //do things
+ }
+ });
+ android.support.v7.app.AlertDialog alert = builder.create();
+ alert.show();
+ }
+}
diff --git a/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/Constants.java b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/Constants.java
new file mode 100755
index 000000000..43c3b74c7
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/Constants.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.gms.samples.wallet;
+
+import android.util.Pair;
+
+import com.google.android.gms.wallet.WalletConstants;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class Constants {
+ // This file contains several constants you must edit before proceeding.
+ //
+ // Please take a look at PaymentsUtil.java to see where the constants are used and to
+ // potentially remove ones not relevant to your integration.
+ // Required changes:
+ // 1. Update SUPPORTED_NETWORKS and SUPPORTED_METHODS if required (consult your processor if
+ // unsure).
+ // 2. Update CURRENCY_CODE to the currency you use.
+ // 3. Update SHIPPING_SUPPORTED_COUNTRIES to list the countries where you currently ship. If
+ // this is not applicable to your app, remove the relevant bits from PaymentsUtil.java.
+ // 4. If you're integrating with your processor / gateway directly, update
+ // GATEWAY_TOKENIZATION_NAME and GATEWAY_TOKENIZATION_PARAMETERS per the instructions they
+ // provided. You don't need to update DIRECT_TOKENIZATION_PUBLIC_KEY.
+ // 5. If you're using direct integration, please consult the documentation to learn about
+ // next steps.
+
+ // Changing this to ENVIRONMENT_PRODUCTION will make the API return real card information.
+ // Please refer to the documentation to read about the required steps needed to enable
+ // ENVIRONMENT_PRODUCTION.
+ public static final int PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST;
+
+ // The allowed networks to be requested from the API. If the user has cards from networks not
+ // specified here in their account, these will not be offered for them to choose in the popup.
+ public static final List SUPPORTED_NETWORKS = Arrays.asList(
+ WalletConstants.CARD_NETWORK_AMEX,
+ WalletConstants.CARD_NETWORK_DISCOVER,
+ WalletConstants.CARD_NETWORK_VISA,
+ WalletConstants.CARD_NETWORK_MASTERCARD
+ );
+
+ public static final List SUPPORTED_METHODS = Arrays.asList(
+ // PAYMENT_METHOD_CARD returns to any card the user has stored in their Google Account.
+ WalletConstants.PAYMENT_METHOD_CARD,
+
+ // PAYMENT_METHOD_TOKENIZED_CARD refers to EMV tokenized credentials stored in the
+ // Google Pay app, assuming it's installed.
+ // Please keep in mind tokenized cards may exist in the Google Pay app without being
+ // added to the user's Google Account.
+ WalletConstants.PAYMENT_METHOD_TOKENIZED_CARD
+ );
+
+ // Required by the API, but not visible to the user.
+ public static final String CURRENCY_CODE = "USD";
+
+ // Supported countries for shipping (use ISO 3166-1 alpha-2 country codes).
+ // Relevant only when requesting a shipping address.
+ public static final List SHIPPING_SUPPORTED_COUNTRIES = Arrays.asList(
+ "US",
+ "GB"
+ );
+
+ // The name of your payment processor / gateway. Please refer to their documentation for
+ // more information.
+ public static final String GATEWAY_TOKENIZATION_NAME = "checkoutltd";
+
+ // Custom parameters required by the processor / gateway.
+ // In many cases, your processor / gateway will only require a gatewayMerchantId.
+ // Please refer to your processor's documentation for more information. The number of parameters
+ // required and their names vary depending on the processor.
+ public static final List> GATEWAY_TOKENIZATION_PARAMETERS = Arrays.asList(
+ Pair.create("gatewayMerchantId", "pk_test_6e40a700-d563-43cd-89d0-f9bb17d35e73")
+
+ // Your processor may require additional parameters.
+ );
+
+ // Only used for DIRECT tokenization. Can be removed when using GATEWAY tokenization.
+ public static final String DIRECT_TOKENIZATION_PUBLIC_KEY = "REPLACE_ME";
+
+ private Constants() {
+ }
+
+}
diff --git a/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/ItemInfo.java b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/ItemInfo.java
new file mode 100755
index 000000000..59d92488c
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/ItemInfo.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.gms.samples.wallet;
+
+/**
+ * Used for storing the (hard coded) info about the item we're selling.
+ *
+ * This POJO class is used only for example purposes - you don't need need it in your code.
+ */
+public class ItemInfo {
+ private final String name;
+ private final int imageResourceId;
+
+ // Micros are used for prices to avoid rounding errors when converting between currencies.
+ private final long priceMicros;
+
+ public ItemInfo(String name, long price, int imageResourceId) {
+ this.name = name;
+ this.priceMicros = price;
+ this.imageResourceId = imageResourceId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getImageResourceId() {
+ return imageResourceId;
+ }
+
+ public long getPriceMicros() {
+ return priceMicros;
+ }
+}
diff --git a/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/PaymentsUtil.java b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/PaymentsUtil.java
new file mode 100755
index 000000000..863140285
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/java/com/google/android/gms/samples/wallet/PaymentsUtil.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.gms.samples.wallet;
+
+import android.app.Activity;
+import android.util.Pair;
+
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.wallet.CardRequirements;
+import com.google.android.gms.wallet.IsReadyToPayRequest;
+import com.google.android.gms.wallet.PaymentDataRequest;
+import com.google.android.gms.wallet.PaymentMethodTokenizationParameters;
+import com.google.android.gms.wallet.PaymentsClient;
+import com.google.android.gms.wallet.ShippingAddressRequirements;
+import com.google.android.gms.wallet.TransactionInfo;
+import com.google.android.gms.wallet.Wallet;
+import com.google.android.gms.wallet.WalletConstants;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * Contains helper static methods for dealing with the Payments API.
+ *
+ * Many of the parameters used in the code are optional and are set here merely to call out their
+ * existence. Please consult the documentation to learn more and feel free to remove ones not
+ * relevant to your implementation.
+ */
+public class PaymentsUtil {
+ private static final BigDecimal MICROS = new BigDecimal(1000000d);
+
+ private PaymentsUtil() {
+ }
+
+ /**
+ * Creates an instance of {@link PaymentsClient} for use in an {@link Activity} using the
+ * environment and theme set in {@link Constants}.
+ *
+ * @param activity is the caller's activity.
+ */
+ public static PaymentsClient createPaymentsClient(Activity activity) {
+ Wallet.WalletOptions walletOptions = new Wallet.WalletOptions.Builder()
+ .setEnvironment(Constants.PAYMENTS_ENVIRONMENT)
+ .build();
+ return Wallet.getPaymentsClient(activity, walletOptions);
+ }
+
+ /**
+ * Builds {@link PaymentDataRequest} to be consumed by {@link PaymentsClient#loadPaymentData}.
+ *
+ * @param transactionInfo contains the price for this transaction.
+ */
+ public static PaymentDataRequest createPaymentDataRequest(TransactionInfo transactionInfo) {
+ PaymentMethodTokenizationParameters.Builder paramsBuilder =
+ PaymentMethodTokenizationParameters.newBuilder()
+ .setPaymentMethodTokenizationType(
+ WalletConstants.PAYMENT_METHOD_TOKENIZATION_TYPE_PAYMENT_GATEWAY)
+ .addParameter("gateway", Constants.GATEWAY_TOKENIZATION_NAME);
+ for (Pair param : Constants.GATEWAY_TOKENIZATION_PARAMETERS) {
+ paramsBuilder.addParameter(param.first, param.second);
+ }
+
+ return createPaymentDataRequest(transactionInfo, paramsBuilder.build());
+ }
+
+ /**
+ * Builds {@link PaymentDataRequest} for use with DIRECT integration to be consumed by
+ * {@link PaymentsClient#loadPaymentData}.
+ *
+ * Please refer to the documentation for more information about DIRECT integration. The type of
+ * integration you use depends on your payment processor.
+ *
+ * @param transactionInfo contains the price for this transaction.
+ */
+ public static PaymentDataRequest createPaymentDataRequestDirect(TransactionInfo transactionInfo) {
+ PaymentMethodTokenizationParameters params =
+ PaymentMethodTokenizationParameters.newBuilder()
+ .setPaymentMethodTokenizationType(
+ WalletConstants.PAYMENT_METHOD_TOKENIZATION_TYPE_DIRECT)
+
+ // Omitting the publicKey will result in a request for unencrypted data.
+ // Please refer to the documentation for more information on unencrypted
+ // requests.
+ .addParameter("publicKey", Constants.DIRECT_TOKENIZATION_PUBLIC_KEY)
+ .build();
+
+ return createPaymentDataRequest(transactionInfo, params);
+ }
+
+ private static PaymentDataRequest createPaymentDataRequest(TransactionInfo transactionInfo, PaymentMethodTokenizationParameters params) {
+ PaymentDataRequest request =
+ PaymentDataRequest.newBuilder()
+ .setPhoneNumberRequired(false)
+ .setEmailRequired(true)
+ .setShippingAddressRequired(true)
+
+ // Omitting ShippingAddressRequirements all together means all countries are
+ // supported.
+ .setShippingAddressRequirements(
+ ShippingAddressRequirements.newBuilder()
+ .addAllowedCountryCodes(Constants.SHIPPING_SUPPORTED_COUNTRIES)
+ .build())
+
+ .setTransactionInfo(transactionInfo)
+ .addAllowedPaymentMethods(Constants.SUPPORTED_METHODS)
+ .setCardRequirements(
+ CardRequirements.newBuilder()
+ .addAllowedCardNetworks(Constants.SUPPORTED_NETWORKS)
+ .setAllowPrepaidCards(true)
+ .setBillingAddressRequired(true)
+
+ // Omitting this parameter will result in the API returning
+ // only a "minimal" billing address (post code only).
+ .setBillingAddressFormat(WalletConstants.BILLING_ADDRESS_FORMAT_FULL)
+ .build())
+ .setPaymentMethodTokenizationParameters(params)
+
+ // If the UI is not required, a returning user will not be asked to select
+ // a card. Instead, the card they previously used will be returned
+ // automatically (if still available).
+ // Prior whitelisting is required to use this feature.
+ .setUiRequired(true)
+ .build();
+
+ return request;
+ }
+
+ /**
+ * Determines if the user is eligible to Pay with Google by calling
+ * {@link PaymentsClient#isReadyToPay}. The nature of this check depends on the methods set in
+ * {@link Constants#SUPPORTED_METHODS}.
+ *
+ * If {@link WalletConstants#PAYMENT_METHOD_CARD} is specified among supported methods, this
+ * function will return true even if the user has no cards stored. Please refer to the
+ * documentation for more information on how the check is performed.
+ *
+ * @param client used to send the request.
+ */
+ public static Task isReadyToPay(PaymentsClient client) {
+ IsReadyToPayRequest.Builder request = IsReadyToPayRequest.newBuilder();
+ for (Integer allowedMethod : Constants.SUPPORTED_METHODS) {
+ request.addAllowedPaymentMethod(allowedMethod);
+ }
+ return client.isReadyToPay(request.build());
+ }
+
+ /**
+ * Builds {@link TransactionInfo} for use with {@link PaymentsUtil#createPaymentDataRequest}.
+ *
+ * The price is not displayed to the user and must be in the following format: "12.34".
+ * {@link PaymentsUtil#microsToString} can be used to format the string.
+ *
+ * @param price total of the transaction.
+ */
+ public static TransactionInfo createTransaction(String price) {
+ return TransactionInfo.newBuilder()
+ .setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL)
+ .setTotalPrice(price)
+ .setCurrencyCode(Constants.CURRENCY_CODE)
+ .build();
+ }
+
+ /**
+ * Converts micros to a string format accepted by {@link PaymentsUtil#createTransaction}.
+ *
+ * @param micros value of the price.
+ */
+ public static String microsToString(long micros) {
+ return new BigDecimal(micros).divide(MICROS).setScale(2, RoundingMode.HALF_EVEN).toString();
+ }
+}
diff --git a/demos/googlepay_example/app/src/main/res/drawable-de/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-de/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..e86ee3daf
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-de/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-es/add_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-es/add_to_googlepay_button_content.xml
new file mode 100755
index 000000000..4a323744e
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-es/add_to_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-es/buy_with_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-es/buy_with_googlepay_button_content.xml
new file mode 100755
index 000000000..a278a035d
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-es/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-es/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-es/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..b19ba4f73
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-es/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-fr/add_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-fr/add_to_googlepay_button_content.xml
new file mode 100755
index 000000000..01aab494e
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-fr/add_to_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml
new file mode 100755
index 000000000..fcdae02f7
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-fr/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-fr/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-fr/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..5ffa3dc50
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-fr/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image.9.png
new file mode 100755
index 000000000..1d6c0e373
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image_focused.9.png
new file mode 100755
index 000000000..342f248e8
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image_pressed.9.png
new file mode 100755
index 000000000..337c9325c
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 000000000..affa0d38c
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image_focused.9.png
new file mode 100755
index 000000000..b82ca103a
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image_pressed.9.png
new file mode 100755
index 000000000..35a44a23c
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-hdpi/googlepay_button_no_shadow_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-ja/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-ja/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..c7f5f829a
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-ja/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-ko/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-ko/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..42ddceea3
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-ko/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image.9.png
new file mode 100755
index 000000000..98ece61c9
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image_focused.9.png
new file mode 100755
index 000000000..4b0729484
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image_pressed.9.png
new file mode 100755
index 000000000..4964cdc1b
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 000000000..5d2ca4a44
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image_focused.9.png
new file mode 100755
index 000000000..c98d387b1
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image_pressed.9.png
new file mode 100755
index 000000000..340d4494a
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-mdpi/googlepay_button_no_shadow_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-nl/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-nl/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..f7cfe21a9
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-nl/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-pl/add_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-pl/add_to_googlepay_button_content.xml
new file mode 100755
index 000000000..76e22ab5c
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-pl/add_to_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-pl/buy_with_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-pl/buy_with_googlepay_button_content.xml
new file mode 100755
index 000000000..ab3652af0
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-pl/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-ru/add_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-ru/add_to_googlepay_button_content.xml
new file mode 100755
index 000000000..6cf2aca25
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-ru/add_to_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml
new file mode 100755
index 000000000..4ec261bb8
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-ru/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-v21/googlepay_button_background.xml b/demos/googlepay_example/app/src/main/res/drawable-v21/googlepay_button_background.xml
new file mode 100755
index 000000000..0dac0a93a
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-v21/googlepay_button_background.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-v21/googlepay_button_no_shadow_background.xml b/demos/googlepay_example/app/src/main/res/drawable-v21/googlepay_button_no_shadow_background.xml
new file mode 100755
index 000000000..89404a563
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-v21/googlepay_button_no_shadow_background.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png
new file mode 100755
index 000000000..2424f9686
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image_focused.9.png
new file mode 100755
index 000000000..9f1fb5748
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image_pressed.9.png
new file mode 100755
index 000000000..59161bae9
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 000000000..95482f4f5
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image_focused.9.png
new file mode 100755
index 000000000..210493a58
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image_pressed.9.png
new file mode 100755
index 000000000..e246559b7
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png
new file mode 100755
index 000000000..a6a1b1876
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image_focused.9.png
new file mode 100755
index 000000000..e766ccb29
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image_pressed.9.png
new file mode 100755
index 000000000..e544a4f38
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 000000000..69b834009
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image_focused.9.png
new file mode 100755
index 000000000..9b71cf6cf
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image_pressed.9.png
new file mode 100755
index 000000000..454df9b85
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxhdpi/googlepay_button_no_shadow_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png
new file mode 100755
index 000000000..7c781080c
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image_focused.9.png
new file mode 100755
index 000000000..b9223ea3a
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image_pressed.9.png
new file mode 100755
index 000000000..894fdcfbc
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png
new file mode 100755
index 000000000..41dfb8372
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image_focused.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image_focused.9.png
new file mode 100755
index 000000000..7889152ae
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image_focused.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image_pressed.9.png b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image_pressed.9.png
new file mode 100755
index 000000000..c461f63ef
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable-xxxhdpi/googlepay_button_no_shadow_background_image_pressed.9.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable-zh/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable-zh/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..a5710671f
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable-zh/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable/add_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable/add_to_googlepay_button_content.xml
new file mode 100755
index 000000000..87ff0894a
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable/add_to_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable/bike.png b/demos/googlepay_example/app/src/main/res/drawable/bike.png
new file mode 100755
index 000000000..c5a23c6fd
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/drawable/bike.png differ
diff --git a/demos/googlepay_example/app/src/main/res/drawable/buy_with_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable/buy_with_googlepay_button_content.xml
new file mode 100755
index 000000000..e2d6d39f4
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable/buy_with_googlepay_button_content.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_background.xml b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_background.xml
new file mode 100755
index 000000000..894a0bbe1
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_background.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_content.xml
new file mode 100755
index 000000000..57596ba5f
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_content.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml
new file mode 100755
index 000000000..5d564c072
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_overlay.xml b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_overlay.xml
new file mode 100755
index 000000000..a422e5c4d
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable/googlepay_button_overlay.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/drawable/save_to_googlepay_button_content.xml b/demos/googlepay_example/app/src/main/res/drawable/save_to_googlepay_button_content.xml
new file mode 100755
index 000000000..2ff230e0c
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/drawable/save_to_googlepay_button_content.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/activity_checkout.xml b/demos/googlepay_example/app/src/main/res/layout/activity_checkout.xml
new file mode 100755
index 000000000..2545f5f1e
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/activity_checkout.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/googlepay_example/app/src/main/res/layout/add_to_googlepay_button.xml b/demos/googlepay_example/app/src/main/res/layout/add_to_googlepay_button.xml
new file mode 100755
index 000000000..39d8dc9d7
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/add_to_googlepay_button.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/add_to_googlepay_button_no_shadow.xml b/demos/googlepay_example/app/src/main/res/layout/add_to_googlepay_button_no_shadow.xml
new file mode 100755
index 000000000..e2ccb8b71
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/add_to_googlepay_button_no_shadow.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/buy_with_googlepay_button.xml b/demos/googlepay_example/app/src/main/res/layout/buy_with_googlepay_button.xml
new file mode 100755
index 000000000..b109e6ab7
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/buy_with_googlepay_button.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/buy_with_googlepay_button_no_shadow.xml b/demos/googlepay_example/app/src/main/res/layout/buy_with_googlepay_button_no_shadow.xml
new file mode 100755
index 000000000..22d113462
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/buy_with_googlepay_button_no_shadow.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/googlepay_button.xml b/demos/googlepay_example/app/src/main/res/layout/googlepay_button.xml
new file mode 100755
index 000000000..a778f3806
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/googlepay_button.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/googlepay_button_no_shadow.xml b/demos/googlepay_example/app/src/main/res/layout/googlepay_button_no_shadow.xml
new file mode 100755
index 000000000..e3af39c87
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/googlepay_button_no_shadow.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/save_to_googlepay_button.xml b/demos/googlepay_example/app/src/main/res/layout/save_to_googlepay_button.xml
new file mode 100755
index 000000000..f3ea53855
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/save_to_googlepay_button.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/layout/save_to_googlepay_button_no_shadow.xml b/demos/googlepay_example/app/src/main/res/layout/save_to_googlepay_button_no_shadow.xml
new file mode 100755
index 000000000..c399095fe
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/layout/save_to_googlepay_button_no_shadow.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/googlepay_example/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100755
index 000000000..cde69bccc
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/googlepay_example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100755
index 000000000..9a078e3e1
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/googlepay_example/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100755
index 000000000..c133a0cbd
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/googlepay_example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100755
index 000000000..efc028a63
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/googlepay_example/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100755
index 000000000..bfa42f0e7
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/googlepay_example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100755
index 000000000..3af2608a4
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/googlepay_example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100755
index 000000000..324e72cdd
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/googlepay_example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100755
index 000000000..9bec2e623
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/googlepay_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100755
index 000000000..aee44e138
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/googlepay_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/googlepay_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100755
index 000000000..34947cd6b
Binary files /dev/null and b/demos/googlepay_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/demos/googlepay_example/app/src/main/res/values/dimens.xml b/demos/googlepay_example/app/src/main/res/values/dimens.xml
new file mode 100755
index 000000000..4f3e4820f
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/values/dimens.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 5dp
+ 10dp
+ 30dp
+
+ 300dp
+ 48sp
+ 200dp
+
+
diff --git a/demos/googlepay_example/app/src/main/res/values/googlepay_strings.xml b/demos/googlepay_example/app/src/main/res/values/googlepay_strings.xml
new file mode 100755
index 000000000..569bb669f
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/values/googlepay_strings.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ Google Pay
+ Buy with Google Pay
+ Save to Google Pay
+ Add to Google Pay
+
diff --git a/demos/googlepay_example/app/src/main/res/values/strings.xml b/demos/googlepay_example/app/src/main/res/values/strings.xml
new file mode 100755
index 000000000..29f80a1d3
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ My Online Store App
+ My Online Store
+ Unfortunately, Google Pay is not available on this phone.
+ Checking if Google Pay is available...
+ Successfully received payment data for %s!
+
diff --git a/demos/googlepay_example/app/src/main/res/values/styles.xml b/demos/googlepay_example/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..5885930df
--- /dev/null
+++ b/demos/googlepay_example/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/demos/googlepay_example/build.gradle b/demos/googlepay_example/build.gradle
new file mode 100755
index 000000000..cec66a5d6
--- /dev/null
+++ b/demos/googlepay_example/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/demos/googlepay_example/gradle.properties b/demos/googlepay_example/gradle.properties
new file mode 100755
index 000000000..aac7c9b46
--- /dev/null
+++ b/demos/googlepay_example/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/demos/googlepay_example/gradle/wrapper/gradle-wrapper.jar b/demos/googlepay_example/gradle/wrapper/gradle-wrapper.jar
new file mode 100755
index 000000000..13372aef5
Binary files /dev/null and b/demos/googlepay_example/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/demos/googlepay_example/gradle/wrapper/gradle-wrapper.properties b/demos/googlepay_example/gradle/wrapper/gradle-wrapper.properties
new file mode 100755
index 000000000..4bc4ec262
--- /dev/null
+++ b/demos/googlepay_example/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Oct 26 16:08:18 BST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/demos/googlepay_example/gradlew b/demos/googlepay_example/gradlew
new file mode 100755
index 000000000..9d82f7891
--- /dev/null
+++ b/demos/googlepay_example/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/demos/googlepay_example/gradlew.bat b/demos/googlepay_example/gradlew.bat
new file mode 100755
index 000000000..8a0b282aa
--- /dev/null
+++ b/demos/googlepay_example/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/demos/googlepay_example/settings.gradle b/demos/googlepay_example/settings.gradle
new file mode 100755
index 000000000..e7b4def49
--- /dev/null
+++ b/demos/googlepay_example/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/demos/saved_cards_example/.gitignore b/demos/saved_cards_example/.gitignore
new file mode 100644
index 000000000..5edb4eeb0
--- /dev/null
+++ b/demos/saved_cards_example/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/demos/saved_cards_example/.idea/caches/build_file_checksums.ser b/demos/saved_cards_example/.idea/caches/build_file_checksums.ser
new file mode 100644
index 000000000..8290ff3f7
Binary files /dev/null and b/demos/saved_cards_example/.idea/caches/build_file_checksums.ser differ
diff --git a/demos/saved_cards_example/.idea/codeStyles/Project.xml b/demos/saved_cards_example/.idea/codeStyles/Project.xml
new file mode 100644
index 000000000..30aa626c2
--- /dev/null
+++ b/demos/saved_cards_example/.idea/codeStyles/Project.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/.idea/gradle.xml b/demos/saved_cards_example/.idea/gradle.xml
new file mode 100644
index 000000000..7ac24c777
--- /dev/null
+++ b/demos/saved_cards_example/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/.idea/misc.xml b/demos/saved_cards_example/.idea/misc.xml
new file mode 100644
index 000000000..99202cc2d
--- /dev/null
+++ b/demos/saved_cards_example/.idea/misc.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/.idea/runConfigurations.xml b/demos/saved_cards_example/.idea/runConfigurations.xml
new file mode 100644
index 000000000..7f68460d8
--- /dev/null
+++ b/demos/saved_cards_example/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/.gitignore b/demos/saved_cards_example/app/.gitignore
new file mode 100644
index 000000000..796b96d1c
--- /dev/null
+++ b/demos/saved_cards_example/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/demos/saved_cards_example/app/build.gradle b/demos/saved_cards_example/app/build.gradle
new file mode 100644
index 000000000..fbf583773
--- /dev/null
+++ b/demos/saved_cards_example/app/build.gradle
@@ -0,0 +1,32 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 27
+ defaultConfig {
+ applicationId "checkout.com.saved_cards_example"
+ 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(dir: 'libs', include: ['*.jar'])
+ implementation 'com.android.support:appcompat-v7:27.1.1'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.2'
+ 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'
+ implementation 'com.github.ioan-ghisoi-cko:frames-android:v0.1.4'
+}
diff --git a/demos/saved_cards_example/app/proguard-rules.pro b/demos/saved_cards_example/app/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/demos/saved_cards_example/app/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/demos/saved_cards_example/app/src/androidTest/java/checkout/com/saved_cards_example/ExampleInstrumentedTest.java b/demos/saved_cards_example/app/src/androidTest/java/checkout/com/saved_cards_example/ExampleInstrumentedTest.java
new file mode 100644
index 000000000..de80af41e
--- /dev/null
+++ b/demos/saved_cards_example/app/src/androidTest/java/checkout/com/saved_cards_example/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package checkout.com.saved_cards_example;
+
+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("checkout.com.saved_cards_example", appContext.getPackageName());
+ }
+}
diff --git a/demos/saved_cards_example/app/src/main/AndroidManifest.xml b/demos/saved_cards_example/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..213fe4955
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/AddCardActivity.java b/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/AddCardActivity.java
new file mode 100644
index 000000000..273a39c82
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/AddCardActivity.java
@@ -0,0 +1,115 @@
+package checkout.com.saved_cards_example;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+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.PaymentForm;
+import com.checkout.android_sdk.Request.CardTokenisationRequest;
+import com.checkout.android_sdk.Response.CardTokenisationFail;
+import com.checkout.android_sdk.Response.CardTokenisationResponse;
+import com.checkout.android_sdk.Utils.CardUtils;
+import com.checkout.android_sdk.Utils.Environment;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class AddCardActivity extends Activity {
+
+ // This is the URL of your backend endpoint that will perform the Zero Dollar Transaction request
+ private final String YOUR_API_ZERO_DOLLAR_URL = "https://frames-android-backend-example.herokuapp.com/zerodollar";
+ private final String PUBLIC_KEY = "pk_test_6e40a700-d563-43cd-89d0-f9bb17d35e73";
+
+
+ private PaymentForm mPaymentForm; // include the payment form
+ private CheckoutAPIClient mCheckoutAPIClient; // include the API client
+
+ // The callback used to communicate when the form was submitted
+ PaymentForm.OnSubmitForm mSubmitListener = new PaymentForm.OnSubmitForm() {
+ @Override
+ public void onSubmit(CardTokenisationRequest request) {
+ mCheckoutAPIClient.generateToken(request); // send the request to generate the token
+ }
+ };
+
+ // The callback used to communicate when the card token was generated
+ CheckoutAPIClient.OnTokenGenerated mTokenListener = new CheckoutAPIClient.OnTokenGenerated() {
+ @Override
+ public void onTokenGenerated(CardTokenisationResponse token) {
+ try {
+ callApiToSaveCard(token.getId());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ @Override
+ public void onError(CardTokenisationFail error) {
+ // your error
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.add_card_activity);
+
+ // initialise the payment from
+ mPaymentForm = findViewById(R.id.checkout_card_form);
+ mPaymentForm
+ .setAcceptedCard(new CardUtils.Cards[]{CardUtils.Cards.VISA, CardUtils.Cards.MASTERCARD})
+ .setSubmitListener(mSubmitListener); // set the callback for the form submission
+
+ // initialise the api client
+ mCheckoutAPIClient = new CheckoutAPIClient(
+ this, // context
+ PUBLIC_KEY, // your public key
+ Environment.SANDBOX
+ );
+ mCheckoutAPIClient.setTokenListener(mTokenListener); // set the callback for tokenisation
+ }
+
+ /**
+ * This function will call a backend server that will perform a Zero Dollar Transaction that
+ * will create the customer if it is not already created and it will associate the specific
+ * card with that customer.
+ *
+ * After the API call is completed the user will be redirected to the the Main Activity.
+ *
+ * @param token the card token generated
+ */
+ private void callApiToSaveCard(String token) throws JSONException {
+ RequestQueue queue = Volley.newRequestQueue(this);
+
+ JSONObject json = new JSONObject();
+ json.put("token", token);
+
+ JsonObjectRequest portRequest = new JsonObjectRequest(Request.Method.POST, YOUR_API_ZERO_DOLLAR_URL, json,
+ new Response.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ // redirect to the main activity
+ Intent myIntent = new Intent(AddCardActivity.this, MainActivity.class);
+ AddCardActivity.this.startActivity(myIntent);
+ }
+ },
+ new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ NetworkResponse networkResponse = error.networkResponse;
+ if (networkResponse != null && networkResponse.data != null) {
+ // handle error
+ }
+ }
+ }
+ );
+ queue.add(portRequest);
+ }
+}
diff --git a/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/CustomRadioButton.java b/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/CustomRadioButton.java
new file mode 100644
index 000000000..2e16e757b
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/CustomRadioButton.java
@@ -0,0 +1,69 @@
+package checkout.com.saved_cards_example;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import com.checkout.android_sdk.Utils.CardUtils;
+
+/**
+ * This class is used to extend the RadioButton element so it can hold information
+ * about the card id and display an appropriate card icon.
+ */
+public class CustomRadioButton extends android.support.v7.widget.AppCompatRadioButton {
+
+ private String mType;
+ private String mCardId;
+ private Context mContext;
+
+ public CustomRadioButton(Context context, String type, String cardId) {
+ super(context);
+ this.mType = type;
+ this.mCardId = cardId;
+ this.mContext = context;
+ iniView();
+ }
+
+ public String getCardId() {
+ return mCardId;
+ }
+
+ private void iniView() {
+ setIconForType(mType);
+ // Add some margin
+ setWidth(getResources().getDisplayMetrics().widthPixels-15);
+ }
+
+
+ /**
+ * This function will determine the type of card and it will add a card icon to the entry
+ * corresponding with the type
+ *
+ * @param type the type of card
+ */
+ private void setIconForType(String type) {
+ Drawable img = null;
+ switch (type.toLowerCase()) {
+ case "visa":
+ img = mContext.getResources().getDrawable(CardUtils.Cards.VISA.resourceId);
+ break;
+ case "mastercard":
+ img = mContext.getResources().getDrawable(CardUtils.Cards.MASTERCARD.resourceId);
+ break;
+ case "amex":
+ img = mContext.getResources().getDrawable(CardUtils.Cards.AMEX.resourceId);
+ break;
+ case "dinersclub":
+ img = mContext.getResources().getDrawable(CardUtils.Cards.DINERSCLUB.resourceId);
+ break;
+ case "discover":
+ img = mContext.getResources().getDrawable(CardUtils.Cards.DISCOVER.resourceId);
+ break;
+ case "jcb":
+ img = mContext.getResources().getDrawable(CardUtils.Cards.JCB.resourceId);
+ break;
+ }
+ img.setBounds(0, 0, 68, 68);
+ setCompoundDrawables(null, null, img, null);
+ setCompoundDrawablePadding(5);
+ }
+}
diff --git a/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/MainActivity.java b/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/MainActivity.java
new file mode 100644
index 000000000..432455125
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/java/checkout/com/saved_cards_example/MainActivity.java
@@ -0,0 +1,161 @@
+package checkout.com.saved_cards_example;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.Button;
+import android.widget.RadioGroup;
+
+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 org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class MainActivity extends Activity {
+
+ // This is the URL of your backend endpoint that will perform the Get CardList request
+ private final String GET_CARDS_URL = "https://frames-android-backend-example.herokuapp.com/cardlist";
+
+ // Default value of the Alert, if the the user has not selected any card.
+ private String selectedItem = "No Selection!";
+ private Button mPay, mAdd;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ RadioGroup group = findViewById(R.id.radio_group);
+
+ mPay = findViewById(R.id.pay_button);
+ mAdd = findViewById(R.id.add_button);
+
+ // We populate the RadioGroup with the card linked to the customer
+ callApiAndPopulateList(group);
+
+ // When the used selects a card we store the card id
+ group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(RadioGroup radioGroup, int i) {
+ CustomRadioButton element = radioGroup.findViewById(i);
+ selectedItem = element.getCardId();
+ }
+ });
+
+ // When the user clicks the pay button, we display the card id linked to the
+ // selected card. At this step you can send this card id to your server and
+ // perform a charge.
+ mPay.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // For the purpose of the demo, we are only displaying the card id.
+ displayMessage("Card", selectedItem);
+ }
+ });
+
+ // When the user clicks the add button, we redirect to the Payment Form.
+ mAdd.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent myIntent = new Intent(MainActivity.this, AddCardActivity.class);
+ MainActivity.this.startActivity(myIntent);
+ }
+ });
+ }
+
+ /**
+ * This function will add an entry in a RadioGroup that will contain the name of the card,
+ * the last 4 digits and the icon of the specific card type.
+ *
+ * @param group the RadioGroup where the entry will be added
+ * @param type the type of card
+ * @param last4 the last 4 digits of the card
+ * @param cardId the card id
+ */
+ private void addCardToList(RadioGroup group, String type, String last4, String cardId) {
+ RadioGroup.LayoutParams rprms;
+ CustomRadioButton radioButton = new CustomRadioButton(
+ this,
+ type,
+ cardId
+ );
+ radioButton.setText(type + " ending " + last4);
+ rprms = new RadioGroup.LayoutParams(RadioGroup.LayoutParams.WRAP_CONTENT, RadioGroup.LayoutParams.WRAP_CONTENT);
+ group.addView(radioButton, rprms);
+ }
+
+ /**
+ * This function will call a backend server that will perform a "GetCardList request" to
+ * checkout.com, and it will return a list of cards.
+ *
+ * @param group the RadioGroup where the entry will be added
+ */
+ private void callApiAndPopulateList(final RadioGroup group) {
+ RequestQueue queue = Volley.newRequestQueue(this);
+
+ JsonObjectRequest getRequest = new JsonObjectRequest(Request.Method.GET, GET_CARDS_URL, null,
+ new Response.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ try {
+ JSONArray array = (JSONArray) response.get("cardList");
+
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject jsonobject = array.getJSONObject(i);
+ addCardToList(
+ group,
+ jsonobject.getString("paymentMethod"),
+ jsonobject.getString("last4"),
+ jsonobject.getString("cardId")
+ );
+ String name = jsonobject.getString("paymentMethod");
+ }
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ },
+ new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ NetworkResponse networkResponse = error.networkResponse;
+ if (networkResponse != null && networkResponse.data != null) {
+ // handle error
+ }
+ }
+ }
+ );
+ queue.add(getRequest);
+ }
+
+ /**
+ * This function will display an AlertDialog with the desired title and message.
+ *
+ * @param title the title of the alert
+ * @param message the message of the alert
+ */
+ private void displayMessage(String title, String message) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(title)
+ .setMessage(message)
+ .setCancelable(false)
+ .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ //do things
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+}
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demos/saved_cards_example/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..c7bd21dbd
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/saved_cards_example/app/src/main/res/drawable/ic_launcher_background.xml b/demos/saved_cards_example/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..d5fccc538
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/saved_cards_example/app/src/main/res/layout/activity_main.xml b/demos/saved_cards_example/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..941415dc7
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/src/main/res/layout/add_card_activity.xml b/demos/saved_cards_example/app/src/main/res/layout/add_card_activity.xml
new file mode 100644
index 000000000..d92a892e3
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/layout/add_card_activity.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demos/saved_cards_example/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demos/saved_cards_example/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..eca70cfe5
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/saved_cards_example/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a2f590828
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/demos/saved_cards_example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..1b5239980
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/saved_cards_example/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ff10afd6e
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/saved_cards_example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..115a4c768
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/saved_cards_example/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..dcd3cd808
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/saved_cards_example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..459ca609d
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/saved_cards_example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8ca12fe02
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/saved_cards_example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..8e19b410a
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/saved_cards_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b824ebdd4
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/demos/saved_cards_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..4c19a13c2
Binary files /dev/null and b/demos/saved_cards_example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/demos/saved_cards_example/app/src/main/res/values/colors.xml b/demos/saved_cards_example/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..f7f1fee72
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+ #fff
+ #000
+ #fff
+ #000
+
diff --git a/demos/saved_cards_example/app/src/main/res/values/dimens.xml b/demos/saved_cards_example/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..4559d41dd
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/values/dimens.xml
@@ -0,0 +1,13 @@
+
+
+
+ 18sp
+ 20dp
+ 10dp
+ 40dp
+ 60dp
+ 90dp
+ 40dp
+ 10dp
+
+
\ No newline at end of file
diff --git a/demos/saved_cards_example/app/src/main/res/values/strings.xml b/demos/saved_cards_example/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..3f9fb4d02
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+ saved_cards_example
+ Pay with a saved card
+ Pay
+ Add Card
+ ending
+
diff --git a/demos/saved_cards_example/app/src/main/res/values/styles.xml b/demos/saved_cards_example/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..5b0bcc827
--- /dev/null
+++ b/demos/saved_cards_example/app/src/main/res/values/styles.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/demos/saved_cards_example/app/src/test/java/checkout/com/saved_cards_example/ExampleUnitTest.java b/demos/saved_cards_example/app/src/test/java/checkout/com/saved_cards_example/ExampleUnitTest.java
new file mode 100644
index 000000000..075c5a1f8
--- /dev/null
+++ b/demos/saved_cards_example/app/src/test/java/checkout/com/saved_cards_example/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package checkout.com.saved_cards_example;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/demos/saved_cards_example/build.gradle b/demos/saved_cards_example/build.gradle
new file mode 100644
index 000000000..b31566e0e
--- /dev/null
+++ b/demos/saved_cards_example/build.gradle
@@ -0,0 +1,28 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.1'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ maven { url 'https://jitpack.io' }
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/demos/saved_cards_example/gradle.properties b/demos/saved_cards_example/gradle.properties
new file mode 100644
index 000000000..743d692ce
--- /dev/null
+++ b/demos/saved_cards_example/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/demos/saved_cards_example/gradle/wrapper/gradle-wrapper.jar b/demos/saved_cards_example/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..7a3265ee9
Binary files /dev/null and b/demos/saved_cards_example/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/demos/saved_cards_example/gradle/wrapper/gradle-wrapper.properties b/demos/saved_cards_example/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..7e60c30c4
--- /dev/null
+++ b/demos/saved_cards_example/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 29 09:49:41 BST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/demos/saved_cards_example/gradlew b/demos/saved_cards_example/gradlew
new file mode 100755
index 000000000..cccdd3d51
--- /dev/null
+++ b/demos/saved_cards_example/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/demos/saved_cards_example/gradlew.bat b/demos/saved_cards_example/gradlew.bat
new file mode 100644
index 000000000..f9553162f
--- /dev/null
+++ b/demos/saved_cards_example/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/demos/saved_cards_example/settings.gradle b/demos/saved_cards_example/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/demos/saved_cards_example/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/docs/allclasses-frame.html b/docs/allclasses-frame.html
new file mode 100644
index 000000000..ab859ffeb
--- /dev/null
+++ b/docs/allclasses-frame.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+All Classes
+
+
+
+
+
+
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (CheckoutAPIClient.card c : CheckoutAPIClient.card.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (CheckoutAPIClient.googlepay c : CheckoutAPIClient.googlepay.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
+ It takes a 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
+ 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
+
+
Parameters:
+
request - Custom request body to be used in the HTTP call.
public void generateGooglePayToken(java.lang.String payload)
+ throws org.json.JSONException
+
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())
public class CardInput
+extends android.support.v7.widget.AppCompatEditText
+
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 CardUtils details.
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (MonthInput.Months c : MonthInput.Months.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
public class PaymentForm
+extends android.widget.FrameLayout
+
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
+ 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 static enum CardUtils.Cards
+extends java.lang.Enum<CardUtils.Cards>
+
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.
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (CardUtils.Cards c : CardUtils.Cards.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
public static CardUtils.Cards valueOf(java.lang.String name)
+
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
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.
public static CardUtils.Cards getType(java.lang.String number)
+
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.
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (Environment c : Environment.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
public static Environment valueOf(java.lang.String name)
+
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
+ 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
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
public static interface BillingDetailsView.Listener
+
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 class BillingDetailsView
+extends android.widget.LinearLayout
+
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 static interface CardDetailsView.DetailsCompleted
+
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 class CardDetailsView
+extends android.widget.LinearLayout
+
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.
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
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (CheckoutKit.card c : CheckoutKit.card.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (CheckoutKit.googlepay c : CheckoutKit.googlepay.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
public class CheckoutKit
+extends android.widget.FrameLayout
+
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
+ It takes a CardTokenisationRequest as the argumnet 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
+ CheckoutKit.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
+
+
Parameters:
+
request - Custom request body to be used in the HTTP call.
public void generateGooglePayToken(org.json.JSONObject googlePayToken)
+ throws org.json.JSONException
+
This method used to create a token that can be used in a server environment to create a
+ charge. It take a GooglePay payload in JSONObject. The payload is usually generated in
+ the handlePaymentSuccess method shown in the Google Pay example from Google (token.getToken())
public class CardInput
+extends android.support.v7.widget.AppCompatEditText
+
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 CardUtils details.
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (MonthInput.Months c : MonthInput.Months.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
+ 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 static enum CardUtils.Cards
+extends java.lang.Enum<CardUtils.Cards>
+
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.
Returns an array containing the constants of this enum type, in
+the order they are declared. This method may be used to iterate
+over the constants as follows:
+
+for (CardUtils.Cards c : CardUtils.Cards.values())
+ System.out.println(c);
+
+
+
Returns:
+
an array containing the constants of this enum type, in the order they are declared
public static CardUtils.Cards valueOf(java.lang.String name)
+
Returns the enum constant of this type with the specified name.
+The string must match exactly an identifier used to declare an
+enum constant in this type. (Extraneous whitespace characters are
+not permitted.)
+
+
Parameters:
+
name - the name of the enum constant to be returned.
+
Returns:
+
the enum constant with the specified name
+
Throws:
+
java.lang.IllegalArgumentException - if this enum type has no constant with the specified name
+
java.lang.NullPointerException - if the argument is null
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.
public static CardUtils.Cards getType(java.lang.String number)
+
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.
+ 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
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
public static interface BillingDetailsView.Listener
+
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 class BillingDetailsView
+extends android.widget.LinearLayout
+
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 static interface CardDetailsView.DetailsCompleted
+
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 class CardDetailsView
+extends android.widget.LinearLayout
+
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.
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
This API (Application Programming Interface) document has pages corresponding to the items in the navigation bar, described as follows.
+
+
+
+
+
Overview
+
The Overview page is the front page of this API document and provides a list of all packages with a summary for each. This page can also contain an overall description of the set of packages.
+
+
+
Package
+
Each package has a page that contains a list of its classes and interfaces, with a summary for each. This page can contain six categories:
+
+
Interfaces (italic)
+
Classes
+
Enums
+
Exceptions
+
Errors
+
Annotation Types
+
+
+
+
Class/Interface
+
Each class, interface, nested class and nested interface has its own separate page. Each of these pages has three sections consisting of a class/interface description, summary tables, and detailed member descriptions:
+
+
Class inheritance diagram
+
Direct Subclasses
+
All Known Subinterfaces
+
All Known Implementing Classes
+
Class/interface declaration
+
Class/interface description
+
+
+
Nested Class Summary
+
Field Summary
+
Constructor Summary
+
Method Summary
+
+
+
Field Detail
+
Constructor Detail
+
Method Detail
+
+
Each summary entry contains the first sentence from the detailed description for that item. The summary entries are alphabetical, while the detailed descriptions are in the order they appear in the source code. This preserves the logical groupings established by the programmer.
+
+
+
Annotation Type
+
Each annotation type has its own separate page with the following sections:
+
+
Annotation Type declaration
+
Annotation Type description
+
Required Element Summary
+
Optional Element Summary
+
Element Detail
+
+
+
+
Enum
+
Each enum has its own separate page with the following sections:
+
+
Enum declaration
+
Enum description
+
Enum Constant Summary
+
Enum Constant Detail
+
+
+
+
Tree (Class Hierarchy)
+
There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. The classes are organized by inheritance structure starting with java.lang.Object. The interfaces do not inherit from java.lang.Object.
+
+
When viewing the Overview page, clicking on "Tree" displays the hierarchy for all packages.
+
When viewing a particular package, class or interface page, clicking "Tree" displays the hierarchy for only that package.
+
+
+
+
Deprecated API
+
The Deprecated API page lists all of the API that have been deprecated. A deprecated API is not recommended for use, generally due to improvements, and a replacement API is usually given. Deprecated APIs may be removed in future implementations.
+
+
+
Index
+
The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.
+
+
+
Prev/Next
+
These links take you to the next or previous class, interface, package, or related page.
+
+
+
Frames/No Frames
+
These links show and hide the HTML frames. All pages are available with or without frames.
+
+
+
All Classes
+
The All Classes link shows all classes and interfaces except non-static nested types.
+
+
+
Serialized Form
+
Each serializable or externalizable class has a description of its serialization fields and methods. This information is of interest to re-implementors, not to developers using the API. While there is no link in the navigation bar, you can get to this information by going to any serialized class and clicking "Serialized Form" in the "See also" section of the class description.
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.