diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..96cc43e
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..e7bedf3
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..dc178bd
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/android_volley_1_0_0.xml b/.idea/libraries/android_volley_1_0_0.xml
new file mode 100644
index 0000000..33ad956
--- /dev/null
+++ b/.idea/libraries/android_volley_1_0_0.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/animated_vector_drawable_25_3_1.xml b/.idea/libraries/animated_vector_drawable_25_3_1.xml
new file mode 100644
index 0000000..c0811a6
--- /dev/null
+++ b/.idea/libraries/animated_vector_drawable_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/appcompat_v7_25_3_1.xml b/.idea/libraries/appcompat_v7_25_3_1.xml
new file mode 100644
index 0000000..3a0494c
--- /dev/null
+++ b/.idea/libraries/appcompat_v7_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/constraint_layout_1_0_1.xml b/.idea/libraries/constraint_layout_1_0_1.xml
new file mode 100644
index 0000000..a5a4da8
--- /dev/null
+++ b/.idea/libraries/constraint_layout_1_0_1.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/constraint_layout_solver_1_0_1.xml b/.idea/libraries/constraint_layout_solver_1_0_1.xml
new file mode 100644
index 0000000..496104c
--- /dev/null
+++ b/.idea/libraries/constraint_layout_solver_1_0_1.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/espresso_core_2_2_2.xml b/.idea/libraries/espresso_core_2_2_2.xml
new file mode 100644
index 0000000..86c335d
--- /dev/null
+++ b/.idea/libraries/espresso_core_2_2_2.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/espresso_idling_resource_2_2_2.xml b/.idea/libraries/espresso_idling_resource_2_2_2.xml
new file mode 100644
index 0000000..0dc7dc3
--- /dev/null
+++ b/.idea/libraries/espresso_idling_resource_2_2_2.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/exposed_instrumentation_api_publish_0_5.xml b/.idea/libraries/exposed_instrumentation_api_publish_0_5.xml
new file mode 100644
index 0000000..aed587c
--- /dev/null
+++ b/.idea/libraries/exposed_instrumentation_api_publish_0_5.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/gson_2_8_0.xml b/.idea/libraries/gson_2_8_0.xml
new file mode 100644
index 0000000..0e2abf0
--- /dev/null
+++ b/.idea/libraries/gson_2_8_0.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/hamcrest_core_1_3.xml b/.idea/libraries/hamcrest_core_1_3.xml
new file mode 100644
index 0000000..157e3f3
--- /dev/null
+++ b/.idea/libraries/hamcrest_core_1_3.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/hamcrest_integration_1_3.xml b/.idea/libraries/hamcrest_integration_1_3.xml
new file mode 100644
index 0000000..58b2c4b
--- /dev/null
+++ b/.idea/libraries/hamcrest_integration_1_3.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/hamcrest_library_1_3.xml b/.idea/libraries/hamcrest_library_1_3.xml
new file mode 100644
index 0000000..676cc63
--- /dev/null
+++ b/.idea/libraries/hamcrest_library_1_3.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/javawriter_2_1_1.xml b/.idea/libraries/javawriter_2_1_1.xml
new file mode 100644
index 0000000..a66fefb
--- /dev/null
+++ b/.idea/libraries/javawriter_2_1_1.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/javax_annotation_api_1_2.xml b/.idea/libraries/javax_annotation_api_1_2.xml
new file mode 100644
index 0000000..811e73f
--- /dev/null
+++ b/.idea/libraries/javax_annotation_api_1_2.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/javax_inject_1.xml b/.idea/libraries/javax_inject_1.xml
new file mode 100644
index 0000000..0d1d5fc
--- /dev/null
+++ b/.idea/libraries/javax_inject_1.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/jsr305_2_0_1.xml b/.idea/libraries/jsr305_2_0_1.xml
new file mode 100644
index 0000000..cdf9878
--- /dev/null
+++ b/.idea/libraries/jsr305_2_0_1.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/junit_4_12.xml b/.idea/libraries/junit_4_12.xml
new file mode 100644
index 0000000..305df30
--- /dev/null
+++ b/.idea/libraries/junit_4_12.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/rules_0_5.xml b/.idea/libraries/rules_0_5.xml
new file mode 100644
index 0000000..1d51e0a
--- /dev/null
+++ b/.idea/libraries/rules_0_5.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/runner_0_5.xml b/.idea/libraries/runner_0_5.xml
new file mode 100644
index 0000000..db91766
--- /dev/null
+++ b/.idea/libraries/runner_0_5.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_annotations_25_3_1.xml b/.idea/libraries/support_annotations_25_3_1.xml
new file mode 100644
index 0000000..42adfc6
--- /dev/null
+++ b/.idea/libraries/support_annotations_25_3_1.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_compat_25_3_1.xml b/.idea/libraries/support_compat_25_3_1.xml
new file mode 100644
index 0000000..aee35a5
--- /dev/null
+++ b/.idea/libraries/support_compat_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_core_ui_25_3_1.xml b/.idea/libraries/support_core_ui_25_3_1.xml
new file mode 100644
index 0000000..b80d99d
--- /dev/null
+++ b/.idea/libraries/support_core_ui_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_core_utils_25_3_1.xml b/.idea/libraries/support_core_utils_25_3_1.xml
new file mode 100644
index 0000000..2dd66dd
--- /dev/null
+++ b/.idea/libraries/support_core_utils_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_fragment_25_3_1.xml b/.idea/libraries/support_fragment_25_3_1.xml
new file mode 100644
index 0000000..a4fffd6
--- /dev/null
+++ b/.idea/libraries/support_fragment_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_media_compat_25_3_1.xml b/.idea/libraries/support_media_compat_25_3_1.xml
new file mode 100644
index 0000000..d61962d
--- /dev/null
+++ b/.idea/libraries/support_media_compat_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_v4_25_3_1.xml b/.idea/libraries/support_v4_25_3_1.xml
new file mode 100644
index 0000000..3028232
--- /dev/null
+++ b/.idea/libraries/support_v4_25_3_1.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/support_vector_drawable_25_3_1.xml b/.idea/libraries/support_vector_drawable_25_3_1.xml
new file mode 100644
index 0000000..e2e8d1f
--- /dev/null
+++ b/.idea/libraries/support_vector_drawable_25_3_1.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..9fc28d4
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..549ad90
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /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 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..76588ae
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion "25.0.2"
+ defaultConfig {
+ applicationId "ch.itomy.xendit_example"
+ minSdkVersion 9
+ targetSdkVersion 25
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ compile 'com.android.support:appcompat-v7:25.3.1'
+ compile 'com.android.support.constraint:constraint-layout:1.0.1'
+ testCompile 'junit:junit:4.12'
+ compile project(':xendit-android')
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..c59202b
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/SergeyOmelyanovskiy/AndroidStudio/android-sdk-macosx/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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/app/src/androidTest/java/ch/itomy/xendit_example/ExampleInstrumentedTest.java b/app/src/androidTest/java/ch/itomy/xendit_example/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..38b648f
--- /dev/null
+++ b/app/src/androidTest/java/ch/itomy/xendit_example/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package ch.itomy.xendit_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.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("ch.itomy.xendit_example", appContext.getPackageName());
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..af8c091
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/ch/itomy/xendit_example/AuthenticationActivity.java b/app/src/main/java/ch/itomy/xendit_example/AuthenticationActivity.java
new file mode 100644
index 0000000..a7a54dd
--- /dev/null
+++ b/app/src/main/java/ch/itomy/xendit_example/AuthenticationActivity.java
@@ -0,0 +1,88 @@
+package ch.itomy.xendit_example;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.xendit.Models.Card;
+import com.xendit.Models.Token;
+import com.xendit.Models.XenditError;
+import com.xendit.TokenCallback;
+import com.xendit.Xendit;
+
+/**
+ * Created by Sergey on 4/3/17.
+ */
+
+public class AuthenticationActivity extends AppCompatActivity implements View.OnClickListener {
+
+ private EditText tokenIdEditText;
+ private EditText amountEditText;
+ private EditText cardCvnEditText;
+ private Button authenticateBtn;
+
+ public static Intent getLaunchIntent(Context context) {
+ return new Intent(context, AuthenticationActivity.class);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_authentication);
+
+ setActionBarTitle("Authentication");
+
+ tokenIdEditText = (EditText) findViewById(R.id.tokenIdEditText_AuthenticationActivity);
+ amountEditText = (EditText) findViewById(R.id.amountEditText_AuthenticationActivity);
+ cardCvnEditText = (EditText) findViewById(R.id.cardCvnEditText_AuthenticationActivity);
+ authenticateBtn = (Button) findViewById(R.id.authenticateBtn_AuthenticationActivity);
+
+ authenticateBtn.setOnClickListener(this);
+ }
+
+ private void setActionBarTitle(String title) {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(title);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ Card card = new Card("4000000000000002", "12", "2017", cardCvnEditText.getText().toString());
+
+ Xendit xendit = new Xendit(getApplicationContext(), CreateTokenActivity.PUBLISH_KEY);
+ xendit.createAuthentication(card, tokenIdEditText.getText().toString(), amountEditText.getText().toString(), new TokenCallback() {
+ @Override
+ public void onSuccess(Token token) {
+ Toast.makeText(AuthenticationActivity.this, "Status: " + token.getAuthentication().getStatus(), Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onError(XenditError xenditError) {
+ Toast.makeText(AuthenticationActivity.this, xenditError.getError(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ this.finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ch/itomy/xendit_example/CreateTokenActivity.java b/app/src/main/java/ch/itomy/xendit_example/CreateTokenActivity.java
new file mode 100644
index 0000000..1dc93e4
--- /dev/null
+++ b/app/src/main/java/ch/itomy/xendit_example/CreateTokenActivity.java
@@ -0,0 +1,103 @@
+package ch.itomy.xendit_example;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.xendit.Models.Card;
+import com.xendit.Models.Token;
+import com.xendit.Models.XenditError;
+import com.xendit.TokenCallback;
+import com.xendit.Xendit;
+
+/**
+ * Created by Sergey on 4/3/17.
+ */
+
+public class CreateTokenActivity extends AppCompatActivity implements View.OnClickListener {
+
+ public static final String PUBLISH_KEY = "xnd_public_development_O4iFfuQhgLOsl8M9eeEYGzeWYNH3otV5w3Dh/BFj/mHW+72nCQR/";
+
+ private EditText cardNumberEditText;
+ private EditText expMonthEditText;
+ private EditText expYearEditText;
+ private EditText cvnEditText;
+ private EditText amountEditText;
+ private Button createTokenBtn;
+
+ public static Intent getLaunchIntent(Context context) {
+ return new Intent(context, CreateTokenActivity.class);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_create_token);
+
+ setActionBarTitle("Create Token");
+
+ cardNumberEditText = (EditText) findViewById(R.id.cardNumberEditText_CreateTokenActivity);
+ expMonthEditText = (EditText) findViewById(R.id.expMonthEditText_CreateTokenActivity);
+ expYearEditText = (EditText) findViewById(R.id.expYearEditText_CreateTokenActivity);
+ cvnEditText = (EditText) findViewById(R.id.cvnEditText_CreateTokenActivity);
+ amountEditText = (EditText) findViewById(R.id.amountEditText_CreateTokenActivity);
+ createTokenBtn = (Button) findViewById(R.id.createTokenBtn_CreateTokenActivity);
+
+ createTokenBtn.setOnClickListener(this);
+
+ cardNumberEditText.setText("4000000000000002");
+ expMonthEditText.setText("12");
+ expYearEditText.setText("2017");
+ cvnEditText.setText("123");
+ amountEditText.setText("222");
+ }
+
+ private void setActionBarTitle(String title) {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(title);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ Card card = new Card(cardNumberEditText.getText().toString(),
+ expMonthEditText.getText().toString(),
+ expYearEditText.getText().toString(),
+ cvnEditText.getText().toString());
+
+ Xendit xendit = new Xendit(getApplicationContext(), PUBLISH_KEY);
+ xendit.createToken(card, amountEditText.getText().toString(), new TokenCallback() {
+ @Override
+ public void onSuccess(Token token) {
+ Toast.makeText(CreateTokenActivity.this, "Status: " + token.getAuthentication().getStatus(), Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onError(XenditError xenditError) {
+ Toast.makeText(CreateTokenActivity.this, xenditError.getError(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ this.finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ch/itomy/xendit_example/MainActivity.java b/app/src/main/java/ch/itomy/xendit_example/MainActivity.java
new file mode 100644
index 0000000..d808e16
--- /dev/null
+++ b/app/src/main/java/ch/itomy/xendit_example/MainActivity.java
@@ -0,0 +1,52 @@
+package ch.itomy.xendit_example;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.TextView;
+
+public class MainActivity extends AppCompatActivity implements View.OnClickListener {
+
+ private TextView createTokenTextView;
+ private TextView authenticationTextView;
+ private TextView validationUtilTextView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ setActionBarTitle("Xendit");
+
+ createTokenTextView = (TextView) findViewById(R.id.createTokenTextView_MainActivity);
+ authenticationTextView = (TextView) findViewById(R.id.authenticationTextView_MainActivity);
+ validationUtilTextView = (TextView) findViewById(R.id.validationUtilTextView_MainActivity);
+
+ createTokenTextView.setOnClickListener(this);
+ authenticationTextView.setOnClickListener(this);
+ validationUtilTextView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.createTokenTextView_MainActivity:
+ startActivity(CreateTokenActivity.getLaunchIntent(this));
+ break;
+ case R.id.authenticationTextView_MainActivity:
+ startActivity(AuthenticationActivity.getLaunchIntent(this));
+ break;
+ case R.id.validationUtilTextView_MainActivity:
+ startActivity(ValidationUtilActivity.getLaunchIntent(this));
+ break;
+ }
+ }
+
+ private void setActionBarTitle(String title) {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(title);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ch/itomy/xendit_example/ValidationUtilActivity.java b/app/src/main/java/ch/itomy/xendit_example/ValidationUtilActivity.java
new file mode 100644
index 0000000..5b70409
--- /dev/null
+++ b/app/src/main/java/ch/itomy/xendit_example/ValidationUtilActivity.java
@@ -0,0 +1,85 @@
+package ch.itomy.xendit_example;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.xendit.Xendit;
+
+/**
+ * Created by Sergey on 4/3/17.
+ */
+
+public class ValidationUtilActivity extends AppCompatActivity implements View.OnClickListener {
+
+ private EditText cardNumberEditText;
+ private EditText expMonthEditText;
+ private EditText expYearEditText;
+ private EditText cvnEditText;
+ private Button validateBtn;
+
+ public static Intent getLaunchIntent(Context context) {
+ return new Intent(context, ValidationUtilActivity.class);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_validation_util);
+
+ setActionBarTitle("Validation Util");
+
+ cardNumberEditText = (EditText) findViewById(R.id.cardNumberEditText_ValidationUtilActivity);
+ expMonthEditText = (EditText) findViewById(R.id.expMonthEditText_ValidationUtilActivity);
+ expYearEditText = (EditText) findViewById(R.id.expYearEditText_ValidationUtilActivity);
+ cvnEditText = (EditText) findViewById(R.id.cvnEditText_ValidationUtilActivity);
+ validateBtn = (Button) findViewById(R.id.validateBtn_ValidationUtilActivity);
+
+ validateBtn.setOnClickListener(this);
+ }
+
+ private void setActionBarTitle(String title) {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(title);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+
+
+ if (!Xendit.isCardNumberValid(cardNumberEditText.getText().toString())) {
+ Toast.makeText(this, "Card number is invalid", Toast.LENGTH_SHORT).show();
+ } else if (!Xendit.isExpiryValid(expMonthEditText.getText().toString(), expYearEditText.getText().toString())) {
+ Toast.makeText(this, "Card expiration date is invalid", Toast.LENGTH_SHORT).show();
+ } else if (!Xendit.isCvnValid(cvnEditText.getText().toString())) {
+ Toast.makeText(this, "Card cvn is invalid", Toast.LENGTH_SHORT).show();
+ } else if (Xendit.isCardNumberValid(cardNumberEditText.getText().toString())
+ && Xendit.isExpiryValid(expMonthEditText.getText().toString(), expYearEditText.getText().toString())
+ && Xendit.isCvnValid(cvnEditText.getText().toString())) {
+ Toast.makeText(this, "Card is valid", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ this.finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_authentication.xml b/app/src/main/res/layout/activity_authentication.xml
new file mode 100644
index 0000000..5192f2d
--- /dev/null
+++ b/app/src/main/res/layout/activity_authentication.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_create_token.xml b/app/src/main/res/layout/activity_create_token.xml
new file mode 100644
index 0000000..b494b2c
--- /dev/null
+++ b/app/src/main/res/layout/activity_create_token.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..45f69fb
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_validation_util.xml b/app/src/main/res/layout/activity_validation_util.xml
new file mode 100644
index 0000000..c0f2241
--- /dev/null
+++ b/app/src/main/res/layout/activity_validation_util.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9a078e3
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..efc028a
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..3af2608
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9bec2e6
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..34947cd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..b15dd42
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ xendit_example
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..daa2a5c
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/app/src/test/java/ch/itomy/xendit_example/ExampleUnitTest.java b/app/src/test/java/ch/itomy/xendit_example/ExampleUnitTest.java
new file mode 100644
index 0000000..31e9dc5
--- /dev/null
+++ b/app/src/test/java/ch/itomy/xendit_example/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package ch.itomy.xendit_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() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..b78a0b8
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.3.1'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..5acd198
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Mar 07 15:08:50 EET 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/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/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/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/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..6baca91
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':xendit-android'
diff --git a/xendit-android/.gitignore b/xendit-android/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/xendit-android/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/xendit-android/build.gradle b/xendit-android/build.gradle
new file mode 100644
index 0000000..c70b026
--- /dev/null
+++ b/xendit-android/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion "25.0.2"
+ defaultConfig {
+ minSdkVersion 9
+ targetSdkVersion 25
+ versionCode 1
+ versionName "1.0"
+
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ productFlavors {
+ }
+}
+
+dependencies {
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ // testCompile 'junit:junit:4.12'
+}
\ No newline at end of file
diff --git a/xendit-android/libs/android-volley-1.0.0.jar b/xendit-android/libs/android-volley-1.0.0.jar
new file mode 100644
index 0000000..f52a789
Binary files /dev/null and b/xendit-android/libs/android-volley-1.0.0.jar differ
diff --git a/xendit-android/libs/gson-2.8.0.jar b/xendit-android/libs/gson-2.8.0.jar
new file mode 100644
index 0000000..1235f63
Binary files /dev/null and b/xendit-android/libs/gson-2.8.0.jar differ
diff --git a/xendit-android/proguard-rules.pro b/xendit-android/proguard-rules.pro
new file mode 100644
index 0000000..4e2bd87
--- /dev/null
+++ b/xendit-android/proguard-rules.pro
@@ -0,0 +1,29 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/Dimon_GDA/Prog/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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
+
+-keep public class com.xendit.** { public *;}
+
+-keepattributes LocalVariableTable,LocalVariableTypeTable
\ No newline at end of file
diff --git a/xendit-android/src/androidTest/java/com/xendit/ExampleInstrumentedTest.java b/xendit-android/src/androidTest/java/com/xendit/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..7d2ec0d
--- /dev/null
+++ b/xendit-android/src/androidTest/java/com/xendit/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.xendit;
+
+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.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.xendit.test", appContext.getPackageName());
+ }
+}
diff --git a/xendit-android/src/main/AndroidManifest.xml b/xendit-android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0dd5700
--- /dev/null
+++ b/xendit-android/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/Models/Authentication.java b/xendit-android/src/main/java/com/xendit/Models/Authentication.java
new file mode 100644
index 0000000..fd910ec
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/Models/Authentication.java
@@ -0,0 +1,64 @@
+package com.xendit.Models;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Created by Sergey on 3/22/17.
+ */
+
+public class Authentication implements Parcelable {
+
+ @SerializedName("id")
+ private String id;
+
+ @SerializedName("status")
+ private String status;
+
+ @SerializedName("payer_authentication_url")
+ private String payerAuthenticationUrl;
+
+ private Authentication(Parcel in) {
+ id = in.readString();
+ status = in.readString();
+ payerAuthenticationUrl = in.readString();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public Authentication createFromParcel(Parcel in) {
+ return new Authentication(in);
+ }
+
+ @Override
+ public Authentication[] newArray(int size) {
+ return new Authentication[size];
+ }
+ };
+
+ public String getId() {
+ return id;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public String getPayerAuthenticationUrl() {
+ return payerAuthenticationUrl;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeString(id);
+ parcel.writeString(status);
+ parcel.writeString(payerAuthenticationUrl);
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/Models/Card.java b/xendit-android/src/main/java/com/xendit/Models/Card.java
new file mode 100644
index 0000000..76865a9
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/Models/Card.java
@@ -0,0 +1,37 @@
+package com.xendit.Models;
+
+/**
+ * Created by Dimon_GDA on 3/7/17.
+ */
+
+public class Card {
+
+ private String creditCardNumber;
+ private String cardExpirationMonth;
+ private String cardExpirationYear;
+ private String creditCardCVN;
+
+
+ public Card(String creditCardNumber, String cardExpirationMonth, String cardExpirationYear, String creditCardCVN) {
+ this.creditCardNumber = creditCardNumber;
+ this.cardExpirationMonth = cardExpirationMonth;
+ this.cardExpirationYear = cardExpirationYear;
+ this.creditCardCVN = creditCardCVN;
+ }
+
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ public String getCardExpirationMonth() {
+ return cardExpirationMonth;
+ }
+
+ public String getCardExpirationYear() {
+ return cardExpirationYear;
+ }
+
+ public String getCreditCardCVN() {
+ return creditCardCVN;
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/Models/Token.java b/xendit-android/src/main/java/com/xendit/Models/Token.java
new file mode 100644
index 0000000..f5fb3a9
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/Models/Token.java
@@ -0,0 +1,18 @@
+package com.xendit.Models;
+
+/**
+ * Created by Sergey on 3/16/17.
+ */
+
+public class Token {
+
+ private Authentication authentication;
+
+ public Token(Authentication authentication) {
+ this.authentication = authentication;
+ }
+
+ public Authentication getAuthentication() {
+ return authentication;
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/Models/TokenCredentials.java b/xendit-android/src/main/java/com/xendit/Models/TokenCredentials.java
new file mode 100644
index 0000000..94c4e66
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/Models/TokenCredentials.java
@@ -0,0 +1,24 @@
+package com.xendit.Models;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Created by Sergey on 3/22/17.
+ */
+
+public class TokenCredentials {
+
+ @SerializedName("tokenization_auth_key_id")
+ private String tokenizationAuthKeyId;
+
+ @SerializedName("flex_api_key")
+ private String flexApiKey;
+
+ public String getTokenizationAuthKeyId() {
+ return tokenizationAuthKeyId;
+ }
+
+ public String getFlexApiKey() {
+ return flexApiKey;
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/Models/TokenCreditCard.java b/xendit-android/src/main/java/com/xendit/Models/TokenCreditCard.java
new file mode 100644
index 0000000..438de59
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/Models/TokenCreditCard.java
@@ -0,0 +1,59 @@
+package com.xendit.Models;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Created by Sergey on 3/22/17.
+ */
+
+public class TokenCreditCard {
+
+ @SerializedName("keyId")
+ private String keyId;
+
+ @SerializedName("token")
+ private String token;
+
+ @SerializedName("maskedPan")
+ private String maskedPan;
+
+ @SerializedName("cardType")
+ private String cardType;
+
+ @SerializedName("timestamp")
+ private long timestamp;
+
+ @SerializedName("signedFields")
+ private String signedFields;
+
+ @SerializedName("signature")
+ private String signature;
+
+ public String getKeyId() {
+ return keyId;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public String getMaskedPan() {
+ return maskedPan;
+ }
+
+ public String getCardType() {
+ return cardType;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getSignedFields() {
+ return signedFields;
+ }
+
+ public String getSignature() {
+ return signature;
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/Models/XenditError.java b/xendit-android/src/main/java/com/xendit/Models/XenditError.java
new file mode 100644
index 0000000..2fa470e
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/Models/XenditError.java
@@ -0,0 +1,38 @@
+package com.xendit.Models;
+
+/**
+ * Created by Sergey on 3/16/17.
+ */
+
+public class XenditError {
+
+ private String error;
+ private String networkError;
+ private Authentication authentication;
+
+ public XenditError(String error) {
+ this.error = error;
+ }
+
+ public XenditError(String error, Authentication authentication) {
+ this(error);
+ this.authentication = authentication;
+ }
+
+ public XenditError(String error, String networkError) {
+ this(error);
+ this.networkError = networkError;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public String getNetworkError() {
+ return networkError;
+ }
+
+ public Authentication getAuthentication() {
+ return authentication;
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/TokenBroadcastReceiver.java b/xendit-android/src/main/java/com/xendit/TokenBroadcastReceiver.java
new file mode 100644
index 0000000..a5f1794
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/TokenBroadcastReceiver.java
@@ -0,0 +1,41 @@
+package com.xendit;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.google.gson.Gson;
+import com.xendit.Models.Authentication;
+import com.xendit.Models.Token;
+import com.xendit.Models.XenditError;
+
+/**
+ * Created by Sergey on 3/30/17.
+ */
+
+public class TokenBroadcastReceiver extends BroadcastReceiver {
+
+ private TokenCallback tokenCallback;
+
+ public TokenBroadcastReceiver(TokenCallback tokenCallback) {
+ this.tokenCallback = tokenCallback;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String message = intent.getExtras().getString(XenditActivity.MESSAGE_KEY);
+ if (message != null && message.equals(context.getString(R.string.create_token_error_validation))) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_validation)));
+ } else {
+ Gson gson = new Gson();
+ Authentication authentication = gson.fromJson(message, Authentication.class);
+ if (authentication.getStatus().equals("VERIFIED")) {
+ tokenCallback.onSuccess(new Token(authentication));
+ } else {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_validation), authentication));
+ }
+ }
+
+ context.unregisterReceiver(this);
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/TokenCallback.java b/xendit-android/src/main/java/com/xendit/TokenCallback.java
new file mode 100644
index 0000000..a36f85b
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/TokenCallback.java
@@ -0,0 +1,15 @@
+package com.xendit;
+
+import com.xendit.Models.Token;
+import com.xendit.Models.XenditError;
+
+/**
+ * Created by Sergey on 3/16/17.
+ */
+
+public abstract class TokenCallback {
+
+ public abstract void onSuccess(Token token);
+
+ public abstract void onError(XenditError error);
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/Xendit.java b/xendit-android/src/main/java/com/xendit/Xendit.java
new file mode 100644
index 0000000..4afa24d
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/Xendit.java
@@ -0,0 +1,371 @@
+package com.xendit;
+
+import android.content.Context;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Base64;
+
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.Volley;
+import com.google.gson.JsonObject;
+import com.xendit.Models.Authentication;
+import com.xendit.Models.Card;
+import com.xendit.Models.TokenCredentials;
+import com.xendit.Models.TokenCreditCard;
+import com.xendit.Models.XenditError;
+import com.xendit.network.BaseRequest;
+import com.xendit.network.DefaultResponseHandler;
+import com.xendit.network.NetworkHandler;
+import com.xendit.network.errors.ConnectionError;
+import com.xendit.network.errors.NetworkError;
+import com.xendit.network.interfaces.ResultListener;
+
+import java.io.UnsupportedEncodingException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Created by Dimon_GDA on 3/7/17.
+ */
+
+public class Xendit {
+
+ private static final String NUMBER_REGEX = "^\\d+$";
+
+ private static final String PRODUCTION_FLEX_BASE_URL = "https://api.visa.com";
+ private static final String DEVELOPMENT_FLEX_BASE_URL = "https://sandbox.api.visa.com";
+// private static final String PUBLIC_DEVELOPMENT_FLEX_BASE_URL = "https://sandbox.webapi.visa.com";
+
+ private static final String STAGING_XENDIT_BASE_URL = "https://api-staging.xendit.co";
+ private static final String PRODUCTION_XENDIT_BASE_URL = "https://api.xendit.co";
+
+ private static final String GET_TOKEN_CREDENTIALS_URL = PRODUCTION_XENDIT_BASE_URL + "/credit_card_tokenization_credentials";
+ private static final String TOKENIZE_CREDIT_CARD_URL = "/cybersource/payments/flex/v1/tokens?apikey=";
+ private static final String CREATE_CREDIT_CARD_URL = PRODUCTION_XENDIT_BASE_URL + "/credit_card_tokens";
+
+ static final String ACTION_KEY = "ACTION_KEY";
+
+ private Context context;
+ private String publishableKey;
+ private RequestQueue requestQueue;
+ private ConnectivityManager connectivityManager;
+
+ public Xendit(Context context, String publishableKey) {
+ this.context = context;
+ this.publishableKey = publishableKey;
+ requestQueue = Volley.newRequestQueue(context);
+ connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ public static boolean isCardNumberValid(String creditCardNumber) {
+ return creditCardNumber != null
+ && creditCardNumber.length()
+ >= 12 && creditCardNumber.length()
+ <= 19 && creditCardNumber.matches(NUMBER_REGEX)
+ && getCardType(creditCardNumber) != null;
+ }
+
+ public static boolean isExpiryValid(String cardExpirationMonth, String cardExpirationYear) {
+ return cardExpirationMonth != null
+ && cardExpirationYear != null
+ && cardExpirationMonth.matches(NUMBER_REGEX)
+ && cardExpirationYear.matches(NUMBER_REGEX)
+ && number(cardExpirationMonth) >= 1
+ && number(cardExpirationMonth) <= 12
+ && number(cardExpirationYear) >= 2017
+ && number(cardExpirationYear) <= 2100
+ && getCurrentMonth(cardExpirationMonth, cardExpirationYear);
+ }
+
+ private static boolean getCurrentMonth(String cardExpirationMonth, String cardExpirationYear) {
+ DateFormat monthFormat = new SimpleDateFormat("MM", Locale.US);
+ int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+ Date monthDate = new Date();
+ int currentMonth = number(monthFormat.format(monthDate));
+ int expMonth = number(cardExpirationMonth);
+ int expYear = number(cardExpirationYear);
+ if (expYear == currentYear) {
+ if (expMonth >= currentMonth) {
+ return true;
+ }
+ } else if (expYear > currentYear) {
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isCvnValid(String creditCardCVN) {
+ return creditCardCVN != null
+ && creditCardCVN.matches(NUMBER_REGEX)
+ && number(creditCardCVN) > 0
+ && creditCardCVN.length() > 2
+ && creditCardCVN.length() <= 4;
+ }
+
+ private static CYBCardTypes getCardType(String cardNumber) {
+ if (cardNumber == null) {
+ return null;
+ } else if (cardNumber.indexOf("4") == 0) {
+ if (isCardVisaElectron(cardNumber)) {
+ return CYBCardTypes.VISA_ELECTRON;
+ } else {
+ return CYBCardTypes.VISA;
+ }
+ } else if (isCardAmex(cardNumber)) {
+ return CYBCardTypes.AMEX;
+ } else if (isCardMastercard(cardNumber)) {
+ return CYBCardTypes.MASTERCARD;
+ } else if (isCardDiscover(cardNumber)) {
+ return CYBCardTypes.DISCOVER;
+ } else if (isCardJCB(cardNumber)) {
+ return CYBCardTypes.JCB;
+ } else if (isCardDankort(cardNumber)) {
+ return CYBCardTypes.DANKORT;
+ } else if (isCardMaestro(cardNumber)) {
+ return CYBCardTypes.MAESTRO;
+ } else {
+ return null;
+ }
+ }
+
+ private static boolean isCardAmex(String cardNumber) {
+ return cardNumber != null && (cardNumber.indexOf("34") == 0 || cardNumber.indexOf("37") == 0);
+ }
+
+ private static boolean isCardMastercard(String cardNumber) {
+ if (cardNumber != null && cardNumber.length() >= 2) {
+ int startingNumber = number(cardNumber.substring(0, 2));
+ return startingNumber >= 51 && startingNumber <= 55;
+ } else {
+ return false;
+ }
+ }
+
+ private static int number(String sNumber) {
+ int number = -1;
+ if (sNumber != null) {
+ try {
+ number = Integer.parseInt(sNumber);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return number;
+ }
+
+ private static boolean isCardDiscover(String cardNumber) {
+ if (cardNumber != null && cardNumber.length() >= 6) {
+ int firstStartingNumber = number(cardNumber.substring(0, 3));
+ int secondStartingNumber = number(cardNumber.substring(0, 6));
+ return (firstStartingNumber >= 644 && firstStartingNumber <= 649)
+ || (secondStartingNumber >= 622126 && secondStartingNumber <= 622925)
+ || cardNumber.indexOf("65") == 0
+ || cardNumber.indexOf("6011") == 0;
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean isCardMaestro(String cardNumber) {
+ if (cardNumber != null && cardNumber.length() >= 2) {
+ int startingNumber = number(cardNumber.substring(0, 2));
+ return startingNumber == 50
+ || (startingNumber >= 56 && startingNumber <= 64)
+ || (startingNumber >= 66 && startingNumber <= 69);
+ }
+
+ return false;
+ }
+
+ private static boolean isCardDankort(String cardNumber) {
+ return cardNumber != null && cardNumber.indexOf("5019") == 0;
+ }
+
+ private static boolean isCardJCB(String cardNumber) {
+ if (cardNumber != null && cardNumber.length() >= 4) {
+ int startingNumber = number(cardNumber.substring(0, 4));
+ return startingNumber >= 3528 && startingNumber <= 3589;
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean isCardVisaElectron(String cardNumber) {
+ return cardNumber != null && (cardNumber.indexOf("4026") == 0
+ || cardNumber.indexOf("417500") == 0
+ || cardNumber.indexOf("4405") == 0
+ || cardNumber.indexOf("4508") == 0
+ || cardNumber.indexOf("4844") == 0
+ || cardNumber.indexOf("4913") == 0
+ || cardNumber.indexOf("4917") == 0);
+ }
+
+ public enum CYBCardTypes {
+ VISA("VISA", "001"),
+ MASTERCARD("MASTERCARD", "002"),
+ AMEX("AMEX", "003"),
+ DISCOVER("DISCOVER", "004"),
+ JCB("JCB", "007"),
+ VISA_ELECTRON("VISA_ELECTRON", "033"),
+ DANKORT("DANKORT", "034"),
+ MAESTRO("MAESTRO", "042");
+
+ private String cardName;
+ private String cardKey;
+
+ CYBCardTypes(String cardName, String cardKey) {
+ this.cardName = cardName;
+ this.cardKey = cardKey;
+ }
+
+ public String getCardName() {
+ return cardName;
+ }
+
+ public String getCardKey() {
+ return cardKey;
+ }
+ }
+
+ public void createToken(final Card card, final String amount, final TokenCallback tokenCallback) {
+ if (card != null && tokenCallback != null) {
+ if (!isCardNumberValid(card.getCreditCardNumber())) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_card_number)));
+ return;
+ }
+
+ if (!isExpiryValid(card.getCardExpirationMonth(), card.getCardExpirationYear())) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_card_expiration)));
+ return;
+ }
+
+ if (!isCvnValid(card.getCreditCardCVN())) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_card_cvn)));
+ return;
+ }
+
+ getTokenizationCredentials(new NetworkHandler().setResultListener(new ResultListener() {
+ @Override
+ public void onSuccess(TokenCredentials tokenCredentials) {
+ tokenizeCreditCardRequest(tokenCredentials.getFlexApiKey(), tokenCredentials.getTokenizationAuthKeyId(), card, amount, tokenCallback);
+ }
+
+ @Override
+ public void onFailure(NetworkError error) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_validation), error.getMessage()));
+ }
+ }));
+ }
+ }
+
+ private void tokenizeCreditCardRequest(String flexApiKey, String tokenizationAuthKeyId, final Card card, final String amount, final TokenCallback tokenCallback) {
+ tokenizeCreditCard(flexApiKey, tokenizationAuthKeyId, card, new NetworkHandler().setResultListener(new ResultListener() {
+ @Override
+ public void onSuccess(TokenCreditCard tokenCreditCard) {
+ createAuthentication(card, tokenCreditCard.getToken(), amount, tokenCallback);
+ }
+
+ @Override
+ public void onFailure(NetworkError error) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_tokenization), error.getMessage()));
+ }
+ }));
+ }
+
+ public void createAuthentication(Card card, String token, String amount, final TokenCallback tokenCallback) {
+ if (!isCvnValid(card.getCreditCardCVN())) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_card_cvn)));
+ return;
+ }
+
+ createCreditCardToken(card, token, amount, new NetworkHandler().setResultListener(new ResultListener() {
+ @Override
+ public void onSuccess(Authentication authentication) {
+ registerBroadcastReceiver(tokenCallback);
+ context.startActivity(XenditActivity.getLaunchIntent(context, authentication));
+ }
+
+ @Override
+ public void onFailure(NetworkError error) {
+ tokenCallback.onError(new XenditError(context.getString(R.string.create_token_error_tokenization), error.getMessage()));
+ }
+ }));
+ }
+
+ private void registerBroadcastReceiver(final TokenCallback tokenCallback) {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ context.registerReceiver(new TokenBroadcastReceiver(tokenCallback), new IntentFilter(ACTION_KEY));
+ }
+ });
+ }
+
+ private void getTokenizationCredentials(NetworkHandler handler) {
+ String encodedKey = encodeBase64(publishableKey + ":");
+ String basicAuthCredentials = "Basic " + encodedKey;
+ BaseRequest request = new BaseRequest<>(Request.Method.GET, GET_TOKEN_CREDENTIALS_URL, TokenCredentials.class, new DefaultResponseHandler<>(handler));
+ request.addHeader("Authorization", basicAuthCredentials.replace("\n", ""));
+ sendRequest(request, handler);
+ }
+
+ private void tokenizeCreditCard(String flexApiKey, String tokenizationAuthKeyId, Card card, NetworkHandler handler) {
+ String baseUrl = getEnvironment() ? PRODUCTION_FLEX_BASE_URL : DEVELOPMENT_FLEX_BASE_URL;
+ BaseRequest request = new BaseRequest<>(Request.Method.POST, baseUrl + TOKENIZE_CREDIT_CARD_URL + flexApiKey, TokenCreditCard.class, new DefaultResponseHandler<>(handler));
+ JsonObject cardInfoJson = new JsonObject();
+ cardInfoJson.addProperty("cardNumber", card.getCreditCardNumber());
+ cardInfoJson.addProperty("cardExpirationMonth", card.getCardExpirationMonth());
+ cardInfoJson.addProperty("cardExpirationYear", card.getCardExpirationYear());
+ cardInfoJson.addProperty("cardType", getCardType(card.getCreditCardNumber()).getCardKey());
+ request.addParam("keyId", tokenizationAuthKeyId);
+ request.addJsonParam("cardInfo", cardInfoJson);
+ sendRequest(request, handler);
+ }
+
+ private void createCreditCardToken(Card card, String token, String amount, NetworkHandler handler) {
+ String encodedKey = encodeBase64(publishableKey + ":");
+ String basicAuthCredentials = "Basic " + encodedKey;
+ BaseRequest request = new BaseRequest<>(Request.Method.POST, CREATE_CREDIT_CARD_URL, Authentication.class, new DefaultResponseHandler<>(handler));
+ request.addHeader("Authorization", basicAuthCredentials.replace("\n", ""));
+ request.addParam("is_authentication_bundled", "true");
+ request.addParam("amount", amount);
+ request.addParam("credit_card_token", token);
+ request.addParam("card_cvn", card.getCreditCardCVN());
+ sendRequest(request, handler);
+ }
+
+ private String encodeBase64(String key) {
+ try {
+ byte[] keyData = key.getBytes("UTF-8");
+ return Base64.encodeToString(keyData, Base64.DEFAULT);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private void sendRequest(BaseRequest request, NetworkHandler> handler) {
+ if (isConnectionAvailable()) {
+ requestQueue.add(request);
+ } else if (handler != null) {
+ handler.handleError(new ConnectionError());
+ }
+ }
+
+ private boolean isConnectionAvailable() {
+ NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
+ return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
+ }
+
+ private boolean getEnvironment() {
+ String publishKey = publishableKey.toUpperCase();
+ return publishKey.contains("PRODUCTION");
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/XenditActivity.java b/xendit-android/src/main/java/com/xendit/XenditActivity.java
new file mode 100644
index 0000000..b3b3392
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/XenditActivity.java
@@ -0,0 +1,87 @@
+package com.xendit;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.view.View;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ProgressBar;
+
+import com.xendit.Models.Authentication;
+
+/**
+ * Created by Sergey on 3/23/17.
+ */
+
+public class XenditActivity extends Activity {
+
+ public static final String MESSAGE_KEY = "message_key";
+ private static final String AUTHENTICATION_KEY = "authentication_key";
+
+ private ProgressBar progressBar;
+
+ public static Intent getLaunchIntent(Context context, Authentication authentication) {
+ Intent intent = new Intent(context, XenditActivity.class);
+ intent.setClass(context, XenditActivity.class);
+ intent.setAction(XenditActivity.class.getName());
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ intent.putExtra(AUTHENTICATION_KEY, authentication);
+ return intent;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_xendit);
+ WebView webView = (WebView) findViewById(R.id.webView_XenditActivity);
+ progressBar = (ProgressBar) findViewById(R.id.progressBar_XenditActivity);
+ webView.getSettings().setJavaScriptEnabled(true);
+ webView.setWebChromeClient(new WebChromeClient());
+ webView.setWebViewClient(new WebViewClient() {
+
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ super.onPageStarted(view, url, favicon);
+ progressBar.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ super.onPageFinished(view, url);
+ progressBar.setVisibility(View.GONE);
+ }
+
+ });
+ webView.addJavascriptInterface(new WebViewJavaScriptInterface(), "MobileBridge");
+ Authentication authentication = getIntent().getParcelableExtra(AUTHENTICATION_KEY);
+ String baseUrl = "https://api.xendit.co";
+ String data = " ";
+ webView.loadDataWithBaseURL(baseUrl, data, "text/html", "utf-8", null);
+ }
+
+ private class WebViewJavaScriptInterface {
+
+ @android.webkit.JavascriptInterface
+ public void postMessage(String message) {
+ sendBroadcastReceiver(message);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ sendBroadcastReceiver(getString(R.string.create_token_error_validation));
+ }
+
+ private void sendBroadcastReceiver(String message) {
+ Intent intent = new Intent();
+ intent.setAction(Xendit.ACTION_KEY);
+ intent.putExtra(MESSAGE_KEY, message);
+ sendBroadcast(intent);
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/BaseRequest.java b/xendit-android/src/main/java/com/xendit/network/BaseRequest.java
new file mode 100644
index 0000000..3d66c90
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/BaseRequest.java
@@ -0,0 +1,175 @@
+package com.xendit.network;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.NetworkResponse;
+import com.android.volley.ParseError;
+import com.android.volley.Request;
+import com.android.volley.Response;
+import com.android.volley.RetryPolicy;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.HttpHeaderParser;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.stream.JsonReader;
+import com.xendit.network.errors.NetworkError;
+import com.xendit.network.interfaces.TokenExpiredListener;
+
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+public class BaseRequest extends Request {
+
+ private static final String NO_AUTHENTICATION_CHALLENGES_FOUND_ERROR = "java.io.IOException: No authentication challenges found";
+ private static final String PROTOCOL_CHARSET = "utf-8";
+ private static final String PROTOCOL_CONTENT_TYPE = String.format("application/json; charset=%s", PROTOCOL_CHARSET);
+
+ private final Response.Listener listener;
+ private Map headers;
+ private final GsonBuilder gsonBuilder = new GsonBuilder();
+ private final Type type;
+ private TokenExpiredListener tokenExpiredListener;
+ private boolean isRefreshToken = true;
+ private JsonObject jsonBody;
+ private Gson gson;
+
+ private BaseRequest(int method, String url, Type type, Response.Listener successListener, Response.ErrorListener errorListener) {
+ super(method, url, errorListener);
+ this.listener = successListener;
+ this.type = type;
+ setShouldCache(false);
+ setRetryPolicy(new BaseRetryPolicy());
+ }
+
+ public BaseRequest(int method, String url, Type type, DefaultResponseHandler responseListener) {
+ this(method, url, type, responseListener, responseListener);
+ }
+
+ public void addParam(String key, String value) {
+ if (jsonBody == null) {
+ jsonBody = new JsonObject();
+ }
+ jsonBody.addProperty(key, value);
+ }
+
+ public void addJsonParam(String key, JsonElement jsonParam) {
+ if (jsonBody == null) {
+ jsonBody = new JsonObject();
+ }
+ jsonBody.add(key, jsonParam);
+ }
+
+ public void addHeader(String key, String value) {
+ if (headers == null) {
+ headers = new HashMap<>();
+ }
+ headers.put(key, value);
+ }
+
+ @Override
+ public Map getHeaders() throws AuthFailureError {
+ return headers != null ? headers : super.getHeaders();
+ }
+
+ @Override
+ protected void deliverResponse(T response) {
+ if (null != listener) {
+ listener.onResponse(response);
+ }
+ }
+
+ @Override
+ protected Response parseNetworkResponse(NetworkResponse response) {
+ try {
+ String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
+ return Response.success(parseResult(jsonString), HttpHeaderParser.parseCacheHeaders(response));
+ } catch (UnsupportedEncodingException | JsonParseException e) {
+ return Response.error(new ParseError(e));
+ }
+ }
+
+ private T parseResult(String response) {
+ T result;
+ if (type == String.class) {
+ result = (T) response;
+ } else {
+ JsonReader reader = new JsonReader(new StringReader(response));
+ reader.setLenient(true);
+ result = getGson().fromJson(reader, type);
+ }
+ return result;
+ }
+
+ private Gson createGson() {
+ return gsonBuilder.create();
+ }
+
+ private Gson getGson() {
+ if (gson == null) {
+ gson = createGson();
+ }
+ return gson;
+ }
+
+ @Override
+ public String getBodyContentType() {
+ return PROTOCOL_CONTENT_TYPE;
+ }
+
+ @Override
+ public byte[] getBody() throws AuthFailureError {
+ byte[] body = null;
+ if (jsonBody != null) {
+ try {
+ body = getGson().toJson(jsonBody).getBytes(PROTOCOL_CHARSET);
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+ return body;
+ }
+
+ void setTokenExpiredListener(TokenExpiredListener listener) {
+ tokenExpiredListener = listener;
+ }
+
+ @SuppressWarnings("ThrowableInstanceNeverThrown")
+ @Override
+ public void deliverError(VolleyError error) {
+ boolean isTokenExpired;
+ NetworkError networkError = new NetworkError(error);
+ isTokenExpired = networkError.responseCode == 401 || NO_AUTHENTICATION_CHALLENGES_FOUND_ERROR.equalsIgnoreCase(networkError.getMessage());
+ if (isTokenExpired && isRefreshToken && tokenExpiredListener != null) {
+ isRefreshToken = false;
+ tokenExpiredListener.onTokenExpired(this);
+ } else {
+ super.deliverError(error);
+ }
+ }
+
+ private class BaseRetryPolicy implements RetryPolicy {
+
+ private static final int DEFAULT_TIMEOUT_MS = 15 * 1000;
+ private static final int RETRY_COUNT = 0;
+
+ @Override
+ public int getCurrentTimeout() {
+ return DEFAULT_TIMEOUT_MS;
+ }
+
+ @Override
+ public int getCurrentRetryCount() {
+ return RETRY_COUNT;
+ }
+
+ @Override
+ public void retry(VolleyError error) throws VolleyError {
+ throw error;
+ }
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/DefaultResponseHandler.java b/xendit-android/src/main/java/com/xendit/network/DefaultResponseHandler.java
new file mode 100644
index 0000000..24ad0ac
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/DefaultResponseHandler.java
@@ -0,0 +1,41 @@
+package com.xendit.network;
+
+import com.android.volley.AuthFailureError;
+import com.android.volley.NoConnectionError;
+import com.android.volley.Response;
+import com.android.volley.TimeoutError;
+import com.android.volley.VolleyError;
+import com.xendit.network.errors.AuthorisationError;
+import com.xendit.network.errors.ConnectionError;
+import com.xendit.network.errors.NetworkError;
+
+public class DefaultResponseHandler implements Response.Listener, Response.ErrorListener {
+
+ private NetworkHandler handler;
+
+ public DefaultResponseHandler(NetworkHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (handler != null) {
+ NetworkError netError;
+ if (error instanceof TimeoutError || error instanceof NoConnectionError) {
+ netError = new ConnectionError(error);
+ } else if (error instanceof AuthFailureError) {
+ netError = new AuthorisationError(error);
+ } else {
+ netError = new NetworkError(error);
+ }
+ handler.handleError(netError);
+ }
+ }
+
+ @Override
+ public void onResponse(T response) {
+ if (handler != null) {
+ handler.handleSuccess(response);
+ }
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/NetworkHandler.java b/xendit-android/src/main/java/com/xendit/network/NetworkHandler.java
new file mode 100644
index 0000000..36a07d3
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/NetworkHandler.java
@@ -0,0 +1,52 @@
+package com.xendit.network;
+
+import com.xendit.network.errors.AuthorisationError;
+import com.xendit.network.errors.ConnectionError;
+import com.xendit.network.errors.NetworkError;
+import com.xendit.network.interfaces.AuthorisationErrorListener;
+import com.xendit.network.interfaces.ConnectionErrorListener;
+import com.xendit.network.interfaces.ResultListener;
+
+public class NetworkHandler {
+
+ private ResultListener resultListener;
+ private ConnectionErrorListener connectionListener;
+ private AuthorisationErrorListener authorisationListener;
+
+ final void handleSuccess(final T response) {
+ if (resultListener != null) {
+ resultListener.onSuccess(response);
+ }
+ }
+
+ public final void handleError(final NetworkError error) {
+ if (error instanceof ConnectionError && connectionListener != null) {
+ connectionListener.onConnectionError(error);
+ } else if (error instanceof AuthorisationError && authorisationListener != null) {
+ authorisationListener.onAuthorisationError(error);
+ } else {
+ deliverError(error);
+ }
+ }
+
+ private void deliverError(NetworkError error) {
+ if (resultListener != null) {
+ resultListener.onFailure(error);
+ }
+ }
+
+ public NetworkHandler setResultListener(ResultListener resultListener) {
+ this.resultListener = resultListener;
+ return this;
+ }
+
+ public NetworkHandler setConnectionErrorListener(ConnectionErrorListener connectionErrorListener) {
+ this.connectionListener = connectionErrorListener;
+ return this;
+ }
+
+ public NetworkHandler setAuthorisationErrorListener(AuthorisationErrorListener authorisationListener) {
+ this.authorisationListener = authorisationListener;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/errors/AuthorisationError.java b/xendit-android/src/main/java/com/xendit/network/errors/AuthorisationError.java
new file mode 100644
index 0000000..564c73b
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/errors/AuthorisationError.java
@@ -0,0 +1,10 @@
+package com.xendit.network.errors;
+
+import com.android.volley.VolleyError;
+
+public class AuthorisationError extends NetworkError {
+
+ public AuthorisationError(VolleyError error) {
+ super(error);
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/errors/ConnectionError.java b/xendit-android/src/main/java/com/xendit/network/errors/ConnectionError.java
new file mode 100644
index 0000000..d9a1d7b
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/errors/ConnectionError.java
@@ -0,0 +1,16 @@
+package com.xendit.network.errors;
+
+import com.android.volley.VolleyError;
+
+public class ConnectionError extends NetworkError {
+
+ private static final String NO_INTERNET_CONNECTION_ERROR = "No internet connection";
+
+ public ConnectionError() {
+ super(NO_INTERNET_CONNECTION_ERROR);
+ }
+
+ public ConnectionError(VolleyError error) {
+ super(error);
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/errors/NetworkError.java b/xendit-android/src/main/java/com/xendit/network/errors/NetworkError.java
new file mode 100644
index 0000000..f46ea65
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/errors/NetworkError.java
@@ -0,0 +1,25 @@
+package com.xendit.network.errors;
+
+import com.android.volley.NetworkResponse;
+import com.android.volley.VolleyError;
+
+public class NetworkError extends Exception {
+
+ public int responseCode = -1;
+ private String errorResponse;
+
+ public NetworkError(VolleyError error) {
+ super(error.getMessage(), error.getCause());
+ NetworkResponse networkResponse = error.networkResponse;
+ if (networkResponse != null) {
+ responseCode = networkResponse.statusCode;
+ if (networkResponse.data != null) {
+ errorResponse = new String(networkResponse.data).trim();
+ }
+ }
+ }
+
+ NetworkError(String detailMessage) {
+ super(detailMessage);
+ }
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/interfaces/AuthorisationErrorListener.java b/xendit-android/src/main/java/com/xendit/network/interfaces/AuthorisationErrorListener.java
new file mode 100644
index 0000000..fe14aa7
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/interfaces/AuthorisationErrorListener.java
@@ -0,0 +1,7 @@
+package com.xendit.network.interfaces;
+
+import com.xendit.network.errors.NetworkError;
+
+public interface AuthorisationErrorListener {
+ void onAuthorisationError(NetworkError error);
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/interfaces/ConnectionErrorListener.java b/xendit-android/src/main/java/com/xendit/network/interfaces/ConnectionErrorListener.java
new file mode 100644
index 0000000..4cd43d8
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/interfaces/ConnectionErrorListener.java
@@ -0,0 +1,7 @@
+package com.xendit.network.interfaces;
+
+import com.xendit.network.errors.NetworkError;
+
+public interface ConnectionErrorListener {
+ void onConnectionError(NetworkError error);
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/interfaces/ResultListener.java b/xendit-android/src/main/java/com/xendit/network/interfaces/ResultListener.java
new file mode 100644
index 0000000..01beb3d
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/interfaces/ResultListener.java
@@ -0,0 +1,10 @@
+package com.xendit.network.interfaces;
+
+import com.xendit.network.errors.NetworkError;
+
+public interface ResultListener {
+
+ void onSuccess(T responseObject);
+
+ void onFailure(NetworkError error);
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/java/com/xendit/network/interfaces/TokenExpiredListener.java b/xendit-android/src/main/java/com/xendit/network/interfaces/TokenExpiredListener.java
new file mode 100644
index 0000000..4d66511
--- /dev/null
+++ b/xendit-android/src/main/java/com/xendit/network/interfaces/TokenExpiredListener.java
@@ -0,0 +1,8 @@
+package com.xendit.network.interfaces;
+
+
+import com.xendit.network.BaseRequest;
+
+public interface TokenExpiredListener {
+ void onTokenExpired(BaseRequest request);
+}
\ No newline at end of file
diff --git a/xendit-android/src/main/res/layout/activity_xendit.xml b/xendit-android/src/main/res/layout/activity_xendit.xml
new file mode 100644
index 0000000..73ad6f2
--- /dev/null
+++ b/xendit-android/src/main/res/layout/activity_xendit.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/xendit-android/src/main/res/values/strings.xml b/xendit-android/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8b41fa9
--- /dev/null
+++ b/xendit-android/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+ xendit-android
+
+
+ Card number is invalid
+ Card expiration date is invalid
+ Card CVN is invalid
+ Tokenization Error
+ Validation Error
+
\ No newline at end of file
diff --git a/xendit-android/src/test/java/com/xendit/ExampleUnitTest.java b/xendit-android/src/test/java/com/xendit/ExampleUnitTest.java
new file mode 100644
index 0000000..ae57f73
--- /dev/null
+++ b/xendit-android/src/test/java/com/xendit/ExampleUnitTest.java
@@ -0,0 +1,32 @@
+package com.xendit;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+
+ @Test
+ public void isCvnValid() throws Exception {
+ boolean cvnValid = Xendit.isCvnValid("4256");
+ assertTrue(cvnValid);
+// assertEquals(true, cvnValid);
+ }
+
+ @Test
+ public void isCreditCardValid() throws Exception {
+ boolean cardNumberValid = Xendit.isCardNumberValid("4149045387380958");
+ assertTrue(cardNumberValid);
+ }
+
+ @Test
+ public void isExpiryValid() throws Exception {
+ boolean expiryValid = Xendit.isExpiryValid("06", "2020");
+ assertTrue(expiryValid);
+ }
+}
\ No newline at end of file