├── .gitignore
├── app
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── cloudipsp
│ │ └── android
│ │ └── demo
│ │ ├── BaseExampleActivity.java
│ │ ├── FlexibleExampleActivity.java
│ │ ├── GPay.java
│ │ ├── MainActivity.java
│ │ └── SimpleExampleActivity.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_launcher.png
│ ├── drawable-mdpi
│ └── ic_launcher.png
│ ├── drawable-xhdpi
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ └── ic_launcher.png
│ ├── layout
│ ├── activity_flexible_example.xml
│ ├── activity_main.xml
│ └── activity_simple_example.xml
│ └── values
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── build.gradle
├── gradle.properties
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── cloudipsp
│ │ └── android
│ │ ├── BaseConfirmationErrorHandler.java
│ │ ├── Card.java
│ │ ├── CardCvvEdit.java
│ │ ├── CardDisplay.java
│ │ ├── CardExpMmEdit.java
│ │ ├── CardExpYyEdit.java
│ │ ├── CardInputBase.java
│ │ ├── CardInputLayout.java
│ │ ├── CardInputView.java
│ │ ├── CardNumberEdit.java
│ │ ├── Cloudipsp.java
│ │ ├── CloudipspView.java
│ │ ├── CloudipspWebView.java
│ │ ├── Currency.java
│ │ ├── CvvUtils.java
│ │ ├── GooglePayCall.java
│ │ ├── Order.java
│ │ ├── Receipt.java
│ │ ├── ReceiptUtils.java
│ │ └── Tls12SocketFactory.java
│ └── res
│ ├── layout
│ └── com_cloudipsp_android_card_input_view.xml
│ ├── values-ru
│ └── strings.xml
│ └── values
│ └── strings.xml
├── maven_push.gradle
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # svn
2 | *.svn*
3 |
4 | # built application files
5 | *.apk
6 | *.ap_
7 |
8 | # files for the dex VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # generated GUI files
15 | */R.java
16 |
17 | # generated folder
18 | bin
19 | gen
20 |
21 | # local
22 | local.properties
23 |
24 | proguard_logs/
25 |
26 | # log files
27 | log*.txt
28 |
29 | # archives
30 | *.gz
31 | *.tar
32 | *.zip
33 |
34 | # eclipse
35 | *.metadata
36 | *.settings
37 | *.prefs
38 |
39 | #idea
40 | *.idea
41 | *.iml
42 | .idea/
43 | out/
44 |
45 | build/
46 | .gradle/
47 | .DS_Store
48 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | def safeExtGet(prop, fallback) {
4 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
5 | }
6 |
7 | android {
8 | compileSdkVersion safeExtGet('compileSdkVersion', 33)
9 | buildToolsVersion safeExtGet('buildToolsVersion', '33.0.0')
10 |
11 | defaultConfig {
12 | applicationId 'com.cloudipsp.android.demo'
13 | minSdkVersion safeExtGet('minSdkVersion', 14)
14 | targetSdkVersion safeExtGet('targetSdkVersion', 33)
15 | versionCode 1
16 | versionName '1.0'
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | }
22 | }
23 | }
24 |
25 | dependencies {
26 | implementation 'com.google.android.gms:play-services-base:18.1.0'
27 | implementation 'com.google.android.gms:play-services-wallet:19.1.0'
28 | implementation project(':library')
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
27 |
28 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cloudipsp/android/demo/BaseExampleActivity.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android.demo;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.text.TextUtils;
7 | import android.util.Patterns;
8 | import android.view.View;
9 | import android.widget.ArrayAdapter;
10 | import android.widget.EditText;
11 | import android.widget.Spinner;
12 | import android.widget.Toast;
13 |
14 | import com.cloudipsp.android.Card;
15 | import com.cloudipsp.android.Cloudipsp;
16 | import com.cloudipsp.android.CloudipspWebView;
17 | import com.cloudipsp.android.Currency;
18 | import com.cloudipsp.android.GooglePayCall;
19 | import com.cloudipsp.android.Order;
20 | import com.cloudipsp.android.Receipt;
21 |
22 | /**
23 | * Created by vberegovoy on 6/20/17.
24 | */
25 |
26 | abstract public class BaseExampleActivity extends Activity implements
27 | View.OnClickListener,
28 | Cloudipsp.PayCallback,
29 | Cloudipsp.GooglePayCallback {
30 | private static final int RC_GOOGLE_PAY = 100500;
31 | private static final String K_GOOGLE_PAY_CALL = "google_pay_call";
32 |
33 | private EditText editAmount;
34 | private Spinner spinnerCcy;
35 | private EditText editEmail;
36 | private EditText editDescription;
37 | private CloudipspWebView webView;
38 |
39 | private Cloudipsp cloudipsp;
40 | private GooglePayCall googlePayCall;// <- this should be serialized on saving instance state
41 |
42 | protected abstract int getLayoutResId();
43 |
44 | protected abstract Card getCard();
45 |
46 | @Override
47 | protected void onCreate(Bundle savedInstanceState) {
48 | super.onCreate(savedInstanceState);
49 | setContentView(getLayoutResId());
50 |
51 | findViewById(R.id.btn_amount).setOnClickListener(this);
52 | editAmount = findViewById(R.id.edit_amount);
53 | spinnerCcy = findViewById(R.id.spinner_ccy);
54 | editEmail = findViewById(R.id.edit_email);
55 | editDescription = findViewById(R.id.edit_description);
56 | findViewById(R.id.btn_pay_card).setOnClickListener(this);
57 | findViewById(R.id.btn_pay_google).setOnClickListener(this);
58 |
59 | webView = findViewById(R.id.web_view);
60 | cloudipsp = new Cloudipsp(1396424, webView);
61 |
62 | spinnerCcy.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, Currency.values()));
63 |
64 | if (savedInstanceState != null) {
65 | googlePayCall = savedInstanceState.getParcelable(K_GOOGLE_PAY_CALL);
66 | }
67 | }
68 |
69 | @Override
70 | protected void onSaveInstanceState(Bundle outState) {
71 | super.onSaveInstanceState(outState);
72 | outState.putParcelable(K_GOOGLE_PAY_CALL, googlePayCall);
73 | }
74 |
75 | @Override
76 | public void onBackPressed() {
77 | if (webView.waitingForConfirm()) {
78 | webView.skipConfirm();
79 | } else {
80 | super.onBackPressed();
81 | }
82 | }
83 |
84 | @Override
85 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
86 | super.onActivityResult(requestCode, resultCode, data);
87 |
88 | switch (requestCode) {
89 | case RC_GOOGLE_PAY:
90 | if (!cloudipsp.googlePayComplete(resultCode, data, googlePayCall, this)) {
91 | Toast.makeText(this, R.string.e_google_pay_canceled, Toast.LENGTH_LONG).show();
92 | }
93 | break;
94 | }
95 | }
96 |
97 | @Override
98 | public void onClick(View v) {
99 | switch (v.getId()) {
100 | case R.id.btn_amount:
101 | fillTest();
102 | break;
103 | case R.id.btn_pay_card:
104 | processPayCard();
105 | break;
106 | case R.id.btn_pay_google:
107 | processGooglePay();
108 | break;
109 | }
110 | }
111 |
112 | private void fillTest() {
113 | editAmount.setText("1");
114 | editEmail.setText("test@example.com");
115 | editDescription.setText("test payment");
116 | }
117 |
118 | private void processPayCard() {
119 | final Order order = createOrder();
120 | if (order != null) {
121 | final Card card = getCard();
122 | if (card != null) {
123 | cloudipsp.pay(card, order, this);
124 | }
125 | }
126 | }
127 |
128 | private void processGooglePay() {
129 | if (Cloudipsp.supportsGooglePay(this)) {
130 | final Order googlePayOrder = createOrder();
131 | if (googlePayOrder != null) {
132 | cloudipsp.googlePayInitialize(googlePayOrder, this, RC_GOOGLE_PAY, this);
133 | }
134 | } else {
135 | Toast.makeText(this, R.string.e_google_pay_unsupported, Toast.LENGTH_LONG).show();
136 |
137 | }
138 | }
139 |
140 | private Order createOrder() {
141 | editAmount.setError(null);
142 | editEmail.setError(null);
143 | editDescription.setError(null);
144 |
145 | final int amount;
146 | try {
147 | amount = Integer.valueOf(editAmount.getText().toString());
148 | } catch (Exception e) {
149 | editAmount.setError(getString(R.string.e_invalid_amount));
150 | return null;
151 | }
152 |
153 | final String email = editEmail.getText().toString();
154 | final String description = editDescription.getText().toString();
155 | if (TextUtils.isEmpty(email) || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
156 | editEmail.setError(getString(R.string.e_invalid_email));
157 | return null;
158 | } else if (TextUtils.isEmpty(description)) {
159 | editDescription.setError(getString(R.string.e_invalid_description));
160 | return null;
161 | }
162 | final Currency currency = (Currency) spinnerCcy.getSelectedItem();
163 | final Order order = new Order(amount, currency, "vb_" + System.currentTimeMillis(), description, email);
164 | order.setLang(Order.Lang.ru);
165 | return order;
166 | }
167 |
168 | @Override
169 | public void onPaidProcessed(Receipt receipt) {
170 | Toast.makeText(this, "Paid " + receipt.status.name() + "\nPaymentId:" + receipt.paymentId, Toast.LENGTH_LONG).show();
171 | }
172 |
173 | @Override
174 | public void onPaidFailure(Cloudipsp.Exception e) {
175 | if (e instanceof Cloudipsp.Exception.Failure) {
176 | Cloudipsp.Exception.Failure f = (Cloudipsp.Exception.Failure) e;
177 |
178 | Toast.makeText(this, "Failure\nErrorCode: " +
179 | f.errorCode + "\nMessage: " + f.getMessage() + "\nRequestId: " + f.requestId, Toast.LENGTH_LONG).show();
180 | } else if (e instanceof Cloudipsp.Exception.NetworkSecurity) {
181 | Toast.makeText(this, "Network security error: " + e.getMessage(), Toast.LENGTH_LONG).show();
182 | } else if (e instanceof Cloudipsp.Exception.ServerInternalError) {
183 | Toast.makeText(this, "Internal server error: " + e.getMessage(), Toast.LENGTH_LONG).show();
184 | } else if (e instanceof Cloudipsp.Exception.NetworkAccess) {
185 | Toast.makeText(this, "Network error", Toast.LENGTH_LONG).show();
186 | } else {
187 | Toast.makeText(this, "Payment Failed", Toast.LENGTH_LONG).show();
188 | }
189 | e.printStackTrace();
190 | }
191 |
192 | @Override
193 | public void onGooglePayInitialized(GooglePayCall result) {
194 | this.googlePayCall = result;
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cloudipsp/android/demo/FlexibleExampleActivity.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android.demo;
2 |
3 | import android.os.Bundle;
4 | import android.util.Log;
5 | import android.widget.EditText;
6 |
7 | import com.cloudipsp.android.Card;
8 | import com.cloudipsp.android.CardInputLayout;
9 | import com.cloudipsp.android.Cloudipsp;
10 |
11 | public class FlexibleExampleActivity extends BaseExampleActivity {
12 | private EditText editCard;
13 | private EditText editExpYy;
14 | private EditText editExpMm;
15 | private EditText editCvv;
16 | private CardInputLayout cardLayout;
17 |
18 | @Override
19 | protected void onCreate(Bundle savedInstanceState) {
20 | super.onCreate(savedInstanceState);
21 |
22 | editCard = findViewById(R.id.edit_card_number);
23 | editExpYy = findViewById(R.id.edit_yy);
24 | editExpMm = findViewById(R.id.edit_mm);
25 | editCvv = findViewById(R.id.edit_cvv);
26 | // ^^^ these fields used only as example for Cloudipsp.setStrictUiBlocking(false);
27 | cardLayout = findViewById(R.id.card_layout);
28 | cardLayout.setCardNumberFormatting(false);
29 | }
30 |
31 | @Override
32 | protected int getLayoutResId() {
33 | return R.layout.activity_flexible_example;
34 | }
35 |
36 | @Override
37 | protected Card getCard() {
38 | // Cloudipsp.setStrictUiBlocking(false);
39 | // Log.i("Cloudipsp", "CardNumber: " + editCard.getText());
40 | // Log.i("Cloudipsp", "ExpYy: " + editExpYy.getText());
41 | // Log.i("Cloudipsp", "ExpMm: " + editExpMm.getText());
42 | // Log.i("Cloudipsp", "Cvv: " + editCvv.getText());
43 |
44 | return cardLayout.confirm(new CardInputLayout.ConfirmationErrorHandler() {
45 | @Override
46 | public void onCardInputErrorClear(CardInputLayout view, EditText editText) {
47 |
48 | }
49 |
50 | @Override
51 | public void onCardInputErrorCatched(CardInputLayout view, EditText editText, String error) {
52 |
53 | }
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cloudipsp/android/demo/GPay.java:
--------------------------------------------------------------------------------
1 | package com.example.myapplication;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.util.Log;
6 | import android.view.View;
7 | import android.widget.Button;
8 | import android.widget.Toast;
9 |
10 | import androidx.annotation.Nullable;
11 | import androidx.appcompat.app.AppCompatActivity;
12 |
13 | import com.cloudipsp.android.Cloudipsp;
14 | import com.cloudipsp.android.CloudipspWebView;
15 | import com.cloudipsp.android.GooglePayCall;
16 | import com.cloudipsp.android.Order;
17 | import com.cloudipsp.android.Receipt;
18 |
19 | public class MainActivity extends AppCompatActivity implements
20 | View.OnClickListener, // Implementing OnClickListener for handling button clicks
21 | Cloudipsp.PayCallback, // Implementing Cloudipsp.PayCallback for payment callbacks
22 | Cloudipsp.GooglePayCallback { // Implementing Cloudipsp.GooglePayCallback for Google Pay callbacks
23 |
24 | private static final int RC_GOOGLE_PAY = 100500;
25 | private static final String K_GOOGLE_PAY_CALL = "google_pay_call";
26 | private Cloudipsp cloudipsp;
27 | private GooglePayCall googlePayCall; // <- this should be serialized on saving instance state
28 | private CloudipspWebView webView;
29 | private Button googlePayButton;
30 |
31 | @Override
32 | protected void onCreate(Bundle savedInstanceState) {
33 | super.onCreate(savedInstanceState);
34 | setContentView(R.layout.activity_main); // Set the layout for this activity
35 |
36 | // Initialize UI elements
37 | webView = findViewById(R.id.webView); // Initialize CloudipspWebView from layout
38 | googlePayButton = findViewById(R.id.google_pay_button); // Initialize Button from layout
39 | googlePayButton.setOnClickListener(this); // Set click listener for Google Pay button
40 |
41 | // Check if Google Pay is supported and set button visibility accordingly
42 | if (Cloudipsp.supportsGooglePay(this)) {
43 | googlePayButton.setVisibility(View.VISIBLE); // Show Google Pay button
44 | } else {
45 | googlePayButton.setVisibility(View.GONE); // Hide Google Pay button if unsupported
46 | Toast.makeText(this, R.string.e_google_pay_unsupported, Toast.LENGTH_LONG).show(); // Show unsupported message
47 | }
48 |
49 | if (savedInstanceState != null) {
50 | googlePayCall = savedInstanceState.getParcelable(K_GOOGLE_PAY_CALL);
51 | }
52 | }
53 |
54 | @Override
55 | public void onBackPressed() {
56 | if (webView.waitingForConfirm()) {
57 | webView.skipConfirm(); // Skip confirmation in WebView if waiting
58 | } else {
59 | super.onBackPressed(); // Otherwise, perform default back button behavior
60 | }
61 | }
62 |
63 |
64 | @Override
65 | public void onClick(View v) {
66 | if (v.getId() == R.id.google_pay_button) {
67 | processGooglePay(); // Handle click on Google Pay button
68 | // processGooglePayWithToken(); // Handle click on Google Pay button
69 | }
70 | }
71 |
72 | private void processGooglePay() {
73 | // Initialize Cloudipsp with merchant ID and WebView
74 | cloudipsp = new Cloudipsp(0, webView); // Initialize the payment process with the merchant ID
75 | final Order googlePayOrder = createOrder(); // Create order for Google Pay payment
76 | if (googlePayOrder != null) {
77 | cloudipsp.googlePayInitialize(googlePayOrder, this, RC_GOOGLE_PAY, this); // Initialize Google Pay payment
78 | }
79 | }
80 |
81 | private void processGooglePayWithToken() {
82 | // Initialize Cloudipsp with merchant ID and WebView
83 | cloudipsp = new Cloudipsp(0, webView); // Initialize the payment process with the merchant ID
84 | cloudipsp.googlePayInitialize("321d7ebe83c2b34ce38fee59c4c845e9fef67a0b", this, RC_GOOGLE_PAY, this); // Initialize Google Pay payment
85 | }
86 |
87 |
88 |
89 | private Order createOrder() {
90 | final int amount = 100;
91 | final String email = "test@gmail.com";
92 | final String description = "test payment";
93 | final String currency = "GEL";
94 | return new Order(amount, currency, "vb_" + System.currentTimeMillis(), description, email); // Create and return new payment order
95 | }
96 |
97 | @Override
98 | protected void onSaveInstanceState(Bundle outState) {
99 | super.onSaveInstanceState(outState);
100 | outState.putParcelable(K_GOOGLE_PAY_CALL, googlePayCall);
101 | }
102 |
103 | @Override
104 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
105 | super.onActivityResult(requestCode, resultCode, data);
106 | switch (requestCode) {
107 | case RC_GOOGLE_PAY:
108 | if (!cloudipsp.googlePayComplete(resultCode, data, googlePayCall, this)) {
109 | Toast.makeText(this, R.string.e_google_pay_canceled, Toast.LENGTH_LONG).show(); // Show payment canceled message
110 | }
111 | break;
112 | }
113 | }
114 |
115 | @Override
116 | public void onPaidProcessed(Receipt receipt) {
117 | Toast.makeText(this, "Paid " + receipt.status.name() + "\nPaymentId:" + receipt.paymentId, Toast.LENGTH_LONG).show(); // Show payment success message
118 | Log.d("PaymentStatus", "Paid " + receipt.status.name() + " PaymentId: " + receipt.paymentId);
119 |
120 | }
121 |
122 | @Override
123 | public void onPaidFailure(Cloudipsp.Exception e) {
124 | if (e instanceof Cloudipsp.Exception.Failure) {
125 | Cloudipsp.Exception.Failure f = (Cloudipsp.Exception.Failure) e;
126 | Toast.makeText(this, "Failure\nErrorCode: " +
127 | f.errorCode + "\nMessage: " + f.getMessage() + "\nRequestId: " + f.requestId, Toast.LENGTH_LONG).show(); // Show specific failure details
128 | } else if (e instanceof Cloudipsp.Exception.NetworkSecurity) {
129 | Toast.makeText(this, "Network security error: " + e.getMessage(), Toast.LENGTH_LONG).show(); // Show network security error
130 | } else if (e instanceof Cloudipsp.Exception.ServerInternalError) {
131 | Toast.makeText(this, "Internal server error: " + e.getMessage(), Toast.LENGTH_LONG).show(); // Show internal server error
132 | } else if (e instanceof Cloudipsp.Exception.NetworkAccess) {
133 | Toast.makeText(this, "Network error", Toast.LENGTH_LONG).show(); // Show network access error
134 | } else {
135 | Toast.makeText(this, "Payment Failed", Toast.LENGTH_LONG).show(); // Show generic payment failure
136 | }
137 | e.printStackTrace(); // Print stack trace for debugging
138 | }
139 |
140 | @Override
141 | public void onGooglePayInitialized(GooglePayCall result) {
142 | // Handle Google Pay initialization if needed
143 | Toast.makeText(this, "Google Pay initialized", Toast.LENGTH_LONG).show(); // Show Google Pay initialization message
144 | this.googlePayCall = result; // Store Google Pay call result
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cloudipsp/android/demo/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android.demo;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.view.View;
7 |
8 | /**
9 | * Created by vberegovoy on 6/20/17.
10 | */
11 |
12 | public class MainActivity extends Activity {
13 | @Override
14 | protected void onCreate(Bundle savedInstanceState) {
15 | super.onCreate(savedInstanceState);
16 | setContentView(R.layout.activity_main);
17 | }
18 |
19 | public void onSimpleExampleClicked(View view) {
20 | startActivity(new Intent(this, SimpleExampleActivity.class));
21 | }
22 |
23 | public void onFlexibleExampleClicked(View view) {
24 | startActivity(new Intent(this, FlexibleExampleActivity.class));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/cloudipsp/android/demo/SimpleExampleActivity.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android.demo;
2 |
3 | import android.os.Bundle;
4 | import android.widget.EditText;
5 |
6 | import com.cloudipsp.android.Card;
7 | import com.cloudipsp.android.CardInputView;
8 |
9 | public class SimpleExampleActivity extends BaseExampleActivity {
10 | private CardInputView cardInput;
11 |
12 | @Override
13 | protected void onCreate(Bundle savedInstanceState) {
14 | super.onCreate(savedInstanceState);
15 |
16 | cardInput = findViewById(R.id.card_input);
17 | if (BuildConfig.DEBUG) {
18 | cardInput.setHelpedNeeded(true);
19 | }
20 | }
21 |
22 | @Override
23 | protected int getLayoutResId() {
24 | return R.layout.activity_simple_example;
25 | }
26 |
27 | @Override
28 | protected Card getCard() {
29 | return cardInput.confirm(new CardInputView.ConfirmationErrorHandler() {
30 | @Override
31 | public void onCardInputErrorClear(CardInputView view, EditText editText) {
32 |
33 | }
34 |
35 | @Override
36 | public void onCardInputErrorCatched(CardInputView view, EditText editText, String error) {
37 |
38 | }
39 | });
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudipsp/android-sdk/00c21ef23c1f217d900a9a665b60d0e54f5d7c5b/app/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudipsp/android-sdk/00c21ef23c1f217d900a9a665b60d0e54f5d7c5b/app/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudipsp/android-sdk/00c21ef23c1f217d900a9a665b60d0e54f5d7c5b/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudipsp/android-sdk/00c21ef23c1f217d900a9a665b60d0e54f5d7c5b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_flexible_example.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
14 |
15 |
20 |
21 |
28 |
29 |
34 |
35 |
40 |
41 |
46 |
47 |
53 |
54 |
59 |
60 |
65 |
66 |
70 |
71 |
75 |
76 |
82 |
83 |
93 |
94 |
99 |
100 |
105 |
106 |
118 |
119 |
131 |
132 |
133 |
138 |
139 |
146 |
147 |
148 |
149 |
155 |
156 |
160 |
161 |
168 |
169 |
176 |
177 |
178 |
179 |
180 |
185 |
186 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
17 |
18 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_simple_example.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
14 |
15 |
20 |
21 |
28 |
29 |
34 |
35 |
40 |
41 |
46 |
47 |
53 |
54 |
59 |
60 |
65 |
66 |
70 |
71 |
77 |
78 |
82 |
83 |
90 |
91 |
98 |
99 |
100 |
101 |
102 |
107 |
108 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | cloudipsp.com demo
4 | Simple Example
5 | Flexible Example
6 |
7 | Amount:
8 | Currency:
9 | Email:
10 | Description:
11 | GooglePay
12 | Pay by Card
13 | Invalid amount
14 | Invalid email
15 | Enter description
16 | GooglePay is not supported
17 | GooglePay has been canceled
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | buildToolsVersion = '33.0.0'
4 | minSdkVersion = 14
5 | compileSdkVersion = 33
6 | targetSdkVersion = 33
7 | }
8 | repositories {
9 | google()
10 | mavenCentral()
11 | }
12 | dependencies {
13 | classpath 'com.android.tools.build:gradle:4.2.2'
14 | }
15 | }
16 |
17 | def isReleaseBuild() {
18 | return version.contains("SNAPSHOT") == false
19 | }
20 |
21 | allprojects {
22 | version = VERSION_NAME
23 | group = GROUP
24 |
25 | repositories {
26 | google()
27 | mavenCentral()
28 | }
29 | }
30 |
31 | task clean(type: Delete) {
32 | delete rootProject.buildDir
33 | }
34 |
35 |
36 | apply plugin: 'android-reporting'
37 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | VERSION_NAME=1.17.2
2 | VERSION_CODE=42
3 | GROUP=com.cloudipsp
4 |
5 | POM_DESCRIPTION=Library for accepting payments directly from android application's clients.
6 | POM_URL=https://github.com/cloudipsp/android-sdk
7 | POM_SCM_URL=https://github.com/cloudipsp/android-sdk
8 | POM_SCM_CONNECTION=scm:git@github.com:cloudipsp/android-sdk.git
9 | POM_SCM_DEV_CONNECTION=scm:git@github.com:cloudipsp/android-sdk.git
10 | POM_LICENCE_NAME=The MIT License (MIT)
11 | POM_LICENCE_URL=https://opensource.org/licenses/MIT
12 | POM_LICENCE_DIST=repo
13 | POM_DEVELOPER_ID=cloudipsp
14 | POM_DEVELOPER_NAME=Maxim Kozenko
15 |
16 | android.useAndroidX=true
17 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudipsp/android-sdk/00c21ef23c1f217d900a9a665b60d0e54f5d7c5b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Nov 17 22:42:33 EET 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.3-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply from: '../maven_push.gradle'
3 |
4 | def safeExtGet(prop, fallback) {
5 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
6 | }
7 |
8 | android {
9 | compileSdkVersion safeExtGet('compileSdkVersion', 33)
10 | buildToolsVersion safeExtGet('buildToolsVersion', '33.0.0')
11 |
12 | def apiHost
13 | if (project.hasProperty('host')) {
14 | apiHost = project.property('host')
15 | } else {
16 | apiHost = 'https://api.fondy.eu'
17 | }
18 | defaultConfig {
19 | minSdkVersion safeExtGet('minSdkVersion', 10)
20 | targetSdkVersion safeExtGet('targetSdkVersion', 33)
21 | versionCode 1
22 | versionName "1.0"
23 |
24 | buildConfigField 'String', 'API_HOST', "\"$apiHost\""
25 | }
26 | println("API_HOST: $apiHost")
27 | buildTypes {
28 | release {
29 | minifyEnabled false
30 | }
31 | }
32 | }
33 |
34 | dependencies {
35 | compileOnly 'com.google.android.gms:play-services-base:18.1.0'
36 | compileOnly 'com.google.android.gms:play-services-wallet:19.1.0'
37 | }
38 |
--------------------------------------------------------------------------------
/library/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Cloud IPSP Client
2 | POM_ARTIFACT_ID=android
3 | POM_PACKAGING=aar
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/BaseConfirmationErrorHandler.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.widget.EditText;
4 | import android.widget.FrameLayout;
5 |
6 | /**
7 | * Created by vberegovoy on 6/20/17.
8 | */
9 | interface BaseConfirmationErrorHandler {
10 | void onCardInputErrorClear(V view, EditText editText);
11 |
12 | void onCardInputErrorCatched(V view, EditText editText, String error);
13 | }
14 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/Card.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.os.Parcel;
4 | import android.os.Parcelable;
5 |
6 | import java.util.Calendar;
7 |
8 | /**
9 | * Created by vberegovoy on 09.11.15.
10 | */
11 | public final class Card implements Parcelable {
12 | public static final Creator CREATOR = new Creator() {
13 | @Override
14 | public Card createFromParcel(Parcel input) {
15 | return new Card(input);
16 | }
17 |
18 | @Override
19 | public Card[] newArray(int size) {
20 | return new Card[size];
21 | }
22 | };
23 |
24 | static final int INVALID_VALUE = -1;
25 | public static final int SOURCE_FORM = 0;
26 | public static final int SOURCE_NFC = 1;
27 |
28 | public enum Type {
29 | VISA {
30 | @Override
31 | protected boolean is(String cardNumber) {
32 | return cardNumber.charAt(0) == '4';
33 | }
34 | },
35 | MASTERCARD {
36 | @Override
37 | protected boolean is(String cardNumber) {
38 | if (cardNumber.charAt(0) != '5') {
39 | return false;
40 | }
41 | final char a = cardNumber.charAt(1);
42 | return '0' <= a && a <= '5';
43 | }
44 | },
45 | MAESTRO {
46 | @Override
47 | protected boolean is(String cardNumber) {
48 | return cardNumber.charAt(0) == '6';
49 | }
50 | },
51 | UNKNOWN {
52 | @Override
53 | protected boolean is(String cardNumber) {
54 | return true;
55 | }
56 | };
57 |
58 | abstract protected boolean is(String cardNumber);
59 |
60 | private static Type fromCardNumber(String cardNumber) {
61 | for (Type type : values()) {
62 | if (type.is(cardNumber)) {
63 | return type;
64 | }
65 | }
66 | return null;
67 | }
68 | }
69 |
70 | String cardNumber;
71 | int mm;
72 | int yy;
73 | String cvv;
74 | public final int source;
75 |
76 | Card(String cardNumber, String expireMm, String expireYy, String cvv) {
77 | this(cardNumber, expireMm, expireYy, cvv, SOURCE_FORM);
78 | }
79 |
80 | Card(String cardNumber, String expireMm, String expireYy, String cvv, int source) {
81 | setCardNumber(cardNumber);
82 | setExpireMonth(expireMm);
83 | setExpireYear(expireYy);
84 | setCvv(cvv);
85 | this.source = source;
86 | }
87 |
88 | private Card(Parcel input) {
89 | cardNumber = input.readString();
90 | mm = input.readInt();
91 | yy = input.readInt();
92 | cvv = input.readString();
93 | source = input.readInt();
94 | }
95 |
96 | @Override
97 | public int describeContents() {
98 | return 0;
99 | }
100 |
101 | @Override
102 | public void writeToParcel(Parcel output, int flags) {
103 | output.writeString(cardNumber);
104 | output.writeInt(mm);
105 | output.writeInt(yy);
106 | output.writeString(cvv);
107 | output.writeInt(source);
108 | }
109 |
110 | public void setCvv(String value) {
111 | cvv = value;
112 | }
113 |
114 | public void setExpireMonth(String value) {
115 | try {
116 | mm = Integer.valueOf(value);
117 | } catch (Exception e) {
118 | mm = INVALID_VALUE;
119 | }
120 | }
121 |
122 | public void setExpireYear(String value) {
123 | try {
124 | yy = Integer.valueOf(value);
125 | } catch (Exception e) {
126 | yy = INVALID_VALUE;
127 | }
128 | }
129 |
130 | public void setCardNumber(String value) {
131 | cardNumber = value;
132 | }
133 |
134 | public boolean isValidExpireMonth() {
135 | return mm >= 1 && mm <= 12;
136 | }
137 |
138 | private boolean isValidExpireYearValue() {
139 | return yy >= 21 && yy <= 99;
140 | }
141 |
142 | public boolean isValidExpireYear() {
143 | if (!isValidExpireYearValue()) {
144 | return false;
145 | }
146 | final Calendar calendar = Calendar.getInstance();
147 | final int year = calendar.get(Calendar.YEAR) - 2000;
148 | return year <= yy;
149 | }
150 |
151 | public boolean isValidExpireDate() {
152 | if (!isValidExpireMonth()) {
153 | return false;
154 | }
155 | if (!isValidExpireYear()) {
156 | return false;
157 | }
158 |
159 | final Calendar calendar = Calendar.getInstance();
160 | final int year = calendar.get(Calendar.YEAR) - 2000;
161 |
162 | return (yy > year) || (yy >= year && mm >= calendar.get(Calendar.MONTH) + 1);
163 | }
164 |
165 | public boolean isValidCvv() {
166 | if (source == SOURCE_FORM) {
167 | if (cvv == null) {
168 | return false;
169 | }
170 | final int length = cvv.length();
171 | if (CvvUtils.isCvv4Length(cardNumber)) {
172 | return length == 4;
173 | } else {
174 | return length == 3;
175 | }
176 | } else {
177 | return true;
178 | }
179 | }
180 |
181 | private static boolean lunaCheck(String cardNumber) {
182 | final char[] cardChars = cardNumber.toCharArray();
183 |
184 | int sum = 0;
185 | boolean odd = true;
186 | for (int i = cardChars.length - 1; i >= 0; --i) {
187 | final char a = cardChars[i];
188 |
189 | if (!('0' <= a && a <= '9')) {
190 | return false;
191 | }
192 | int num = (a - '0');
193 | odd = !odd;
194 | if (odd) {
195 | num *= 2;
196 | }
197 | if (num > 9) {
198 | num -= 9;
199 | }
200 | sum += num;
201 | }
202 |
203 | return sum % 10 == 0;
204 | }
205 |
206 | public boolean isValidCardNumber() {
207 | if (cardNumber == null) {
208 | return false;
209 | }
210 |
211 | final int length = cardNumber.length();
212 | if (!(12 <= length && length <= 19)) {
213 | return false;
214 | }
215 |
216 | if (!lunaCheck(cardNumber)) {
217 | return false;
218 | }
219 |
220 | return true;
221 | }
222 |
223 | public boolean isValidCard() {
224 | return isValidExpireDate() && isValidCvv() && isValidCardNumber();
225 | }
226 |
227 | public Type getType() {
228 | if (!isValidCardNumber()) {
229 | throw new IllegalStateException("CardNumber should be valid before for getType");
230 | }
231 | return Type.fromCardNumber(cardNumber);
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardCvvEdit.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.content.Context;
4 | import android.text.InputFilter;
5 | import android.util.AttributeSet;
6 | import android.view.inputmethod.EditorInfo;
7 |
8 | /**
9 | * Created by vberegovoy on 6/20/17.
10 | */
11 |
12 | public class CardCvvEdit extends CardInputBase {
13 | public CardCvvEdit(Context context) {
14 | super(context);
15 | init();
16 | }
17 |
18 | public CardCvvEdit(Context context, AttributeSet attrs) {
19 | super(context, attrs);
20 | init();
21 | }
22 |
23 | public CardCvvEdit(Context context, AttributeSet attrs, int defStyleAttr) {
24 | super(context, attrs, defStyleAttr);
25 | init();
26 | }
27 |
28 | private void init() {
29 | setCvv4(false);
30 | setSingleLine();
31 | setInputType(EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD | EditorInfo.TYPE_CLASS_NUMBER);
32 | }
33 |
34 | void setCvv4(boolean enabled) {
35 | setFiltersInternal(new InputFilter[]{new InputFilter.LengthFilter(enabled ? 4 : 3)});
36 | if (!enabled) {
37 | final String cvv = getTextInternal().toString();
38 | if (cvv.length() == 4) {
39 | setTextInternal(cvv.substring(0, 3));
40 | }
41 | }
42 | }
43 |
44 | @Override
45 | protected CharSequence getMaskedValue() {
46 | final int length = getTextInternal().length();
47 | final StringBuilder builder = new StringBuilder();
48 | for (int i = 0; i < length; ++i) {
49 | builder.append('*');
50 | }
51 | return builder.toString();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardDisplay.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | /**
4 | * Created by vberegovoy on 07.09.17.
5 | */
6 |
7 | public interface CardDisplay {
8 | void display(Card card);
9 | }
10 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardExpMmEdit.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.content.Context;
4 | import android.text.InputFilter;
5 | import android.util.AttributeSet;
6 | import android.view.inputmethod.EditorInfo;
7 |
8 | /**
9 | * Created by vberegovoy on 6/20/17.
10 | */
11 |
12 | public class CardExpMmEdit extends CardInputBase {
13 | public CardExpMmEdit(Context context) {
14 | super(context);
15 | init();
16 | }
17 |
18 | public CardExpMmEdit(Context context, AttributeSet attrs) {
19 | super(context, attrs);
20 | init();
21 | }
22 |
23 | public CardExpMmEdit(Context context, AttributeSet attrs, int defStyleAttr) {
24 | super(context, attrs, defStyleAttr);
25 | init();
26 | }
27 |
28 | @Override
29 | protected CharSequence getMaskedValue() {
30 | return getTextInternal().toString();
31 | }
32 |
33 | private void init() {
34 | setFiltersInternal(new InputFilter[]{new InputFilter.LengthFilter(2)});
35 | setInputType(EditorInfo.TYPE_CLASS_NUMBER);
36 | setSingleLine();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardExpYyEdit.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.content.Context;
4 | import android.text.InputFilter;
5 | import android.util.AttributeSet;
6 | import android.view.inputmethod.EditorInfo;
7 |
8 | /**
9 | * Created by vberegovoy on 6/20/17.
10 | */
11 |
12 | public class CardExpYyEdit extends CardInputBase {
13 | public CardExpYyEdit(Context context) {
14 | super(context);
15 | init();
16 | }
17 |
18 | public CardExpYyEdit(Context context, AttributeSet attrs) {
19 | super(context, attrs);
20 | init();
21 | }
22 |
23 | public CardExpYyEdit(Context context, AttributeSet attrs, int defStyleAttr) {
24 | super(context, attrs, defStyleAttr);
25 | init();
26 | }
27 |
28 | private void init() {
29 | setFiltersInternal(new InputFilter[]{new InputFilter.LengthFilter(2)});
30 | setInputType(EditorInfo.TYPE_CLASS_NUMBER);
31 | setSingleLine();
32 | }
33 |
34 | @Override
35 | protected CharSequence getMaskedValue() {
36 | return getTextInternal().toString();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardInputBase.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.content.Context;
4 | import android.text.Editable;
5 | import android.text.InputFilter;
6 | import android.text.TextWatcher;
7 | import android.util.AttributeSet;
8 | import android.util.Log;
9 | import android.widget.EditText;
10 |
11 | /**
12 | * Created by vberegovoy on 6/20/17.
13 | */
14 |
15 | abstract class CardInputBase extends EditText {
16 | public CardInputBase(Context context) {
17 | super(context);
18 | }
19 |
20 | public CardInputBase(Context context, AttributeSet attrs) {
21 | super(context, attrs);
22 | }
23 |
24 | public CardInputBase(Context context, AttributeSet attrs, int defStyleAttr) {
25 | super(context, attrs, defStyleAttr);
26 | }
27 |
28 | @Override
29 | public void addTextChangedListener(TextWatcher watcher) {
30 | if (isParentCall()) {
31 | super.addTextChangedListener(watcher);
32 | } else {
33 | if (Cloudipsp.strictUiBlocking) {
34 | throw new RuntimeException("unsupported operation");
35 | }
36 | }
37 | }
38 |
39 | @Override
40 | public void setFilters(InputFilter[] filters) {
41 | if (isParentCall()) {
42 | super.setFilters(filters);
43 | } else {
44 | if (Cloudipsp.strictUiBlocking) {
45 | throw new RuntimeException("unsupported operation");
46 | }
47 | }
48 | }
49 |
50 | protected void setFiltersInternal(InputFilter[] filters) {
51 | super.setFilters(filters);
52 | }
53 |
54 | void addTextChangedListenerInternal(TextWatcher watcher) {
55 | super.addTextChangedListener(watcher);
56 | }
57 |
58 | protected void setTextInternal(CharSequence text) {
59 | super.setText(text, BufferType.NORMAL);
60 | }
61 |
62 | protected CharSequence getTextInternal() {
63 | return super.getText();
64 | }
65 |
66 | @Override
67 | @Deprecated
68 | public Editable getText() {
69 | if (isParentCall()) {
70 | return super.getText();
71 | } else {
72 | if (Cloudipsp.strictUiBlocking) {
73 | throw new RuntimeException("unsupported operation");
74 | } else {
75 | return Editable.Factory.getInstance().newEditable(getMaskedValue());
76 | }
77 | }
78 | }
79 |
80 | protected abstract CharSequence getMaskedValue();
81 |
82 | @Override
83 | @Deprecated
84 | public void setText(CharSequence text, BufferType type) {
85 | if (isParentCall()) {
86 | super.setText(text, type);
87 | } else {
88 | if (Cloudipsp.strictUiBlocking) {
89 | throw new RuntimeException("unsupported operation");
90 | }
91 | }
92 | }
93 |
94 | private boolean isParentCall() {
95 | final StackTraceElement[] stack = new Throwable().getStackTrace();
96 |
97 | return isParentClass(stack[2].getClassName()) ||
98 | isParentClass(stack[3].getClassName()) ||
99 | isParentClass(stack[4].getClassName());
100 | }
101 |
102 | private boolean isParentClass(String className) {
103 | return className.startsWith("android.widget.")
104 | || className.startsWith("android.view.")
105 | || className.startsWith("com.google.android.material.textfield.TextInputLayout")
106 | || className.equals("android.support.design.widget.TextInputLayout")
107 | || className.equals("com.huawei.android.hwcontrol.HwEditor")
108 | || className.startsWith("com.letv.leui.text.");
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardInputLayout.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.content.Context;
4 | import android.os.Parcelable;
5 | import android.text.Editable;
6 | import android.text.TextWatcher;
7 | import android.util.AttributeSet;
8 | import android.util.SparseArray;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.FrameLayout;
12 |
13 | import java.util.ArrayList;
14 |
15 | /**
16 | * Created by vberegovoy on 6/20/17.
17 | */
18 |
19 | public final class CardInputLayout extends FrameLayout implements CardDisplay {
20 | private CardNumberEdit editCardNumber;
21 | private CardExpMmEdit editMm;
22 | private CardExpYyEdit editYy;
23 | CardCvvEdit editCvv;
24 |
25 | private Card displayedCard;
26 |
27 | public CardInputLayout(Context context) {
28 | super(context);
29 | }
30 |
31 | public CardInputLayout(Context context, AttributeSet attrs) {
32 | super(context, attrs);
33 | }
34 |
35 | public CardInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
36 | super(context, attrs, defStyleAttr);
37 | }
38 |
39 | @Override
40 | protected void onFinishInflate() {
41 | super.onFinishInflate();
42 | editCardNumber = findOne(CardNumberEdit.class);
43 | editMm = findOne(CardExpMmEdit.class);
44 | editYy = findOne(CardExpYyEdit.class);
45 | editCvv = findOne(CardCvvEdit.class);
46 | editCardNumber.addTextChangedListenerInternal(new TextWatcher() {
47 | @Override
48 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
49 | }
50 |
51 | @Override
52 | public void onTextChanged(CharSequence s, int start, int before, int count) {
53 | }
54 |
55 | @Override
56 | public void afterTextChanged(Editable s) {
57 | editCvv.setCvv4(CvvUtils.isCvv4Length(s.toString()));
58 | }
59 | });
60 | }
61 |
62 | @Override
63 | protected void dispatchSaveInstanceState(SparseArray container) {
64 | setRealIds();
65 | super.dispatchSaveInstanceState(container);
66 | setFakeIds();
67 | }
68 |
69 | @Override
70 | protected void dispatchRestoreInstanceState(SparseArray container) {
71 | setRealIds();
72 | super.dispatchRestoreInstanceState(container);
73 | setFakeIds();
74 | }
75 |
76 | public void setCardNumberFormatting(boolean enable) {
77 | editCardNumber.setCardNumberFormatting(enable);
78 | }
79 |
80 | public void display(Card card) {
81 | boolean enabled = true;
82 | if (card == null) {
83 | editCardNumber.setTextInternal("");
84 | editMm.setTextInternal("");
85 | editYy.setTextInternal("");
86 | editCvv.setTextInternal("");
87 | displayedCard = null;
88 | } else if (card.source == Card.SOURCE_NFC) {
89 | enabled = false;
90 | editCardNumber.setTextInternal(formattedCardNumber(card.cardNumber));
91 | editMm.setTextInternal(String.valueOf(card.mm));
92 | editYy.setTextInternal(String.valueOf(card.yy));
93 | editCvv.setTextInternal("");
94 | displayedCard = card;
95 | }
96 | editCardNumber.setEnabled(enabled);
97 | editMm.setEnabled(enabled);
98 | editYy.setEnabled(enabled);
99 | editCvv.setEnabled(enabled);
100 | }
101 |
102 | static String formattedCardNumber(String cardNumber) {
103 | String masked = cardNumber.substring(0, 4) + " " + cardNumber.substring(4, 6);
104 |
105 | masked += "** **** ";
106 | masked += cardNumber.substring(12, 16);
107 |
108 | return masked;
109 | }
110 |
111 | void setHelpCard(String card, String expMm, String expYy, String cvv) {
112 | editCardNumber.setTextInternal(card);
113 | editMm.setTextInternal(expMm);
114 | editYy.setTextInternal(expYy);
115 | editCvv.setTextInternal(cvv);
116 | editCardNumber.setEnabled(true);
117 | editMm.setEnabled(true);
118 | editYy.setEnabled(true);
119 | editCvv.setEnabled(true);
120 | }
121 |
122 | public Card confirm(ConfirmationErrorHandler handler) {
123 | if (displayedCard != null) {
124 | return null;
125 | }
126 |
127 | handler.onCardInputErrorClear(this, editCardNumber);
128 | handler.onCardInputErrorClear(this, editMm);
129 | handler.onCardInputErrorClear(this, editYy);
130 | handler.onCardInputErrorClear(this, editCvv);
131 |
132 | final Card card = new Card
133 | (
134 | editCardNumber.getTextInternal().toString(),
135 | editMm.getTextInternal().toString(),
136 | editYy.getTextInternal().toString(),
137 | editCvv.getTextInternal().toString()
138 | );
139 |
140 | if (!card.isValidCardNumber()) {
141 | handler.onCardInputErrorCatched(this, editCardNumber, getContext().getString(R.string.e_invalid_card_number));
142 | } else if (!card.isValidExpireMonth()) {
143 | handler.onCardInputErrorCatched(this, editMm, getContext().getString(R.string.e_invalid_mm));
144 | } else if (!card.isValidExpireYear()) {
145 | handler.onCardInputErrorCatched(this, editYy, getContext().getString(R.string.e_invalid_yy));
146 | } else if (!card.isValidExpireDate()) {
147 | handler.onCardInputErrorCatched(this, editYy, getContext().getString(R.string.e_invalid_date));
148 | } else if (!card.isValidCvv()) {
149 | handler.onCardInputErrorCatched(this, editCvv, getContext().getString(R.string.e_invalid_cvv));
150 | } else {
151 | return card;
152 | }
153 | return null;
154 | }
155 |
156 | private void setFakeIds() {
157 | if (editCardNumber == null) {
158 | return;
159 | }
160 | editCardNumber.setId(View.NO_ID);
161 | editMm.setId(View.NO_ID);
162 | editYy.setId(View.NO_ID);
163 | editCvv.setId(View.NO_ID);
164 | }
165 |
166 | private void setRealIds() {
167 | if (editCardNumber == null) {
168 | return;
169 | }
170 | editCardNumber.setId(R.id.edit_card_number);
171 | editMm.setId(R.id.edit_mm);
172 | editYy.setId(R.id.edit_yy);
173 | editCvv.setId(R.id.edit_cvv);
174 | }
175 |
176 | private V findOne(Class clazz) {
177 | final ArrayList views = new ArrayList();
178 | find(clazz, this, views);
179 | final int count = views.size();
180 | if (count == 0) {
181 | throw new RuntimeException(getClass().getName() + " should contains " + clazz.getName());
182 | }
183 | if (count > 1) {
184 | throw new RuntimeException(getClass().getName() + " should contains only one view " + clazz.getName()+". "+
185 | "Now here "+count +" instances of "+clazz.getName()+".");
186 | }
187 | return views.get(0);
188 | }
189 |
190 | private static void find(Class clazz, ViewGroup parent, ArrayList out) {
191 | final int childCount = parent.getChildCount();
192 | for (int i = 0; i < childCount; ++i) {
193 | final View child = parent.getChildAt(i);
194 | if (child instanceof ViewGroup) {
195 | find(clazz, (ViewGroup) child, out);
196 | } else if (child.getClass().equals(clazz)) {
197 | out.add((V) child);
198 | }
199 | }
200 | }
201 |
202 | public interface ConfirmationErrorHandler extends BaseConfirmationErrorHandler {
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardInputView.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.KeyEvent;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.inputmethod.EditorInfo;
9 | import android.widget.EditText;
10 | import android.widget.FrameLayout;
11 | import android.widget.TextView;
12 |
13 | /**
14 | * Created by vberegovoy on 22.12.15.
15 | */
16 | public final class CardInputView extends FrameLayout implements CardDisplay {
17 | private static final ConfirmationErrorHandler DEFAULT_CONFIRMATION_ERROR_HANDLER = new ConfirmationErrorHandler() {
18 | @Override
19 | public void onCardInputErrorClear(CardInputView view, EditText editText) {
20 | editText.setError(null);
21 | }
22 |
23 | @Override
24 | public void onCardInputErrorCatched(CardInputView view, EditText editText, String error) {
25 | editText.setError(error);
26 | editText.requestFocus();
27 | }
28 | };
29 | private static final String[] HELP_CARDS = new String[]{"4444555566661111", "4444111166665555", "4444555511116666", "4444111155556666"};
30 |
31 | private final CardInputLayout view;
32 | private CompletionListener completionListener;
33 |
34 | private int currentHelpCard = 0;
35 | private boolean helpedNeeded = false;
36 |
37 | public CardInputView(Context context) {
38 | this(context, null);
39 | }
40 |
41 | public CardInputView(Context context, AttributeSet attrs) {
42 | this(context, attrs, 0);
43 | }
44 |
45 | public CardInputView(Context context, AttributeSet attrs, int defStyleAttr) {
46 | super(context, attrs, defStyleAttr);
47 | view = (CardInputLayout) LayoutInflater.from(context).inflate(R.layout.com_cloudipsp_android_card_input_view, null);
48 | view.findViewById(R.id.btn_help_next_card).setOnClickListener(new OnClickListener() {
49 | @Override
50 | public void onClick(View v) {
51 | nextHelpCard();
52 | }
53 | });
54 | addView(view);
55 | setCompletionListener(null);
56 | }
57 |
58 | public void setCompletionListener(CompletionListener listener) {
59 | setCompletionListener(listener, EditorInfo.IME_ACTION_DONE);
60 | }
61 |
62 | public void setCompletionListener(final CompletionListener listener, final int lastViewImeOptions) {
63 | completionListener = listener;
64 |
65 | view.editCvv.setImeOptions(lastViewImeOptions);
66 | if (completionListener != null) {
67 | view.editCvv.setOnEditorActionListener(new TextView.OnEditorActionListener() {
68 | @Override
69 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
70 | if (lastViewImeOptions == actionId) {
71 | listener.onCompleted(CardInputView.this);
72 | return true;
73 | }
74 | return false;
75 | }
76 | });
77 | }
78 | }
79 |
80 | private void nextHelpCard() {
81 | if (helpedNeeded) {
82 | currentHelpCard %= HELP_CARDS.length;
83 | view.setHelpCard(HELP_CARDS[currentHelpCard++], "12", "29", "111");
84 | }
85 | }
86 |
87 | public void setHelpedNeeded(boolean value) {
88 | helpedNeeded = value;
89 | }
90 |
91 | public void setCardNumberFormatting(boolean enable) {
92 | view.setCardNumberFormatting(enable);
93 | }
94 |
95 | public boolean isHelpedNeeded() {
96 | return helpedNeeded;
97 | }
98 |
99 | public void display(Card card) {
100 | view.display(card);
101 | }
102 |
103 | public Card confirm() {
104 | return confirm(DEFAULT_CONFIRMATION_ERROR_HANDLER);
105 | }
106 |
107 | public Card confirm(final ConfirmationErrorHandler handler) {
108 | return view.confirm(new CardInputLayout.ConfirmationErrorHandler() {
109 | @Override
110 | public void onCardInputErrorClear(CardInputLayout view, EditText editText) {
111 | handler.onCardInputErrorClear(CardInputView.this, editText);
112 | }
113 |
114 | @Override
115 | public void onCardInputErrorCatched(CardInputLayout view, EditText editText, String error) {
116 | handler.onCardInputErrorCatched(CardInputView.this, editText, error);
117 | }
118 | });
119 | }
120 |
121 | public interface ConfirmationErrorHandler extends BaseConfirmationErrorHandler {
122 | }
123 |
124 | public interface CompletionListener {
125 | void onCompleted(CardInputView view);
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CardNumberEdit.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.graphics.Paint;
6 | import android.text.Editable;
7 | import android.text.InputFilter;
8 | import android.text.Spannable;
9 | import android.text.TextWatcher;
10 | import android.text.style.ReplacementSpan;
11 | import android.util.AttributeSet;
12 | import android.view.inputmethod.EditorInfo;
13 |
14 | import androidx.annotation.NonNull;
15 |
16 | /**
17 | * Created by vberegovoy on 22.12.15.
18 | */
19 | public final class CardNumberEdit extends CardInputBase {
20 | private static final int MIN_NUMBER_LENGTH = 16;
21 | private static final int MAX_NUMBER_LENGTH = 19;
22 | private CreditCardFormatTextWatcher formatTextWatcher;
23 |
24 | public CardNumberEdit(Context context) {
25 | super(context);
26 | init();
27 | }
28 |
29 | public CardNumberEdit(Context context, AttributeSet attrs) {
30 | super(context, attrs);
31 | init();
32 | }
33 |
34 | public CardNumberEdit(Context context, AttributeSet attrs, int defStyleAttr) {
35 | super(context, attrs, defStyleAttr);
36 | init();
37 | }
38 |
39 | private void init() {
40 | setRawInputType(EditorInfo.TYPE_CLASS_NUMBER);
41 | setCardNumberFormatting(true);
42 | setFiltersInternal(new InputFilter[]{new InputFilter.LengthFilter(MAX_NUMBER_LENGTH)});
43 | setSingleLine();
44 | }
45 |
46 | @Override
47 | protected CharSequence getMaskedValue() {
48 | final CharSequence cardNumber = getTextInternal();
49 | final int length = cardNumber.length();
50 |
51 | if (length >= MIN_NUMBER_LENGTH) {
52 | final StringBuilder sb = new StringBuilder();
53 | int i = 0;
54 | while (i < 6) {
55 | sb.append(cardNumber.charAt(i++));
56 | }
57 | final int tail = length - 4;
58 | while (i++ < tail) {
59 | sb.append('*');
60 | }
61 | while (i < length) {
62 | sb.append(cardNumber.charAt(i++));
63 | }
64 | return sb.toString();
65 | }
66 | return "";
67 | }
68 |
69 | public void setCardNumberFormatting(boolean enable) {
70 | if (enable) {
71 | if (formatTextWatcher == null) {
72 | formatTextWatcher = new CreditCardFormatTextWatcher(10, MAX_NUMBER_LENGTH);
73 | }
74 | removeTextChangedListener(formatTextWatcher);
75 | addTextChangedListenerInternal(formatTextWatcher);
76 | } else {
77 | if (formatTextWatcher != null) {
78 | removeTextChangedListener(formatTextWatcher);
79 | }
80 | }
81 | }
82 |
83 | private static class CreditCardFormatTextWatcher implements TextWatcher {
84 | private final int maxLength;
85 | private final int paddingPx;
86 |
87 | private boolean internalStopFormatFlag;
88 |
89 | private CreditCardFormatTextWatcher(int paddingPx, int maxLength) {
90 | this.paddingPx = paddingPx;
91 | this.maxLength = maxLength;
92 | }
93 |
94 | @Override
95 | public void beforeTextChanged(CharSequence s, int start, int count, int after) {
96 | }
97 |
98 | @Override
99 | public void onTextChanged(CharSequence s, int start, int before, int count) {
100 | }
101 |
102 | @Override
103 | public void afterTextChanged(Editable s) {
104 | if (internalStopFormatFlag) {
105 | return;
106 | }
107 | internalStopFormatFlag = true;
108 | formatCardNumber(s, paddingPx, maxLength);
109 | internalStopFormatFlag = false;
110 | }
111 |
112 | private static void formatCardNumber(@NonNull Editable ccNumber, int paddingPx, int maxLength) {
113 | final int textLength = ccNumber.length();
114 | final PaddingRightSpan[] spans = ccNumber.getSpans(0, ccNumber.length(), PaddingRightSpan.class);
115 |
116 | for (PaddingRightSpan span : spans) {
117 | ccNumber.removeSpan(span);
118 | }
119 |
120 | if (maxLength > 0 && textLength > maxLength - 1) {
121 | ccNumber.replace(maxLength, textLength, "");
122 | }
123 |
124 | for (int i = 1; i <= ((textLength - 1) / 4); i++) {
125 | final int end = i * 4;
126 | final int start = end - 1;
127 | final PaddingRightSpan marginSpan = new PaddingRightSpan(paddingPx);
128 |
129 | ccNumber.setSpan(marginSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
130 | }
131 | }
132 |
133 | public static class PaddingRightSpan extends ReplacementSpan {
134 | private final int padding;
135 |
136 | private PaddingRightSpan(int padding) {
137 | this.padding = padding;
138 | }
139 |
140 | @Override
141 | public int getSize(@NonNull Paint paint, CharSequence text,
142 | int start, int end, Paint.FontMetricsInt fm) {
143 | final float[] widths = new float[end - start];
144 | paint.getTextWidths(text, start, end, widths);
145 |
146 | int sum = padding;
147 | for (float width : widths) {
148 | sum += width;
149 | }
150 | return sum;
151 | }
152 |
153 | @Override
154 | public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
155 | float x, int top, int y, int bottom, @NonNull Paint paint) {
156 | canvas.drawText(text, start, end, x, y, paint);
157 | }
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/Cloudipsp.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.os.Build;
7 | import android.os.Handler;
8 | import android.os.Looper;
9 | import android.text.TextUtils;
10 | import android.util.Log;
11 |
12 | import com.google.android.gms.common.ConnectionResult;
13 | import com.google.android.gms.common.GoogleApiAvailability;
14 | import com.google.android.gms.common.api.Status;
15 | import com.google.android.gms.wallet.AutoResolveHelper;
16 | import com.google.android.gms.wallet.PaymentData;
17 | import com.google.android.gms.wallet.PaymentDataRequest;
18 | import com.google.android.gms.wallet.PaymentsClient;
19 | import com.google.android.gms.wallet.Wallet;
20 | import com.google.android.gms.wallet.WalletConstants;
21 |
22 | import org.json.JSONArray;
23 | import org.json.JSONException;
24 | import org.json.JSONObject;
25 |
26 | import java.io.BufferedReader;
27 | import java.io.FileNotFoundException;
28 | import java.io.IOException;
29 | import java.io.InputStream;
30 | import java.io.InputStreamReader;
31 | import java.io.OutputStream;
32 | import java.net.HttpURLConnection;
33 | import java.net.URL;
34 | import java.net.URLEncoder;
35 | import java.security.cert.CertPathValidatorException;
36 | import java.text.SimpleDateFormat;
37 | import java.util.Date;
38 | import java.util.Locale;
39 | import java.util.TimeZone;
40 | import java.util.TreeMap;
41 |
42 | import javax.net.ssl.HostnameVerifier;
43 | import javax.net.ssl.HttpsURLConnection;
44 | import javax.net.ssl.SSLHandshakeException;
45 | import javax.net.ssl.SSLSession;
46 | import javax.net.ssl.SSLSocketFactory;
47 |
48 | /**
49 | * Created by vberegovoy on 09.11.15.
50 | */
51 | public final class Cloudipsp {
52 | private static final String HOST = BuildConfig.API_HOST;
53 | private static final String URL_CALLBACK = "http://callback";
54 | private static final SimpleDateFormat DATE_AND_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.US);
55 | private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy", Locale.US);
56 | private static final SSLSocketFactory tlsSocketFactory = Tls12SocketFactory.getInstance();
57 | static boolean strictUiBlocking = true;
58 |
59 | static {
60 | DATE_AND_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
61 | }
62 |
63 | private static final Handler sMain = new Handler(Looper.getMainLooper());
64 |
65 | public final int merchantId;
66 | private final CloudipspView cloudipspView;
67 |
68 | public Cloudipsp(int merchantId, CloudipspView cloudipspView) {
69 | this.merchantId = merchantId;
70 | this.cloudipspView = cloudipspView;
71 | }
72 |
73 | public interface Callback {
74 | void onPaidFailure(Exception e);
75 | }
76 |
77 | public interface PayCallback extends Callback {
78 | void onPaidProcessed(Receipt receipt);
79 | }
80 |
81 | public interface GooglePayCallback extends Callback {
82 | void onGooglePayInitialized(GooglePayCall result);
83 | }
84 |
85 | public static boolean supportsGooglePay(Context context) {
86 | if (!isGooglePayRuntimeProvided()) {
87 | return false;
88 | }
89 |
90 | return GoogleApiAvailability
91 | .getInstance()
92 | .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS;
93 | }
94 |
95 | public void pay(final Card card, final Order order, PayCallback callback) {
96 | if (!card.isValidCard()) {
97 | throw new IllegalArgumentException("Card should be valid");
98 | }
99 |
100 | new PayTask(callback) {
101 | @Override
102 | public void runInTry() throws java.lang.Exception {
103 | final String token = getToken(order, card);
104 | final Checkout checkout = checkout(card, token, order.email, URL_CALLBACK);
105 | payContinue(token, checkout, callback);
106 | }
107 | }.start();
108 | }
109 |
110 | public void payToken(final Card card, final String token, PayCallback callback) {
111 | if (!card.isValidCard()) {
112 | throw new IllegalArgumentException("Card should be valid");
113 | }
114 |
115 | new PayTask(callback) {
116 | @Override
117 | public void runInTry() throws java.lang.Exception {
118 | final JSONObject receipt = ajaxInfo(token);
119 | final JSONObject orderData = receipt.getJSONObject("order_data");
120 | final Checkout checkout = checkout(card, token, orderData.optString("email", null), receipt.getString("response_url"));
121 | payContinue(token, checkout, callback);
122 | }
123 | }.start();
124 | }
125 |
126 | private static boolean isGooglePayRuntimeProvided() {
127 | try {
128 | Class.forName("com.google.android.gms.common.GoogleApiAvailability");
129 | Class.forName("com.google.android.gms.wallet.PaymentDataRequest");
130 | return true;
131 | } catch (ClassNotFoundException e) {
132 | return false;
133 | }
134 | }
135 |
136 | public void googlePayInitialize(final String token,
137 | final Activity activity,
138 | final int requestCode,
139 | final GooglePayCallback googlePayCallback) {
140 | googlePayInitialize(activity, requestCode, googlePayCallback, new GooglePayMetaInfoProvider() {
141 | @Override
142 | public GooglePayMetaInfo getGooglePayMetaInfo() throws java.lang.Exception {
143 | final Receipt receipt = order(token);
144 | return new GooglePayMetaInfo(token, null, receipt.amount, receipt.currency, receipt.responseUrl);
145 | }
146 | });
147 | }
148 |
149 | public void googlePayInitialize(final Order order,
150 | final Activity activity,
151 | final int requestCode,
152 | final GooglePayCallback googlePayCallback) {
153 | googlePayInitialize(activity, requestCode, googlePayCallback, new GooglePayMetaInfoProvider() {
154 | @Override
155 | public GooglePayMetaInfo getGooglePayMetaInfo() throws java.lang.Exception {
156 | return new GooglePayMetaInfo(null, order, order.amount, order.currency, URL_CALLBACK);
157 | }
158 | });
159 | }
160 |
161 | private static class GooglePayMetaInfo {
162 | private final String token;
163 | private final Order order;
164 | private final int amount;
165 | private final String currency;
166 | private final String callbackUrl;
167 |
168 | private GooglePayMetaInfo(String token, Order order, int amount, String currency, String callbackUrl) {
169 | this.token = token;
170 | this.order = order;
171 | this.amount = amount;
172 | this.currency = currency;
173 | this.callbackUrl = callbackUrl;
174 | }
175 | }
176 |
177 | private interface GooglePayMetaInfoProvider {
178 | GooglePayMetaInfo getGooglePayMetaInfo() throws java.lang.Exception;
179 | }
180 |
181 | private void googlePayInitialize(final Activity activity,
182 | final int requestCode,
183 | final GooglePayCallback googlePayCallback,
184 | final GooglePayMetaInfoProvider metaInfoProvider) {
185 |
186 | if (!isGooglePayRuntimeProvided()) {
187 | return;
188 | }
189 |
190 | new Task(new GooglePayCallback() {
191 | @Override
192 | public void onGooglePayInitialized(final GooglePayCall result) {
193 | sMain.post(new Runnable() {
194 | @Override
195 | public void run() {
196 | googlePayCallback.onGooglePayInitialized(result);
197 | }
198 | });
199 | }
200 |
201 | @Override
202 | public void onPaidFailure(final Exception e) {
203 | sMain.post(new Runnable() {
204 | @Override
205 | public void run() {
206 | googlePayCallback.onPaidFailure(e);
207 | }
208 | });
209 | }
210 | }) {
211 | @Override
212 | public void runInTry() throws java.lang.Exception {
213 | final GooglePayMetaInfo metaInfo = metaInfoProvider.getGooglePayMetaInfo();
214 | final GooglePayMerchantConfig googlePayConfig = googlePayMerchantConfig(metaInfo);
215 | final PaymentDataRequest request = PaymentDataRequest.fromJson(googlePayConfig.data.toString());
216 |
217 | final PaymentsClient paymentsClient = Wallet.getPaymentsClient(activity,
218 | new Wallet.WalletOptions.Builder()
219 | .setEnvironment(googlePayConfig.data.getString("environment").equals("PRODUCTION")
220 | ? WalletConstants.ENVIRONMENT_PRODUCTION
221 | : WalletConstants.ENVIRONMENT_TEST
222 | )
223 | .build());
224 |
225 | callback.onGooglePayInitialized(new GooglePayCall(
226 | metaInfo.token,
227 | metaInfo.order,
228 | metaInfo.callbackUrl,
229 | googlePayConfig.paymentSystem
230 | ));
231 |
232 | AutoResolveHelper.resolveTask(
233 | paymentsClient.loadPaymentData(request),
234 | activity,
235 | requestCode);
236 | }
237 | }.start();
238 | }
239 |
240 | private static class GooglePayMerchantConfig {
241 | public final String paymentSystem;
242 | public final JSONObject data;
243 |
244 | private GooglePayMerchantConfig(String paymentSystem, JSONObject data) {
245 | this.paymentSystem = paymentSystem;
246 | this.data = data;
247 | }
248 | }
249 |
250 | private GooglePayMerchantConfig googlePayMerchantConfig(GooglePayMetaInfo metaInfo) throws java.lang.Exception {
251 | final TreeMap mobilePayRequest = new TreeMap<>();
252 | mobilePayRequest.put("merchant_id", merchantId);
253 | if (metaInfo.token == null) {
254 | mobilePayRequest.put("amount", metaInfo.amount);
255 | mobilePayRequest.put("currency", metaInfo.currency);
256 | } else {
257 | mobilePayRequest.put("token", metaInfo.token);
258 | }
259 |
260 | final JSONObject mobilePayResponse = callJson("/api/checkout/ajax/mobile_pay", mobilePayRequest);
261 | if (mobilePayResponse.has("error_message")) {
262 | handleResponseError(mobilePayResponse);
263 | }
264 | final String paymentSystem = mobilePayResponse.getString("payment_system");
265 |
266 | final JSONArray methodsJson = mobilePayResponse.getJSONArray("methods");
267 | JSONObject data = null;
268 | for (int i = methodsJson.length() - 1; i >= 0; --i) {
269 | final JSONObject methodJson = methodsJson.getJSONObject(i);
270 | if ("https://google.com/pay".equals(methodJson.getString("supportedMethods"))) {
271 | data = methodJson.getJSONObject("data");
272 | break;
273 | }
274 | }
275 | if (data == null) {
276 | throw new Exception.GooglePayUnsupported();
277 | }
278 | return new GooglePayMerchantConfig(paymentSystem, data);
279 | }
280 |
281 | public boolean googlePayComplete(int resultCode, Intent data, final GooglePayCall googlePayCall, PayCallback payCallback) {
282 | if (Activity.RESULT_CANCELED == resultCode) {
283 | return false;
284 | }
285 | if (data == null) {
286 | throw new NullPointerException("data should be not null");
287 | }
288 | if (googlePayCall == null) {
289 | throw new NullPointerException("googlePayCall should be not null");
290 | }
291 | if (payCallback == null) {
292 | throw new NullPointerException("payCallback should be not null");
293 | }
294 | if (Activity.RESULT_OK == resultCode) {
295 | final PaymentData paymentData = PaymentData.getFromIntent(data);
296 | new PayTask(payCallback) {
297 | @Override
298 | public void runInTry() throws java.lang.Exception {
299 | if (googlePayCall.order != null) {
300 | final String token = getToken(googlePayCall.order, null);
301 | final Checkout checkout = checkoutGooglePay(
302 | token,
303 | googlePayCall.paymentSystem,
304 | googlePayCall.order.email,
305 | googlePayCall.callbackUrl,
306 | paymentData
307 | );
308 | payContinue(token, checkout, callback);
309 | } else if (googlePayCall.token != null) {
310 | final Checkout checkout = checkoutGooglePay(
311 | googlePayCall.token,
312 | googlePayCall.paymentSystem,
313 | null,
314 | googlePayCall.callbackUrl,
315 | paymentData
316 | );
317 | payContinue(googlePayCall.token, checkout, callback);
318 | }
319 | }
320 | }.start();
321 | } else if (AutoResolveHelper.RESULT_ERROR == resultCode) {
322 | final Status status = AutoResolveHelper.getStatusFromIntent(data);
323 | payCallback.onPaidFailure(new Exception.GooglePayFailure(status));
324 | }
325 | return true;
326 | }
327 |
328 | private interface RunInTry {
329 | void runInTry() throws java.lang.Exception;
330 | }
331 |
332 | private static void runInTry(RunInTry runInTry, Callback callback) {
333 | try {
334 | runInTry.runInTry();
335 | } catch (CertPathValidatorException | SSLHandshakeException e) {
336 | callback.onPaidFailure(new Exception.NetworkSecurity(e.getMessage()));
337 | } catch (FileNotFoundException e) {
338 | callback.onPaidFailure(new Exception.ServerInternalError(e));
339 | } catch (IOException e) {
340 | callback.onPaidFailure(new Exception.NetworkAccess(e.getMessage()));
341 | } catch (Exception e) {
342 | callback.onPaidFailure(e);
343 | } catch (JSONException e) {
344 | callback.onPaidFailure(new Exception.IllegalServerResponse(e));
345 | } catch (java.lang.Exception e) {
346 | callback.onPaidFailure(new Exception.Unknown(e));
347 | }
348 | }
349 |
350 | private abstract static class Task extends Thread implements RunInTry {
351 | final C callback;
352 |
353 | private Task(C callback) {
354 | this.callback = callback;
355 | }
356 |
357 | @Override
358 | public final void run() {
359 | Cloudipsp.runInTry(this, callback);
360 | }
361 | }
362 |
363 | private abstract static class PayTask extends Task {
364 | PayTask(final PayCallback payCallback) {
365 | super(new PayCallback() {
366 | @Override
367 | public void onPaidProcessed(final Receipt receipt) {
368 | sMain.post(new Runnable() {
369 | @Override
370 | public void run() {
371 | payCallback.onPaidProcessed(receipt);
372 | }
373 | });
374 | }
375 |
376 | @Override
377 | public void onPaidFailure(final Exception e) {
378 | sMain.post(new Runnable() {
379 | @Override
380 | public void run() {
381 | payCallback.onPaidFailure(e);
382 | }
383 | });
384 | }
385 | });
386 | }
387 | }
388 |
389 | public String getToken(Order order, Card card) throws java.lang.Exception {
390 | final TreeMap request = new TreeMap();
391 |
392 | request.put("order_id", order.id);
393 | request.put("merchant_id", String.valueOf(merchantId));
394 | request.put("order_desc", order.description);
395 | request.put("amount", String.valueOf(order.amount));
396 | request.put("currency", order.currency);
397 | if (!TextUtils.isEmpty(order.productId)) {
398 | request.put("product_id", order.productId);
399 | }
400 | if (!TextUtils.isEmpty(order.paymentSystems)) {
401 | request.put("payment_systems", order.paymentSystems);
402 | }
403 | if (!TextUtils.isEmpty(order.defaultPaymentSystem)) {
404 | request.put("default_payment_system", order.defaultPaymentSystem);
405 | }
406 | if (order.lifetime != -1) {
407 | request.put("lifetime", order.lifetime);
408 | }
409 | if (TextUtils.isEmpty(order.merchantData)) {
410 | request.put("merchant_data", "[]");
411 | } else {
412 | request.put("merchant_data", order.merchantData);
413 | }
414 | if (!TextUtils.isEmpty(order.version)) {
415 | request.put("version", order.version);
416 | }
417 | if (!TextUtils.isEmpty(order.serverCallbackUrl)) {
418 | request.put("server_callback_url", order.serverCallbackUrl);
419 | }
420 | if (card != null && Card.SOURCE_NFC == card.source) {
421 | request.put("reservation_data", "eyJ0eXBlIjoibmZjX21vYmlsZSJ9");
422 | } else {
423 | if (!TextUtils.isEmpty(order.reservationData)) {
424 | request.put("reservation_data", order.reservationData);
425 | }
426 | }
427 | if (order.lang != null) {
428 | request.put("lang", order.lang.name());
429 | }
430 | request.put("preauth", order.preauth ? "Y" : "N");
431 | request.put("required_rectoken", order.requiredRecToken ? "Y" : "N");
432 | request.put("verification", order.verification ? "Y" : "N");
433 | request.put("verification_type", order.verificationType.name());
434 | request.putAll(order.arguments);
435 | request.put("response_url", URL_CALLBACK);
436 | request.put("delayed", order.delayed ? "Y" : "N");
437 |
438 | final JSONObject response = call("/api/checkout/token", request);
439 | final String token = response.getString("token");
440 | return token;
441 | }
442 |
443 | private static class Checkout {
444 | static final int WITHOUT_3DS = 0;
445 | static final int WITH_3DS = 1;
446 |
447 | final SendData sendData;
448 | final String url;
449 | final int action;
450 | final String callbackUrl;
451 |
452 | private Checkout(SendData sendData, String url, int action, String callbackUrl) {
453 | this.sendData = sendData;
454 | this.url = url;
455 | this.action = action;
456 | this.callbackUrl = callbackUrl;
457 | }
458 |
459 | private static class SendData {
460 | final String md;
461 | final String paReq;
462 | final String termUrl;
463 |
464 | private SendData(String md, String paReq, String termUrl) {
465 | this.md = md;
466 | this.paReq = paReq;
467 | this.termUrl = termUrl;
468 | }
469 | }
470 | }
471 |
472 | private static Checkout checkout(Card card, String token, String email, String callbackUrl) throws java.lang.Exception {
473 | final TreeMap request = new TreeMap<>();
474 | request.put("card_number", card.cardNumber);
475 | if (card.source == Card.SOURCE_FORM) {
476 | request.put("cvv2", card.cvv);
477 | }
478 | request.put("expiry_date", String.format(Locale.US, "%02d%02d", card.mm, card.yy));
479 | request.put("payment_system", "card");
480 | request.put("token", token);
481 | if (email != null) {
482 | request.put("email", email);
483 | }
484 | return checkoutContinue(request, callbackUrl);
485 | }
486 |
487 | private static Checkout checkoutGooglePay(String token, String paymentSystem,
488 | String email, String callbackUrl,
489 | PaymentData paymentData) throws java.lang.Exception {
490 | final TreeMap request = new TreeMap<>();
491 | request.put("payment_system", paymentSystem);
492 | request.put("token", token);
493 | request.put("email", email);
494 |
495 | final JSONObject data = new JSONObject(paymentData.toJson());
496 | request.put("data", data);
497 |
498 | return checkoutContinue(request, callbackUrl);
499 | }
500 |
501 | private static Checkout checkoutContinue(TreeMap request, String callbackUrl) throws java.lang.Exception {
502 | final JSONObject response = call("/api/checkout/ajax", request);
503 | final String url = response.getString("url");
504 | if (url.startsWith(callbackUrl)) {
505 | return new Checkout(null, url, Checkout.WITHOUT_3DS, callbackUrl);
506 | } else {
507 | final JSONObject sendData = response.getJSONObject("send_data");
508 |
509 | return new Checkout
510 | (
511 | new Checkout.SendData
512 | (
513 | sendData.getString("MD"),
514 | sendData.getString("PaReq"),
515 | sendData.getString("TermUrl")
516 | ),
517 | url,
518 | Checkout.WITH_3DS,
519 | callbackUrl
520 | );
521 | }
522 | }
523 |
524 | private void payContinue(final String token, final Checkout checkout, final PayCallback callback) throws java.lang.Exception {
525 | final RunInTry orderChecker = new RunInTry() {
526 | @Override
527 | public void runInTry() throws java.lang.Exception {
528 | final Receipt receipt = order(token);
529 | callback.onPaidProcessed(receipt);
530 | }
531 | };
532 |
533 | if (checkout.action == Checkout.WITHOUT_3DS) {
534 | Cloudipsp.runInTry(orderChecker, callback);
535 | } else {
536 | url3ds(token, checkout, callback);
537 | }
538 | }
539 |
540 | private static Receipt order(String token) throws java.lang.Exception {
541 | final TreeMap request = new TreeMap<>();
542 | request.put("token", token);
543 | final JSONObject response = call("/api/checkout/merchant/order", request);
544 | final JSONObject orderData = response.getJSONObject("order_data");
545 | return parseOrder(orderData, response.getString("response_url"));
546 | }
547 |
548 | private static JSONObject ajaxInfo(String token) throws java.lang.Exception {
549 | final TreeMap request = new TreeMap<>();
550 | request.put("token", token);
551 | return call("/api/checkout/ajax/info", request);
552 | }
553 |
554 | private static Receipt parseOrder(JSONObject orderData, String responseUrl) throws JSONException {
555 | Card.Type cardType;
556 | try {
557 | cardType = Card.Type.valueOf(orderData.getString("card_type").toUpperCase());
558 | } catch (IllegalArgumentException e) {
559 | cardType = Card.Type.UNKNOWN;
560 | }
561 |
562 | Date recTokenLifeTime;
563 | try {
564 | recTokenLifeTime = DATE_AND_FORMAT.parse(orderData.getString("rectoken_lifetime"));
565 | } catch (java.lang.Exception e) {
566 | recTokenLifeTime = null;
567 | }
568 | Date settlementDate;
569 | try {
570 | settlementDate = DATE_FORMAT.parse(orderData.getString("settlement_date"));
571 | } catch (java.lang.Exception e) {
572 | settlementDate = null;
573 | }
574 | final String verificationStatus = orderData.optString("verification_status");
575 | final Receipt.VerificationStatus verificationStatusEnum;
576 | if (TextUtils.isEmpty(verificationStatus)) {
577 | verificationStatusEnum = null;
578 | } else {
579 | verificationStatusEnum = Receipt.VerificationStatus.valueOf(verificationStatus);
580 | }
581 |
582 | return new Receipt
583 | (
584 | orderData.getString("masked_card"),
585 | orderData.getString("card_bin"),
586 | Integer.valueOf(orderData.getString("amount")),
587 | orderData.getInt("payment_id"),
588 | orderData.getString("currency"),
589 | Receipt.Status.valueOf(orderData.getString("order_status")),
590 | Receipt.TransationType.valueOf(orderData.getString("tran_type")),
591 | orderData.getString("sender_cell_phone"),
592 | orderData.getString("sender_account"),
593 | cardType,
594 | orderData.getString("rrn"),
595 | orderData.getString("approval_code"),
596 | orderData.getString("response_code"),
597 | orderData.getString("product_id"),
598 | orderData.getString("rectoken"),
599 | recTokenLifeTime,
600 | orderData.optInt("reversal_amount", -1),
601 | orderData.optInt("settlement_amount", -1),
602 | orderData.optString("settlement_currency"),
603 | settlementDate,
604 | orderData.optInt("eci", -1),
605 | orderData.optInt("fee", -1),
606 | orderData.optInt("actual_amount", -1),
607 | orderData.optString("actual_currency"),
608 | orderData.optString("payment_system"),
609 | verificationStatusEnum,
610 | orderData.getString("signature"),
611 | responseUrl,
612 | orderData
613 | );
614 | }
615 |
616 | private static JSONObject call(String path, TreeMap request) throws java.lang.Exception {
617 | final JSONObject json = callJson(path, request);
618 | checkResponse(json);
619 | return json;
620 | }
621 |
622 | private static JSONObject callJson(String path, TreeMap request) throws java.lang.Exception {
623 | final JSONObject body = new JSONObject();
624 | try {
625 | body.put("request", new JSONObject(request));
626 | } catch (JSONException e) {
627 | throw new RuntimeException(e);
628 | }
629 | final String jsonOfResponse = call(HOST + path, body.toString(), "application/json");
630 | if (BuildConfig.DEBUG) {
631 | Log.i("Cloudipsp", "Read: " + jsonOfResponse);
632 | }
633 | return new JSONObject(jsonOfResponse).getJSONObject("response");
634 | }
635 |
636 | private static void checkResponse(JSONObject response) throws java.lang.Exception {
637 | if (!response.getString("response_status").equals("success")) {
638 | handleResponseError(response);
639 | }
640 | }
641 |
642 | private static void handleResponseError(JSONObject response) throws java.lang.Exception {
643 | throw new Exception.Failure
644 | (
645 | response.getString("error_message"),
646 | response.getInt("error_code"),
647 | response.getString("request_id")
648 | );
649 | }
650 |
651 | private void url3ds(final String token, final Checkout checkout, final PayCallback callback) throws java.lang.Exception {
652 | final Rs rs;
653 | final String[] contentType = new String[1];
654 | final ResponseInterceptor interceptor = new ResponseInterceptor() {
655 | @Override
656 | public void onIntercept(HttpURLConnection httpURLConnection) {
657 | contentType[0] = httpURLConnection.getHeaderField("Content-Type");
658 | }
659 | };
660 | if (TextUtils.isEmpty(checkout.sendData.paReq)) {
661 | final JSONObject sendData = new JSONObject();
662 | sendData.put("MD", checkout.sendData.md);
663 | sendData.put("PaReq", checkout.sendData.paReq);
664 | sendData.put("TermUrl", checkout.sendData.termUrl);
665 | rs = callRaw(checkout.url, sendData.toString(), "application/json", interceptor);
666 | } else {
667 | final String urlEncoded =
668 | "MD=" + URLEncoder.encode(checkout.sendData.md, "UTF-8") + "&" +
669 | "PaReq=" + URLEncoder.encode(checkout.sendData.paReq, "UTF-8") + "&" +
670 | "TermUrl=" + URLEncoder.encode(checkout.sendData.termUrl, "UTF-8");
671 |
672 | rs = callRaw(checkout.url, urlEncoded, "application/x-www-form-urlencoded", interceptor);
673 | }
674 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1 || TextUtils.isEmpty(contentType[0])) {
675 | contentType[0] = "text/html";
676 | }
677 |
678 | final CloudipspView.PayConfirmation confirmation = new CloudipspView.PayConfirmation(
679 | rs.content,
680 | contentType[0],
681 | checkout.url,
682 | checkout.callbackUrl,
683 | HOST,
684 | rs.cookie,
685 | new CloudipspView.PayConfirmation.Listener() {
686 | @Override
687 | public void onConfirmed(final JSONObject response) {
688 | runInTry(new RunInTry() {
689 | @Override
690 | public void runInTry() throws java.lang.Exception {
691 | if (response == null) {
692 | new Task(callback) {
693 | @Override
694 | public void runInTry() throws java.lang.Exception {
695 | callback.onPaidProcessed(order(token));
696 | }
697 | }.start();
698 | } else {
699 | if (!response.getString("url").startsWith(checkout.callbackUrl)) {
700 | throw new java.lang.Exception();
701 | }
702 | final JSONObject orderData = response.getJSONObject("params");
703 | checkResponse(orderData);
704 | callback.onPaidProcessed(parseOrder(orderData, null));
705 | }
706 | }
707 | }, callback);
708 | }
709 |
710 | @Override
711 | public void onNetworkAccessError(String description) {
712 | callback.onPaidFailure(new Exception.NetworkAccess(description));
713 | }
714 |
715 | @Override
716 | public void onNetworkSecurityError(String description) {
717 | callback.onPaidFailure(new Exception.NetworkSecurity(description));
718 | }
719 | });
720 |
721 | sMain.post(new Runnable() {
722 | @Override
723 | public void run() {
724 | cloudipspView.confirm(confirmation);
725 | }
726 | });
727 | }
728 |
729 | private static String call(String url, String content, String contentType) throws java.lang.Exception {
730 | return call(url, content, contentType, null);
731 | }
732 |
733 | private interface ResponseInterceptor {
734 | void onIntercept(HttpURLConnection httpURLConnection);
735 | }
736 |
737 | private static String call(String url, String content, String contentType, ResponseInterceptor responseInterceptor) throws java.lang.Exception {
738 | return callRaw(url, content, contentType, responseInterceptor).content;
739 | }
740 |
741 | private static Rs callRaw(String url, String content, String contentType, ResponseInterceptor responseInterceptor) throws java.lang.Exception {
742 | final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
743 | if (connection instanceof HttpsURLConnection) {
744 | final HttpsURLConnection secureConnection = (HttpsURLConnection) connection;
745 | if (tlsSocketFactory != null) {
746 | secureConnection.setSSLSocketFactory(tlsSocketFactory);
747 | }
748 | secureConnection.setHostnameVerifier(new HostnameVerifier() {
749 | @Override
750 | public boolean verify(String hostname, SSLSession session) {
751 | final String peerHost = session.getPeerHost();
752 | return hostname.equals(peerHost);
753 | }
754 | });
755 | }
756 | final byte[] sentBytes = content.getBytes();
757 | try {
758 | connection.setRequestMethod("POST");
759 | connection.setRequestProperty("Content-Type", contentType);
760 | connection.setRequestProperty("Content-Length", String.valueOf(sentBytes.length));
761 | connection.setRequestProperty("User-Agent", "Android-SDK");
762 | connection.setRequestProperty("SDK-OS", "android");
763 | connection.setRequestProperty("SDK-Version", "1.17.2");
764 | connection.setDoInput(true);
765 | connection.setDoOutput(true);
766 | connection.setUseCaches(false);
767 |
768 | final OutputStream output = connection.getOutputStream();
769 | output.write(sentBytes);
770 | output.flush();
771 | if (BuildConfig.DEBUG) {
772 | Log.i("Cloudipsp", "Sent(" + url + "):" + content);
773 | }
774 | connection.connect();
775 | final int contentLength = connection.getHeaderFieldInt("ContentLength", 350);
776 | if (responseInterceptor != null) {
777 | responseInterceptor.onIntercept(connection);
778 | }
779 | final Rs rs = new Rs();
780 |
781 | final StringBuilder sb = new StringBuilder(contentLength);
782 | readAll(connection.getInputStream(), sb);
783 | rs.cookie = connection.getHeaderField("Set-Cookie");
784 | rs.content = sb.toString();
785 | return rs;
786 | } finally {
787 | connection.disconnect();
788 | }
789 | }
790 |
791 | private static class Rs {
792 | public String content;
793 | public String cookie;
794 | }
795 |
796 | private static void readAll(InputStream from, StringBuilder to) throws IOException {
797 | final BufferedReader reader = new BufferedReader(new InputStreamReader(from, "UTF-8"));
798 | String line;
799 | while ((line = reader.readLine()) != null) {
800 | to.append(line);
801 | to.append('\n');
802 | }
803 | }
804 |
805 | public static class Exception extends java.lang.Exception {
806 | Exception() {
807 | }
808 |
809 | Exception(String detailMessage) {
810 | super(detailMessage);
811 | }
812 |
813 | Exception(Throwable throwable) {
814 | super(throwable);
815 | }
816 |
817 | public final static class Failure extends Exception {
818 | public final int errorCode;
819 | public final String requestId;
820 |
821 | Failure(String detailMessage, int errorCode, String requestId) {
822 | super(detailMessage);
823 | this.errorCode = errorCode;
824 | this.requestId = requestId;
825 | }
826 | }
827 |
828 | public final static class GooglePayUnsupported extends Exception {
829 | }
830 |
831 | public final static class GooglePayFailure extends Exception {
832 | public final Status status;
833 |
834 | private GooglePayFailure(Status status) {
835 | this.status = status;
836 | }
837 | }
838 |
839 | public final static class IllegalServerResponse extends Exception {
840 | IllegalServerResponse(Throwable throwable) {
841 | super(throwable);
842 | }
843 | }
844 |
845 | public final static class NetworkSecurity extends Exception {
846 | NetworkSecurity(String detailMessage) {
847 | super(detailMessage);
848 | }
849 | }
850 |
851 | public final static class NetworkAccess extends Exception {
852 | NetworkAccess(String detailMessage) {
853 | super(detailMessage);
854 | }
855 | }
856 |
857 | public final static class ServerInternalError extends Exception {
858 | ServerInternalError(Throwable throwable) {
859 | super(throwable);
860 | }
861 | }
862 |
863 | public final static class Unknown extends Exception {
864 | Unknown(Throwable throwable) {
865 | super(throwable);
866 | }
867 | }
868 | }
869 |
870 | public static void setStrictUiBlocking(boolean value) {
871 | strictUiBlocking = value;
872 | }
873 | }
874 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CloudipspView.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import org.json.JSONObject;
4 |
5 | /**
6 | * Created by vberegovoy on 28.11.15.
7 | */
8 | public interface CloudipspView {
9 | void confirm(PayConfirmation confirmation);
10 |
11 | final class PayConfirmation {
12 | final String htmlPageContent;
13 | final String contentType;
14 | final String url;
15 | final String callbackUrl;
16 | final String host;
17 | final String cookie;
18 | final Listener listener;
19 |
20 | PayConfirmation(String htmlPageContent, String contentType, String url, String callbackUrl, String host, String cookie, Listener listener) {
21 | this.htmlPageContent = htmlPageContent;
22 | this.contentType = contentType;
23 | this.url = url;
24 | this.callbackUrl = callbackUrl;
25 | this.host = host;
26 | this.cookie = cookie;
27 | this.listener = listener;
28 | }
29 |
30 | interface Listener {
31 | void onConfirmed(JSONObject response);
32 |
33 | void onNetworkAccessError(String description);
34 |
35 | void onNetworkSecurityError(String description);
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CloudipspWebView.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.annotation.TargetApi;
4 | import android.content.Context;
5 | import android.graphics.Bitmap;
6 | import android.net.Uri;
7 | import android.net.http.SslError;
8 | import android.os.Build;
9 | import android.util.AttributeSet;
10 | import android.util.Log;
11 | import android.view.View;
12 | import android.webkit.CookieManager;
13 | import android.webkit.CookieSyncManager;
14 | import android.webkit.SslErrorHandler;
15 | import android.webkit.ValueCallback;
16 | import android.webkit.WebResourceError;
17 | import android.webkit.WebResourceRequest;
18 | import android.webkit.WebSettings;
19 | import android.webkit.WebView;
20 | import android.webkit.WebViewClient;
21 |
22 | import org.json.JSONException;
23 | import org.json.JSONObject;
24 |
25 | import java.net.URLDecoder;
26 |
27 | /**
28 | * Created by vberegovoy on 28.11.15.
29 | */
30 | public class CloudipspWebView extends WebView implements CloudipspView {
31 | private static final String URL_START_PATTERN = "http://secure-redirect.cloudipsp.com/submit/#";
32 |
33 | public CloudipspWebView(Context context) {
34 | super(context);
35 | init();
36 | }
37 |
38 | public CloudipspWebView(Context context, AttributeSet attrs) {
39 | super(context, attrs);
40 | init();
41 | }
42 |
43 | public CloudipspWebView(Context context, AttributeSet attrs, int defStyle) {
44 | super(context, attrs, defStyle);
45 | init();
46 | }
47 |
48 | private void init() {
49 | final WebSettings settings = getSettings();
50 | settings.setJavaScriptEnabled(true);
51 | settings.setJavaScriptCanOpenWindowsAutomatically(false);
52 | settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
53 | settings.setDomStorageEnabled(true);
54 | settings.setLoadsImagesAutomatically(true);
55 | settings.setAllowFileAccess(false);
56 | settings.setDatabaseEnabled(true);
57 |
58 | setVisibility(View.GONE);
59 | }
60 |
61 | public final boolean waitingForConfirm() {
62 | return getVisibility() == View.VISIBLE;
63 | }
64 |
65 | public final void skipConfirm() {
66 | stopLoading();
67 | setVisibility(View.GONE);
68 | }
69 |
70 | public final void confirm(final PayConfirmation confirmation) {
71 | if (confirmation == null) {
72 | throw new NullPointerException("confirmation should be not null");
73 | }
74 | setVisibility(View.VISIBLE);
75 |
76 | setWebViewClient(new WebViewClient() {
77 | @Override
78 | public boolean shouldOverrideUrlLoading(WebView view, String url) {
79 | if (checkUrl(url)) {
80 | return true;
81 | }
82 | return super.shouldOverrideUrlLoading(view, url);
83 | }
84 |
85 | @Override
86 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
87 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
88 | if (checkUrl(request.getUrl().toString())) {
89 | return true;
90 | }
91 | }
92 | return super.shouldOverrideUrlLoading(view, request);
93 | }
94 |
95 | @Override
96 | public void onPageStarted(WebView view, String url, Bitmap favicon) {
97 | checkUrl(url);
98 | }
99 |
100 | private boolean checkUrl(String url) {
101 | if (BuildConfig.DEBUG) {
102 | Log.i("Cloudipsp", "WebUrl: " + url);
103 | }
104 | boolean detectsStartPattern = url.startsWith(URL_START_PATTERN);
105 | boolean detectsCallbackUrl = false;
106 | boolean detectsApiToken = false;
107 | if (!detectsStartPattern) {
108 | detectsCallbackUrl = url.startsWith(confirmation.callbackUrl);
109 | if (!detectsCallbackUrl) {
110 | detectsApiToken = url.startsWith(confirmation.host + "/api/checkout?token=");
111 | }
112 | }
113 |
114 | if (detectsStartPattern || detectsCallbackUrl || detectsApiToken) {
115 | blankPage();
116 |
117 | JSONObject response = null;
118 | if (detectsStartPattern) {
119 | final String jsonOfConfirmation = url.split(URL_START_PATTERN)[1];
120 | try {
121 | response = new JSONObject(jsonOfConfirmation);
122 | } catch (JSONException jsonException) {
123 | try {
124 | response = new JSONObject(URLDecoder.decode(jsonOfConfirmation, "UTF-8"));
125 | } catch (Exception e) {
126 | response = null;
127 | }
128 | }
129 | }
130 | confirmation.listener.onConfirmed(response);
131 |
132 | setVisibility(View.GONE);
133 | return true;
134 | } else {
135 | return false;
136 | }
137 | }
138 |
139 | @Override
140 | public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
141 | handleError(errorCode, description);
142 | }
143 |
144 | @Override
145 | @TargetApi(Build.VERSION_CODES.M)
146 | public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
147 | handleError(error.getErrorCode(), error.getDescription().toString());
148 | }
149 |
150 | private void handleError(int errorCode, String description) {
151 | switch (errorCode) {
152 | case WebViewClient.ERROR_HOST_LOOKUP:
153 | case WebViewClient.ERROR_IO:
154 | case WebViewClient.ERROR_CONNECT:
155 | confirmation.listener.onNetworkAccessError(description);
156 | handleError();
157 | break;
158 | case WebViewClient.ERROR_FAILED_SSL_HANDSHAKE:
159 | confirmation.listener.onNetworkSecurityError(description);
160 | handleError();
161 | break;
162 | }
163 | }
164 |
165 | @Override
166 | public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
167 | super.onReceivedSslError(view, handler, error);
168 | confirmation.listener.onNetworkSecurityError(error.toString());
169 | handleError();
170 | }
171 |
172 | private void handleError() {
173 | blankPage();
174 | skipConfirm();
175 | }
176 | });
177 |
178 |
179 | if (Tls12SocketFactory.needHere()) {
180 | loadProxy(confirmation);
181 | } else {
182 | final Runnable l = new Runnable() {
183 | @Override
184 | public void run() {
185 | loadDataWithBaseURL(confirmation.url, confirmation.htmlPageContent, confirmation.contentType, encoding(confirmation.contentType), null);
186 | }
187 | };
188 |
189 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && confirmation.cookie != null) {
190 | CookieManager.getInstance().setCookie(confirmation.url, confirmation.cookie, new ValueCallback() {
191 | @Override
192 | public void onReceiveValue(Boolean value) {
193 | l.run();
194 | }
195 | });
196 | } else {
197 | l.run();
198 | }
199 | }
200 | }
201 |
202 | private void blankPage() {
203 | loadDataWithBaseURL(null, "", "text/html", "UTF-8", null);
204 | invalidate();
205 | }
206 |
207 | private static String encoding(String contentType) {
208 | String[] parts = contentType.split("charset\\=");
209 | if (parts.length < 2) {
210 | return "UTF-8";
211 | } else {
212 | return parts[1];
213 | }
214 | }
215 |
216 | private void loadProxy(PayConfirmation confirmation) {
217 | try {
218 | final Uri uri = Uri.parse(confirmation.url);
219 | final String oldHost = uri.getAuthority();
220 | final String newHost = "3dse.fondy.eu";
221 |
222 | final Uri.Builder uriBuilder = uri.buildUpon()
223 | .authority(newHost)
224 | .path(uri.getPath())
225 | .appendQueryParameter("jd91mx8", oldHost);
226 | final String url = uriBuilder.toString();
227 | String htmlPageContent = confirmation.htmlPageContent;
228 |
229 | final String quoted = oldHost.replace(".", "\\.");
230 | htmlPageContent = htmlPageContent.replaceAll(quoted, newHost);
231 |
232 |
233 | clearProxy();
234 | loadDataWithBaseURL(url, htmlPageContent, confirmation.contentType, encoding(confirmation.contentType), null);
235 | } catch (Exception e) {
236 | throw new RuntimeException(e);
237 | }
238 | }
239 |
240 | private void clearProxy() {
241 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
242 | CookieManager.getInstance().removeAllCookies(null);
243 | CookieManager.getInstance().flush();
244 | } else {
245 | CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(getContext());
246 | cookieSyncMngr.startSync();
247 | CookieManager cookieManager = CookieManager.getInstance();
248 | cookieManager.removeAllCookie();
249 | cookieManager.removeSessionCookie();
250 | cookieSyncMngr.stopSync();
251 | cookieSyncMngr.sync();
252 | }
253 |
254 | clearCache(true);
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/Currency.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | /**
4 | * Created by vberegovoy on 09.11.15.
5 | */
6 | public enum Currency {
7 | UAH,
8 | RUB,
9 | USD,
10 | EUR,
11 | GBP,
12 | KZT;
13 |
14 | @Override
15 | public String toString() {
16 | return name();
17 | }
18 |
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/CvvUtils.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | /**
4 | * Created by vberegovoy on 2/19/18.
5 | */
6 |
7 | class CvvUtils {
8 | private static final String[] CVV4_BINS = new String[] {"32", "33", "34", "37"};
9 |
10 | static boolean isCvv4Length(String cardNumber) {
11 | for (String bin : CVV4_BINS) {
12 | if (cardNumber.startsWith(bin)) {
13 | return true;
14 | }
15 | }
16 | return false;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/GooglePayCall.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.os.Parcel;
4 | import android.os.Parcelable;
5 |
6 | public class GooglePayCall implements Parcelable {
7 | public static final Creator CREATOR = new Creator() {
8 | @Override
9 | public GooglePayCall createFromParcel(Parcel input) {
10 | return new GooglePayCall(input);
11 | }
12 |
13 | @Override
14 | public GooglePayCall[] newArray(int size) {
15 | return new GooglePayCall[size];
16 | }
17 | };
18 |
19 | final String token;
20 | final Order order;
21 | final String callbackUrl;
22 | final String paymentSystem;
23 |
24 | private GooglePayCall(Parcel input) {
25 | this.token = input.readString();
26 | this.order = input.readParcelable(Order.class.getClassLoader());
27 | this.callbackUrl = input.readString();
28 | this.paymentSystem = input.readString();
29 | }
30 |
31 | GooglePayCall(String token, Order order, String callbackUrl, String paymentSystem) {
32 | this.token = token;
33 | this.order = order;
34 | this.callbackUrl = callbackUrl;
35 | this.paymentSystem = paymentSystem;
36 | }
37 |
38 | @Override
39 | public int describeContents() {
40 | return 0;
41 | }
42 |
43 | @Override
44 | public void writeToParcel(Parcel output, int flags) {
45 | output.writeString(token);
46 | output.writeParcelable(order, 0);
47 | output.writeString(callbackUrl);
48 | output.writeString(paymentSystem);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/Order.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.os.Bundle;
4 | import android.os.Parcel;
5 | import android.os.Parcelable;
6 | import android.text.TextUtils;
7 | import android.util.Patterns;
8 |
9 | import java.util.HashMap;
10 |
11 | /**
12 | * Created by vberegovoy on 10.11.15.
13 | */
14 | public class Order implements Parcelable {
15 | public static final Creator CREATOR = new Creator() {
16 | @Override
17 | public Order createFromParcel(Parcel input) {
18 | return new Order(input);
19 | }
20 |
21 | @Override
22 | public Order[] newArray(int size) {
23 | return new Order[size];
24 | }
25 | };
26 |
27 | public enum Verification {
28 | amount,
29 | code
30 | }
31 |
32 | public enum Lang {
33 | ru,
34 | uk,
35 | en,
36 | lv,
37 | fr;
38 | }
39 |
40 | public final int amount;
41 | public final String currency;
42 | public final String id;
43 | public final String description;
44 | public final String email;
45 |
46 | String productId;
47 | String paymentSystems;
48 | String defaultPaymentSystem;
49 | int lifetime = -1;
50 | String merchantData;
51 | boolean preauth = false;
52 | boolean requiredRecToken = false;
53 | boolean verification = false;
54 | Verification verificationType = Verification.amount;
55 | String recToken;
56 | String version;
57 | Lang lang;
58 | String serverCallbackUrl;
59 | String reservationData;
60 | String paymentSystem;
61 | boolean delayed = false;
62 |
63 | final HashMap arguments = new HashMap();
64 |
65 | private Order(Parcel input) {
66 | amount = input.readInt();
67 | currency = input.readString();
68 | id = input.readString();
69 | description = input.readString();
70 | email = input.readString();
71 | productId = input.readString();
72 | paymentSystems = input.readString();
73 | defaultPaymentSystem = input.readString();
74 | lifetime = input.readInt();
75 | merchantData = input.readString();
76 | preauth = input.readInt() == 1;
77 | requiredRecToken = input.readInt() == 1;
78 | verification = input.readInt() == 1;
79 | verificationType = (Verification) input.readSerializable();
80 | recToken = input.readString();
81 | version = input.readString();
82 | lang = (Lang) input.readSerializable();
83 | serverCallbackUrl = input.readString();
84 | reservationData = input.readString();
85 | paymentSystem = input.readString();
86 | delayed = input.readInt() == 1;
87 |
88 | final Bundle bundle = input.readBundle();
89 | for (String key : bundle.keySet()) {
90 | arguments.put(key, bundle.getString(key));
91 | }
92 | }
93 |
94 | public Order(int amount, Currency currency, String id, String description) {
95 | this(amount, currency, id, description, null);
96 | }
97 |
98 | public Order(int amount, Currency currency, String id, String description, String email) {
99 | this(amount, currency == null ? null : currency.name(), id, description, email);
100 | }
101 |
102 | public Order(int amount, String currency, String id, String description, String email) {
103 | if (amount <= 0) {
104 | throw new IllegalArgumentException("Amount should be more than 0");
105 | }
106 | if (currency == null) {
107 | throw new NullPointerException("currency should be not null");
108 | }
109 | if (id == null) {
110 | throw new NullPointerException("id should be not null");
111 | }
112 | if (id.length() == 0 || id.length() > 1024) {
113 | throw new IllegalArgumentException("id's length should be > 0 && <= 1024");
114 | }
115 | if (description == null) {
116 | throw new NullPointerException("description should be not null");
117 | }
118 | if (description.length() == 0 || description.length() > 1024) {
119 | throw new IllegalArgumentException("description's length should be > 0 && <= 1024");
120 | }
121 | if (!TextUtils.isEmpty(email) && !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
122 | throw new IllegalArgumentException("email is not valid");
123 | }
124 |
125 | this.amount = amount;
126 | this.currency = currency;
127 | this.id = id;
128 | this.description = description;
129 | this.email = email;
130 | }
131 |
132 | public void setProductId(String value) {
133 | if (value == null) {
134 | throw new NullPointerException("ProductId should be not null");
135 | }
136 | if (value.length() > 1024) {
137 | throw new IllegalArgumentException("ProductId should be not more than 1024 symbols");
138 | }
139 | productId = value;
140 | }
141 |
142 | public void setPaymentSystems(String value) {
143 | if (value == null) {
144 | throw new NullPointerException("PaymentSystems should be not null");
145 | }
146 | paymentSystems = value;
147 | }
148 |
149 | public void setDelayed(boolean value) {
150 | delayed = value;
151 | }
152 |
153 | public void setDefaultPaymentSystem(String value) {
154 | if (value == null) {
155 | throw new NullPointerException("Default payment system should be not null");
156 | }
157 | defaultPaymentSystem = value;
158 | }
159 |
160 | public void setLifetime(int value) {
161 | lifetime = value;
162 | }
163 |
164 | public void setMerchantData(String value) {
165 | if (value == null) {
166 | throw new NullPointerException("MerchantData should be not null");
167 | }
168 | if (value.length() > 2048) {
169 | throw new IllegalArgumentException("MerchantData should be not more than 2048 symbols");
170 | }
171 | merchantData = value;
172 | }
173 |
174 | public void setPreauth(boolean enable) {
175 | preauth = enable;
176 | }
177 |
178 | public void setRequiredRecToken(boolean enable) {
179 | requiredRecToken = enable;
180 | }
181 |
182 | public void setVerification(boolean enable) {
183 | verification = enable;
184 | }
185 |
186 | public void setVerificationType(Verification type) {
187 | if (type == null) {
188 | throw new NullPointerException("VerificationType should be not null");
189 | }
190 | verificationType = type;
191 | }
192 |
193 | public void setRecToken(String value) {
194 | if (value == null) {
195 | throw new NullPointerException("RecToken should be not null");
196 | }
197 | recToken = value;
198 | }
199 |
200 | public void setVersion(String value) {
201 | if (value == null) {
202 | throw new NullPointerException("version should be not null");
203 | }
204 | if (value.length() > 10) {
205 | throw new IllegalArgumentException("version should be not more than 10 symbols");
206 | }
207 | version = value;
208 | }
209 |
210 | public void setLang(Lang value) {
211 | if (value == null) {
212 | throw new NullPointerException("Lang should be not null");
213 | }
214 | lang = value;
215 | }
216 |
217 | public void setServerCallbackUrl(String value) {
218 | if (value == null) {
219 | throw new NullPointerException("server callback url should be not null");
220 | }
221 | if (value.length() > 2048) {
222 | throw new IllegalArgumentException("server callback url should be not more than 10 symbols");
223 | }
224 | serverCallbackUrl = value;
225 | }
226 |
227 | public void setReservationData(String value) {
228 | if (value == null) {
229 | throw new NullPointerException("reservation data should be not null");
230 | }
231 | reservationData = value;
232 | }
233 |
234 | public void addArgument(String name, String value) {
235 | arguments.put(name, value);
236 | }
237 |
238 | @Override
239 | public int describeContents() {
240 | return 0;
241 | }
242 |
243 | @Override
244 | public void writeToParcel(Parcel output, int flags) {
245 | output.writeInt(amount);
246 | output.writeSerializable(currency);
247 | output.writeString(id);
248 | output.writeString(description);
249 | output.writeString(email);
250 | output.writeString(productId);
251 | output.writeString(paymentSystems);
252 | output.writeString(defaultPaymentSystem);
253 | output.writeInt(lifetime);
254 | output.writeString(merchantData);
255 | output.writeInt(preauth ? 1 : 0);
256 | output.writeInt(requiredRecToken ? 1 : 0);
257 | output.writeInt(verification ? 1 : 0);
258 | output.writeSerializable(verificationType);
259 | output.writeString(recToken);
260 | output.writeString(version);
261 | output.writeSerializable(lang);
262 | output.writeString(serverCallbackUrl);
263 | output.writeString(reservationData);
264 | output.writeString(paymentSystem);
265 | output.writeInt(delayed ? 1 : 0);
266 |
267 | final Bundle bundle = new Bundle();
268 | for (String key : arguments.keySet()) {
269 | bundle.putString(key, arguments.get(key));
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/Receipt.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.os.Parcel;
4 | import android.os.Parcelable;
5 |
6 | import org.json.JSONException;
7 | import org.json.JSONObject;
8 |
9 | import java.util.Date;
10 |
11 | /**
12 | * Created by vberegovoy on 10.11.15.
13 | */
14 | public class Receipt implements Parcelable {
15 | public static final Creator CREATOR = new Creator() {
16 | @Override
17 | public Receipt createFromParcel(Parcel input) {
18 | return new Receipt(input);
19 | }
20 |
21 | @Override
22 | public Receipt[] newArray(int size) {
23 | return new Receipt[size];
24 | }
25 | };
26 |
27 | public enum Status {
28 | created,
29 | processing,
30 | declined,
31 | approved,
32 | expired,
33 | reversed;
34 | }
35 |
36 | public enum TransationType {
37 | purchase,
38 | reverse,
39 | verification
40 | }
41 |
42 | public enum VerificationStatus {
43 | verified,
44 | incorrect,
45 | failed,
46 | created
47 | }
48 |
49 | public final String maskedCard;
50 | public final String cardBin;
51 | public final int amount;
52 | public final int paymentId;
53 | public final String currency;
54 | public final Status status;
55 | public final TransationType transationType;
56 | public final String senderCellPhone;
57 | public final String senderAccount;
58 | public final Card.Type cardType;
59 | public final String rrn;
60 | public final String approvalCode;
61 | public final String responseCode;
62 | public final String productId;
63 | public final String recToken;
64 | public final Date recTokenLifeTime;
65 | public final int reversalAmount;
66 | public final int settlementAmount;
67 | public final String settlementCurrency;
68 | public final Date settlementDate;
69 | public final int eci;
70 | public final int fee;
71 | public final int actualAmount;
72 | public final String actualCurrency;
73 | public final String paymentSystem;
74 | public final VerificationStatus verificationStatus;
75 | public final String signature;
76 | final String responseUrl;
77 |
78 | final JSONObject orderData;
79 |
80 | private Receipt(Parcel input) {
81 | maskedCard = input.readString();
82 | cardBin = input.readString();
83 | amount = input.readInt();
84 | paymentId = input.readInt();
85 | currency = input.readString();
86 | status = (Status) input.readSerializable();
87 | transationType = (TransationType) input.readSerializable();
88 | senderCellPhone = input.readString();
89 | senderAccount = input.readString();
90 | cardType = (Card.Type) input.readSerializable();
91 | rrn = input.readString();
92 | approvalCode = input.readString();
93 | responseCode = input.readString();
94 | productId = input.readString();
95 | recToken = input.readString();
96 | recTokenLifeTime = (Date)input.readSerializable();
97 | reversalAmount = input.readInt();
98 | settlementAmount = input.readInt();
99 | settlementCurrency = input.readString();
100 | settlementDate = (Date) input.readSerializable();
101 | eci = input.readInt();
102 | fee = input.readInt();
103 | actualAmount = input.readInt();
104 | actualCurrency = input.readString();
105 | paymentSystem = input.readString();
106 | verificationStatus = (VerificationStatus) input.readSerializable();
107 | signature = input.readString();
108 | responseUrl = input.readString();
109 | try {
110 | orderData = new JSONObject(input.readString());
111 | } catch (JSONException e) {
112 | throw new RuntimeException(e);
113 | }
114 | }
115 |
116 | Receipt(String maskedCard, String cardBin, int amount, int paymentId, String currency,
117 | Status status, TransationType transationType, String senderCellPhone, String senderAccount,
118 | Card.Type cardType, String rrn, String approvalCode, String responseCode, String productId,
119 | String recToken, Date recTokenLifeTime, int reversalAmount, int settlementAmount,
120 | String settlementCurrency, Date settlementDate, int eci, int fee, int actualAmount,
121 | String actualCurrency, String paymentSystem, VerificationStatus verificationStatus,
122 | String signature, String responseUrl, JSONObject orderData) {
123 | this.maskedCard = maskedCard;
124 | this.cardBin = cardBin;
125 | this.amount = amount;
126 | this.paymentId = paymentId;
127 | this.currency = currency;
128 | this.status = status;
129 | this.transationType = transationType;
130 | this.senderCellPhone = senderCellPhone;
131 | this.senderAccount = senderAccount;
132 | this.cardType = cardType;
133 | this.rrn = rrn;
134 | this.approvalCode = approvalCode;
135 | this.responseCode = responseCode;
136 | this.productId = productId;
137 | this.recToken = recToken;
138 | this.recTokenLifeTime = recTokenLifeTime;
139 | this.reversalAmount = reversalAmount;
140 | this.settlementAmount = settlementAmount;
141 | this.settlementCurrency = settlementCurrency;
142 | this.settlementDate = settlementDate;
143 | this.eci = eci;
144 | this.fee = fee;
145 | this.actualAmount = actualAmount;
146 | this.actualCurrency = actualCurrency;
147 | this.paymentSystem = paymentSystem;
148 | this.verificationStatus = verificationStatus;
149 | this.signature = signature;
150 | this.responseUrl = responseUrl;
151 | this.orderData = orderData;
152 | }
153 |
154 | @Override
155 | public int describeContents() {
156 | return 0;
157 | }
158 |
159 | @Override
160 | public void writeToParcel(Parcel output, int flags) {
161 | output.writeString(maskedCard);
162 | output.writeString(cardBin);
163 | output.writeInt(amount);
164 | output.writeInt(paymentId);
165 | output.writeSerializable(currency);
166 | output.writeSerializable(status);
167 | output.writeSerializable(transationType);
168 | output.writeString(senderCellPhone);
169 | output.writeString(senderAccount);
170 | output.writeSerializable(cardType);
171 | output.writeString(rrn);
172 | output.writeString(approvalCode);
173 | output.writeString(responseCode);
174 | output.writeString(productId);
175 | output.writeString(recToken);
176 | output.writeSerializable(recTokenLifeTime);
177 | output.writeInt(reversalAmount);
178 | output.writeInt(settlementAmount);
179 | output.writeSerializable(settlementCurrency);
180 | output.writeSerializable(settlementDate);
181 | output.writeInt(eci);
182 | output.writeInt(fee);
183 | output.writeInt(actualAmount);
184 | output.writeSerializable(actualCurrency);
185 | output.writeString(paymentSystem);
186 | output.writeSerializable(verificationStatus);
187 | output.writeString(signature);
188 | output.writeString(responseUrl);
189 | output.writeString(orderData.toString());
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/ReceiptUtils.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import java.util.HashMap;
4 | import java.util.Iterator;
5 | import java.util.Map;
6 |
7 | /**
8 | * Created by vberegovoy on 11.07.16.
9 | */
10 | public class ReceiptUtils {
11 | public static Map dumpFields(Receipt receipt) {
12 | final Iterator keys = receipt.orderData.keys();
13 | final Map map = new HashMap();
14 |
15 | while (keys.hasNext()) {
16 | final String key = keys.next();
17 | final Object value = receipt.orderData.opt(key);
18 |
19 | if (value instanceof Boolean || value instanceof Number || value instanceof String) {
20 | map.put(key, value);
21 | }
22 | }
23 |
24 | return map;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/library/src/main/java/com/cloudipsp/android/Tls12SocketFactory.java:
--------------------------------------------------------------------------------
1 | package com.cloudipsp.android;
2 |
3 | import android.os.Build;
4 |
5 | import java.io.IOException;
6 | import java.net.InetAddress;
7 | import java.net.Socket;
8 | import java.net.UnknownHostException;
9 | import java.security.KeyManagementException;
10 | import java.security.NoSuchAlgorithmException;
11 |
12 | import javax.net.ssl.SSLContext;
13 | import javax.net.ssl.SSLSocket;
14 | import javax.net.ssl.SSLSocketFactory;
15 |
16 | class Tls12SocketFactory extends SSLSocketFactory {
17 | private static final String[] TLS_V12_ONLY = {"TLSv1.2"};
18 | private final SSLSocketFactory delegate;
19 |
20 | private Tls12SocketFactory(SSLSocketFactory base) {
21 | this.delegate = base;
22 | }
23 |
24 | @Override
25 | public String[] getDefaultCipherSuites() {
26 | return delegate.getDefaultCipherSuites();
27 | }
28 |
29 | @Override
30 | public String[] getSupportedCipherSuites() {
31 | return delegate.getSupportedCipherSuites();
32 | }
33 |
34 | @Override
35 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
36 | return patch(delegate.createSocket(s, host, port, autoClose));
37 | }
38 |
39 | @Override
40 | public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
41 | return patch(delegate.createSocket(host, port));
42 | }
43 |
44 | @Override
45 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
46 | return patch(delegate.createSocket(host, port, localHost, localPort));
47 | }
48 |
49 | @Override
50 | public Socket createSocket(InetAddress host, int port) throws IOException {
51 | return patch(delegate.createSocket(host, port));
52 | }
53 |
54 | @Override
55 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
56 | return patch(delegate.createSocket(address, port, localAddress, localPort));
57 | }
58 |
59 | private Socket patch(Socket s) {
60 | if (s instanceof SSLSocket) {
61 | ((SSLSocket) s).setEnabledProtocols(TLS_V12_ONLY);
62 | }
63 | return s;
64 | }
65 |
66 | static SSLSocketFactory getInstance() {
67 | if (needHere()) {
68 | try {
69 | final SSLContext sc = SSLContext.getInstance("TLSv1.2");
70 | sc.init(null, null, null);
71 | return new Tls12SocketFactory(sc.getSocketFactory());
72 | } catch (NoSuchAlgorithmException | KeyManagementException e) {
73 | }
74 | }
75 |
76 | return null;
77 | }
78 |
79 | static boolean needHere() {
80 | return Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22;
81 | }
82 | }
--------------------------------------------------------------------------------
/library/src/main/res/layout/com_cloudipsp_android_card_input_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
17 |
18 |
27 |
28 |
33 |
34 |
39 |
40 |
52 |
53 |
65 |
66 |
67 |
72 |
73 |
79 |
80 |
--------------------------------------------------------------------------------
/library/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Номер карты:
4 | Срок действия:
5 | ММ
6 | ГГ
7 | CVV:
8 | Неверный номер карты
9 | Неверный год
10 | Неверный месяц
11 | Неверный срок действия
12 | Неверный cvv
13 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Card number:
4 | Expiry:
5 | MM
6 | YY
7 | CVV:
8 | Invalid card number
9 | Invalid year
10 | Invalid month
11 | Invalid date
12 | Invalid cvv
13 |
--------------------------------------------------------------------------------
/maven_push.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven'
2 | apply plugin: 'signing'
3 |
4 | def sonatypeRepositoryUrl;
5 | if (isReleaseBuild()) {
6 | println 'RELEASE BUILD'
7 | sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
8 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
9 | } else {
10 | println 'SNAPSHOT BUILD'
11 | sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
12 | : "https://oss.sonatype.org/content/repositories/snapshots/"
13 | }
14 |
15 | def getRepositoryUsername() {
16 | return hasProperty('nexusUsername') ? nexusUsername : ""
17 | }
18 |
19 | def getRepositoryPassword() {
20 | return hasProperty('nexusPassword') ? nexusPassword : ""
21 | }
22 |
23 | afterEvaluate { project ->
24 | uploadArchives {
25 | repositories {
26 | mavenDeployer {
27 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
28 |
29 | pom.artifactId = POM_ARTIFACT_ID
30 |
31 | repository(url: sonatypeRepositoryUrl) {
32 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
33 | }
34 |
35 | pom.project {
36 | name POM_NAME
37 | packaging POM_PACKAGING
38 | description POM_DESCRIPTION
39 | url POM_URL
40 |
41 | scm {
42 | url POM_SCM_URL
43 | connection POM_SCM_CONNECTION
44 | developerConnection POM_SCM_DEV_CONNECTION
45 | }
46 |
47 | licenses {
48 | license {
49 | name POM_LICENCE_NAME
50 | url POM_LICENCE_URL
51 | distribution POM_LICENCE_DIST
52 | }
53 | }
54 |
55 | developers {
56 | developer {
57 | id POM_DEVELOPER_ID
58 | name POM_DEVELOPER_NAME
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | signing {
67 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
68 | sign configurations.archives
69 | }
70 |
71 | task androidSourcesJar(type: Jar) {
72 | classifier = 'sources'
73 | from android.sourceSets.main.java.sourceFiles
74 | }
75 |
76 | artifacts {
77 | archives androidSourcesJar
78 | }
79 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------