diff --git a/app/build.gradle b/app/build.gradle
index 7a83652..8b521db 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -51,6 +51,7 @@ dependencies {
implementation 'com.google.android.gms:play-services-auth:20.4.1'
implementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
+ implementation files('libs/zpdk-release-v3.1.aar')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
@@ -83,4 +84,5 @@ dependencies {
//Firebase
implementation platform('com.google.firebase:firebase-bom:31.2.3')
implementation("com.squareup.okhttp3:okhttp:4.10.0")
+
}
\ No newline at end of file
diff --git a/app/libs/zpdk-release-v3.1.aar b/app/libs/zpdk-release-v3.1.aar
new file mode 100644
index 0000000..88d7505
Binary files /dev/null and b/app/libs/zpdk-release-v3.1.aar differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 82c8731..27e7499 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -30,7 +30,17 @@
tools:targetApi="31">
+ android:launchMode="singleTask"
+ android:exported="true">
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/capstone/foodify/Activity/OrderCheckOutActivity.java b/app/src/main/java/com/capstone/foodify/Activity/OrderCheckOutActivity.java
index 9d3c407..ecef400 100644
--- a/app/src/main/java/com/capstone/foodify/Activity/OrderCheckOutActivity.java
+++ b/app/src/main/java/com/capstone/foodify/Activity/OrderCheckOutActivity.java
@@ -1,9 +1,15 @@
package com.capstone.foodify.Activity;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Typeface;
+import android.os.Build;
import android.os.Bundle;
+import android.os.StrictMode;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@@ -17,6 +23,8 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.widget.NestedScrollView;
@@ -31,8 +39,15 @@ import com.capstone.foodify.Model.Address;
import com.capstone.foodify.Model.DistrictWardResponse;
import com.capstone.foodify.Model.GoogleMap.GoogleMapResponse;
import com.capstone.foodify.R;
+import com.capstone.foodify.ZaloPay.Api.CreateOrder;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.material.textfield.TextInputLayout;
+import com.thecode.aestheticdialogs.AestheticDialog;
+import com.thecode.aestheticdialogs.DialogStyle;
+import com.thecode.aestheticdialogs.DialogType;
+import com.thecode.aestheticdialogs.OnDialogClickListener;
+
+import org.json.JSONObject;
import java.text.DecimalFormat;
import java.util.ArrayList;
@@ -41,6 +56,10 @@ import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
+import vn.zalopay.sdk.Environment;
+import vn.zalopay.sdk.ZaloPayError;
+import vn.zalopay.sdk.ZaloPaySDK;
+import vn.zalopay.sdk.listeners.PayOrderListener;
public class OrderCheckOutActivity extends AppCompatActivity {
private static final Double LAT_SHOP = 16.0500622;
@@ -54,7 +73,7 @@ public class OrderCheckOutActivity extends AppCompatActivity {
EditText edt_address;
Spinner spn_list_address, spn_district, spn_ward;
ConstraintLayout manual_input_address_layout;
- Button change_address_button, confirm_address_button;
+ Button change_address_button, confirm_address_button, btn_ZaloPay;
RadioButton list_address_input, auto_detect_location, manual_input_address, radio_button_selected, take_food_from_shop;
RadioGroup address_option;
ConstraintLayout progress_layout;
@@ -97,6 +116,7 @@ public class OrderCheckOutActivity extends AppCompatActivity {
progress_layout = findViewById(R.id.progress_layout);
txt_distance = findViewById(R.id.txt_distance);
txt_ship_cost = findViewById(R.id.txt_ship_cost);
+ btn_ZaloPay = findViewById(R.id.btnZaloPay);
if(getIntent() != null){
@@ -104,6 +124,13 @@ public class OrderCheckOutActivity extends AppCompatActivity {
txt_total.setText(Common.changeCurrencyUnit(total));
}
+ StrictMode.ThreadPolicy policy = new
+ StrictMode.ThreadPolicy.Builder().permitAll().build();
+ StrictMode.setThreadPolicy(policy);
+
+ // ZaloPay SDK Init
+ ZaloPaySDK.init(2553, Environment.SANDBOX);
+
//Show list food in basket
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false);
recyclerView.setLayoutManager(linearLayoutManager);
@@ -170,6 +197,8 @@ public class OrderCheckOutActivity extends AppCompatActivity {
disableAllInputAddressOption();
getDistanceAndCalculateShipCost(finalAddress);
+
+ progress_layout.setVisibility(View.GONE);
}
}
});
@@ -181,6 +210,92 @@ public class OrderCheckOutActivity extends AppCompatActivity {
onBackPressed();
}
});
+
+ initZaloPaymentMethod();
+ }
+
+ private void initZaloPaymentMethod(){
+ // handle CreateOrder
+ btn_ZaloPay.setOnClickListener(new View.OnClickListener() {
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @SuppressLint("SetTextI18n")
+ @Override
+ public void onClick(View v) {
+
+ //Check address is null or not
+ if(finalAddress != null){
+
+ CreateOrder orderApi = new CreateOrder();
+
+ try {
+ JSONObject data = orderApi.createOrder(String.valueOf((int) total));
+ String code = data.getString("return_code");
+
+
+ if (code.equals("1")) {
+ //Success create order
+ String token = data.getString("zp_trans_token");
+
+ ZaloPaySDK.getInstance().payOrder(OrderCheckOutActivity.this, token, "demozpdk://app", new PayOrderListener() {
+ @Override
+ public void onPaymentSucceeded(final String transactionId, final String transToken, final String appTransID) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new AestheticDialog.Builder(OrderCheckOutActivity.this, DialogStyle.FLAT, DialogType.SUCCESS)
+ .setTitle("Thành công!")
+ .setMessage("Bạn đã thanh toán thành công cho đơn hàng #" + transactionId)
+ .setCancelable(true)
+ .setOnClickListener(new OnDialogClickListener() {
+ @Override
+ public void onClick(@NonNull AestheticDialog.Builder builder) {
+ Common.LIST_BASKET_FOOD.clear();
+ startActivity(new Intent(OrderCheckOutActivity.this, MainActivity.class));
+ finish();
+ }
+ })
+ .show();
+ }
+
+ });
+ }
+
+ @Override
+ public void onPaymentCanceled(String zpTransToken, String appTransID) {
+ new AestheticDialog.Builder(OrderCheckOutActivity.this, DialogStyle.RAINBOW, DialogType.INFO)
+ .setTitle("Thông báo!")
+ .setMessage("Huỷ thanh toán thành công!")
+ .setCancelable(true)
+ .show();
+ }
+
+ @Override
+ public void onPaymentError(ZaloPayError zaloPayError, String zpTransToken, String appTransID) {
+ new AestheticDialog.Builder(OrderCheckOutActivity.this, DialogStyle.FLAT, DialogType.ERROR)
+ .setTitle("Thanh toán chưa thành công!")
+ .setMessage("Xin vui lòng thử lại!")
+ .setCancelable(true)
+ .setOnClickListener(new OnDialogClickListener() {
+ @Override
+ public void onClick(@NonNull AestheticDialog.Builder builder) {
+ Common.LIST_BASKET_FOOD.clear();
+ startActivity(new Intent(OrderCheckOutActivity.this, MainActivity.class));
+ finish();
+ }
+ })
+ .show();
+ }
+ });
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else {
+ Toast.makeText(OrderCheckOutActivity.this, "Bạn chưa chọn địa chỉ!", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
}
private void getDistanceAndCalculateShipCost(String address) {
@@ -510,4 +625,10 @@ public class OrderCheckOutActivity extends AppCompatActivity {
return view;
}
}
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ ZaloPaySDK.getInstance().onResult(intent);
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/capstone/foodify/ZaloPay/Api/CreateOrder.java b/app/src/main/java/com/capstone/foodify/ZaloPay/Api/CreateOrder.java
new file mode 100644
index 0000000..a12d407
--- /dev/null
+++ b/app/src/main/java/com/capstone/foodify/ZaloPay/Api/CreateOrder.java
@@ -0,0 +1,70 @@
+package com.capstone.foodify.ZaloPay.Api;
+
+import com.capstone.foodify.ZaloPay.Constant.AppInfo;
+import com.capstone.foodify.ZaloPay.Helper.Helpers;
+
+import org.json.JSONObject;
+
+import java.util.Date;
+
+import okhttp3.FormBody;
+import okhttp3.RequestBody;
+
+public class CreateOrder {
+ private class CreateOrderData {
+ String AppId;
+ String AppUser;
+ String AppTime;
+ String Amount;
+ String AppTransId;
+ String EmbedData;
+ String Items;
+ String BankCode;
+ String Description;
+ String Mac;
+
+ private CreateOrderData(String amount) throws Exception {
+ long appTime = new Date().getTime();
+ AppId = String.valueOf(AppInfo.APP_ID);
+ AppUser = "Android_Demo";
+ AppTime = String.valueOf(appTime);
+ Amount = amount;
+ AppTransId = Helpers.getAppTransId();
+ EmbedData = "{}";
+ Items = "[]";
+ BankCode = "zalopayapp";
+ Description = "Hoá đơn thanh toán cho đơn hàng #" + Helpers.getAppTransId();
+ String inputHMac = String.format("%s|%s|%s|%s|%s|%s|%s",
+ this.AppId,
+ this.AppTransId,
+ this.AppUser,
+ this.Amount,
+ this.AppTime,
+ this.EmbedData,
+ this.Items);
+
+ Mac = Helpers.getMac(AppInfo.MAC_KEY, inputHMac);
+ }
+ }
+
+ public JSONObject createOrder(String amount) throws Exception {
+ CreateOrderData input = new CreateOrderData(amount);
+
+ RequestBody formBody = new FormBody.Builder()
+ .add("app_id", input.AppId)
+ .add("app_user", input.AppUser)
+ .add("app_time", input.AppTime)
+ .add("amount", input.Amount)
+ .add("app_trans_id", input.AppTransId)
+ .add("embed_data", input.EmbedData)
+ .add("item", input.Items)
+ .add("bank_code", input.BankCode)
+ .add("description", input.Description)
+ .add("mac", input.Mac)
+ .build();
+
+ JSONObject data = HttpProvider.sendPost(AppInfo.URL_CREATE_ORDER, formBody);
+ return data;
+ }
+}
+
diff --git a/app/src/main/java/com/capstone/foodify/ZaloPay/Api/HttpProvider.java b/app/src/main/java/com/capstone/foodify/ZaloPay/Api/HttpProvider.java
new file mode 100644
index 0000000..0a2c72b
--- /dev/null
+++ b/app/src/main/java/com/capstone/foodify/ZaloPay/Api/HttpProvider.java
@@ -0,0 +1,58 @@
+package com.capstone.foodify.ZaloPay.Api;
+
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.CipherSuite;
+import okhttp3.ConnectionSpec;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.TlsVersion;
+
+public class HttpProvider {
+ public static JSONObject sendPost(String URL, RequestBody formBody) {
+ JSONObject data = new JSONObject();
+ try {
+ ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .cipherSuites(
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256)
+ .build();
+
+ OkHttpClient client = new OkHttpClient.Builder()
+ .connectionSpecs(Collections.singletonList(spec))
+ .callTimeout(5000, TimeUnit.MILLISECONDS)
+ .build();
+
+ Request request = new Request.Builder()
+ .url(URL)
+ .addHeader("Content-Type", "application/x-www-form-urlencoded")
+ .post(formBody)
+ .build();
+
+ Response response = client.newCall(request).execute();
+
+ if (!response.isSuccessful()) {
+ Log.println(Log.ERROR, "BAD_REQUEST", response.body().string());
+ data = null;
+ } else {
+ data = new JSONObject(response.body().string());
+ }
+
+ } catch (IOException | JSONException e) {
+ e.printStackTrace();
+ }
+
+ return data;
+ }
+}
diff --git a/app/src/main/java/com/capstone/foodify/ZaloPay/Constant/AppInfo.java b/app/src/main/java/com/capstone/foodify/ZaloPay/Constant/AppInfo.java
new file mode 100644
index 0000000..45e77ff
--- /dev/null
+++ b/app/src/main/java/com/capstone/foodify/ZaloPay/Constant/AppInfo.java
@@ -0,0 +1,7 @@
+package com.capstone.foodify.ZaloPay.Constant;
+
+public class AppInfo {
+ public static final int APP_ID = 2553;
+ public static final String MAC_KEY = "PcY4iZIKFCIdgZvA6ueMcMHHUbRLYjPL";
+ public static final String URL_CREATE_ORDER = "https://sb-openapi.zalopay.vn/v2/create";
+}
diff --git a/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/HMac/HMacUtil.java b/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/HMac/HMacUtil.java
new file mode 100644
index 0000000..f9a6ec9
--- /dev/null
+++ b/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/HMac/HMacUtil.java
@@ -0,0 +1,93 @@
+package com.capstone.foodify.ZaloPay.Helper.HMac;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.LinkedList;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class HMacUtil {
+ public final static String HMACMD5 = "HmacMD5";
+ public final static String HMACSHA1 = "HmacSHA1";
+ public final static String HMACSHA256 = "HmacSHA256";
+ public final static String HMACSHA512 = "HmacSHA512";
+ public final static Charset UTF8CHARSET = StandardCharsets.UTF_8;
+
+ public final static LinkedList HMACS = new LinkedList(Arrays.asList("UnSupport", "HmacSHA256", "HmacMD5", "HmacSHA384", "HMacSHA1", "HmacSHA512"));
+ // @formatter:on
+
+ private static byte[] HMacEncode(final String algorithm, final String key, final String data) {
+ Mac macGenerator = null;
+ try {
+ macGenerator = Mac.getInstance(algorithm);
+ SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
+ macGenerator.init(signingKey);
+ } catch (Exception ex) {
+ }
+
+ if (macGenerator == null) {
+ return null;
+ }
+
+ byte[] dataByte = null;
+ try {
+ dataByte = data.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ }
+
+ return macGenerator.doFinal(dataByte);
+ }
+
+ /**
+ * Calculating a message authentication code (MAC) involving a cryptographic
+ * hash function in combination with a secret cryptographic key.
+ *
+ * The result will be represented base64-encoded string.
+ *
+ * @param algorithm A cryptographic hash function (such as MD5 or SHA-1)
+ *
+ * @param key A secret cryptographic key
+ *
+ * @param data The message to be authenticated
+ *
+ * @return Base64-encoded HMAC String
+ */
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static String HMacBase64Encode(final String algorithm, final String key, final String data) {
+ byte[] hmacEncodeBytes = HMacEncode(algorithm, key, data);
+ if (hmacEncodeBytes == null) {
+ return null;
+ }
+ return Base64.getEncoder().encodeToString(hmacEncodeBytes);
+ }
+
+ /**
+ * Calculating a message authentication code (MAC) involving a cryptographic
+ * hash function in combination with a secret cryptographic key.
+ *
+ * The result will be represented hex string.
+ *
+ * @param algorithm A cryptographic hash function (such as MD5 or SHA-1)
+ *
+ * @param key A secret cryptographic key
+ *
+ * @param data The message to be authenticated
+ *
+ * @return Hex HMAC String
+ */
+ public static String HMacHexStringEncode(final String algorithm, final String key, final String data) {
+ byte[] hmacEncodeBytes = HMacEncode(algorithm, key, data);
+ if (hmacEncodeBytes == null) {
+ return null;
+ }
+ return HexStringUtil.byteArrayToHexString(hmacEncodeBytes);
+ }
+}
diff --git a/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/HMac/HexStringUtil.java b/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/HMac/HexStringUtil.java
new file mode 100644
index 0000000..80cee8c
--- /dev/null
+++ b/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/HMac/HexStringUtil.java
@@ -0,0 +1,69 @@
+package com.capstone.foodify.ZaloPay.Helper.HMac;
+
+import java.util.Locale;
+
+public class HexStringUtil {
+ private static final byte[] HEX_CHAR_TABLE = {
+ (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+ (byte) '4', (byte) '5', (byte) '6', (byte) '7',
+ (byte) '8', (byte) '9', (byte) 'a', (byte) 'b',
+ (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f'
+ };
+ // @formatter:on
+
+ /**
+ * Convert a byte array to a hexadecimal string
+ *
+ * @param raw
+ * A raw byte array
+ *
+ * @return Hexadecimal string
+ */
+ public static String byteArrayToHexString(byte[] raw) {
+ byte[] hex = new byte[2 * raw.length];
+ int index = 0;
+
+ for (byte b : raw) {
+ int v = b & 0xFF;
+ hex[index++] = HEX_CHAR_TABLE[v >>> 4];
+ hex[index++] = HEX_CHAR_TABLE[v & 0xF];
+ }
+ return new String(hex);
+ }
+
+ /**
+ * Convert a hexadecimal string to a byte array
+ *
+ * @param hex
+ * A hexadecimal string
+ *
+ * @return The byte array
+ */
+ public static byte[] hexStringToByteArray(String hex) {
+ String hexstandard = hex.toLowerCase(Locale.ENGLISH);
+ int sz = hexstandard.length() / 2;
+ byte[] bytesResult = new byte[sz];
+
+ int idx = 0;
+ for (int i = 0; i < sz; i++) {
+ bytesResult[i] = (byte) (hexstandard.charAt(idx));
+ ++idx;
+ byte tmp = (byte) (hexstandard.charAt(idx));
+ ++idx;
+
+ if (bytesResult[i] > HEX_CHAR_TABLE[9]) {
+ bytesResult[i] -= ((byte) ('a') - 10);
+ } else {
+ bytesResult[i] -= (byte) ('0');
+ }
+ if (tmp > HEX_CHAR_TABLE[9]) {
+ tmp -= ((byte) ('a') - 10);
+ } else {
+ tmp -= (byte) ('0');
+ }
+
+ bytesResult[i] = (byte) (bytesResult[i] * 16 + tmp);
+ }
+ return bytesResult;
+ }
+}
diff --git a/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/Helpers.java b/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/Helpers.java
new file mode 100644
index 0000000..0b03a5b
--- /dev/null
+++ b/app/src/main/java/com/capstone/foodify/ZaloPay/Helper/Helpers.java
@@ -0,0 +1,35 @@
+package com.capstone.foodify.ZaloPay.Helper;
+
+import android.annotation.SuppressLint;
+
+import com.capstone.foodify.ZaloPay.Helper.HMac.HMacUtil;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Objects;
+
+public class Helpers {
+ private static int transIdDefault = 1;
+
+ @NotNull
+ @SuppressLint("DefaultLocale")
+ public static String getAppTransId() {
+ if (transIdDefault >= 100000) {
+ transIdDefault = 1;
+ }
+
+ transIdDefault += 1;
+ @SuppressLint("SimpleDateFormat") SimpleDateFormat formatDateTime = new SimpleDateFormat("yyMMdd_hhmmss");
+ String timeString = formatDateTime.format(new Date());
+ return String.format("%s%06d", timeString, transIdDefault);
+ }
+
+ @NotNull
+ public static String getMac(@NotNull String key, @NotNull String data) throws NoSuchAlgorithmException, InvalidKeyException {
+ return Objects.requireNonNull(HMacUtil.HMacHexStringEncode(HMacUtil.HMACSHA256, key, data));
+ }
+}