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)); + } +}