From e0ee6f1f8aea22c3c67fe5b83ffa9201673ef4b9 Mon Sep 17 00:00:00 2001 From: phanhuuloi27 <73208452+Nezumi2711@users.noreply.github.com> Date: Tue, 28 Jun 2022 21:22:28 +0700 Subject: [PATCH] Add ZaloPay method --- Foodify/app/build.gradle | 3 + Foodify/app/src/main/AndroidManifest.xml | 15 + .../main/java/com/waterbase/foodify/Cart.java | 63 +---- .../com/waterbase/foodify/Model/Order.java | 4 +- .../com/waterbase/foodify/Model/Request.java | 15 +- .../com/waterbase/foodify/OrderDetail.java | 264 ++++++++++++++++++ .../foodify/ViewHolder/CartAdapter.java | 2 +- .../ViewHolder/OrderDetailAdapter.java | 83 ++++++ .../foodify/Zalopay/Api/CreateOrder.java | 70 +++++ .../foodify/Zalopay/Api/HttpProvider.java | 58 ++++ .../foodify/Zalopay/Constant/AppInfo.java | 7 + .../foodify/Zalopay/Helper/HMac/HMacUtil.java | 93 ++++++ .../Zalopay/Helper/HMac/HexStringUtil.java | 69 +++++ .../foodify/Zalopay/Helper/Helpers.java | 35 +++ .../app/src/main/res/drawable/zalopayfix.png | Bin 0 -> 7081 bytes .../app/src/main/res/layout/activity_cart.xml | 20 +- .../main/res/layout/activity_order_detail.xml | 143 ++++++++++ .../main/res/layout/order_detail_layout.xml | 85 ++++++ 18 files changed, 970 insertions(+), 59 deletions(-) create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/OrderDetail.java create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/OrderDetailAdapter.java create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/CreateOrder.java create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/HttpProvider.java create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Constant/AppInfo.java create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HMacUtil.java create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HexStringUtil.java create mode 100644 Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/Helpers.java create mode 100644 Foodify/app/src/main/res/drawable/zalopayfix.png create mode 100644 Foodify/app/src/main/res/layout/activity_order_detail.xml create mode 100644 Foodify/app/src/main/res/layout/order_detail_layout.xml diff --git a/Foodify/app/build.gradle b/Foodify/app/build.gradle index 0d8e469..0790b12 100644 --- a/Foodify/app/build.gradle +++ b/Foodify/app/build.gradle @@ -43,6 +43,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' implementation 'androidx.navigation:navigation-fragment:2.3.5' implementation 'androidx.navigation:navigation-ui:2.3.5' + implementation fileTree(dir: 'D:\\ZaloPay', include: ['*.aar', '*.jar'], exclude: []) testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' @@ -68,6 +69,8 @@ dependencies { implementation 'com.github.rey5137:material:1.2.4' implementation 'com.squareup.okhttp3:okhttp:4.9.2' + implementation("commons-codec:commons-codec:1.14") + implementation fileTree(dir: 'D:\\ZaloPay', includes: ['*.aar', '*.jar'], excludes: []) implementation 'io.github.inflationx:calligraphy3:3.1.1' implementation 'io.github.inflationx:viewpump:2.0.3' diff --git a/Foodify/app/src/main/AndroidManifest.xml b/Foodify/app/src/main/AndroidManifest.xml index 788325c..cac6fa1 100644 --- a/Foodify/app/src/main/AndroidManifest.xml +++ b/Foodify/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + + + + + + + + + + + diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/Cart.java b/Foodify/app/src/main/java/com/waterbase/foodify/Cart.java index 6adb614..a8733fe 100644 --- a/Foodify/app/src/main/java/com/waterbase/foodify/Cart.java +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Cart.java @@ -3,6 +3,7 @@ package com.waterbase.foodify; import android.Manifest; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageManager; import android.location.Location; import android.os.Bundle; @@ -314,20 +315,24 @@ public class Cart extends AppCompatActivity implements GoogleApiClient.Connectio txtTotalPrice.getText().toString(), "0", edtComment.getText().toString(), + "Chưa thanh toán!", cart ); + //Pass object to another activity! + Intent intent = new Intent(Cart.this, OrderDetail.class); + intent.putExtra("requests", request); + startActivity(intent); + //Summit to Firebase //We will using System.CurrentMilli to key - String order_number = String.valueOf(System.currentTimeMillis()); - requests.child(order_number).setValue(request); +// String order_number = String.valueOf(System.currentTimeMillis()); +// requests.child(order_number).setValue(request); +// +// sendNotificationOrder(order_number); + - sendNotificationOrder(order_number); - //Delete Cart - new Database(getBaseContext()).cleanCart(); - Toast.makeText(Cart.this, "Đặt hàng thành công!", Toast.LENGTH_SHORT).show(); - finish(); } }); @@ -341,51 +346,7 @@ public class Cart extends AppCompatActivity implements GoogleApiClient.Connectio alertDialog.show(); } - private void sendNotificationOrder(String order_number) { - DatabaseReference tokens = FirebaseDatabase.getInstance().getReference("Tokens"); - Query data = tokens.orderByChild("serverToken").equalTo(true); //Get all node with isServerToken is true; - data.addValueEventListener(new ValueEventListener() { - @Override - public void onDataChange(DataSnapshot dataSnapshot) { - for (DataSnapshot postSnapShot : dataSnapshot.getChildren()) { - Token serverToken = postSnapShot.getValue(Token.class); - - //Create raw payload to send - Notification notification = new Notification("Foodify", "Bạn có 1 đơn hàng mới " + order_number); - Sender content = new Sender(serverToken.getToken(), notification); - - mService.sendNotification(content) - .enqueue(new Callback() { - @Override - public void onResponse(Call call, Response response) { - - //Only run when get result - if (response.code() == 200) { - if (response.body().success == 1) { - new Database(getBaseContext()).cleanCart(); - Toast.makeText(Cart.this, "Đặt hàng thành công!", Toast.LENGTH_SHORT).show(); - finish(); - } else { - Toast.makeText(Cart.this, "Hệ thống bị lỗi. Vui lòng thử lại sau!", Toast.LENGTH_SHORT).show(); - } - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e("ERROR", t.getMessage()); - } - }); - } - } - - @Override - public void onCancelled(DatabaseError databaseError) { - - } - }); - } private void loadListFood() { cart = new Database(this).getCarts(); diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/Model/Order.java b/Foodify/app/src/main/java/com/waterbase/foodify/Model/Order.java index 1bcb6fa..40a69a5 100644 --- a/Foodify/app/src/main/java/com/waterbase/foodify/Model/Order.java +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Model/Order.java @@ -1,6 +1,8 @@ package com.waterbase.foodify.Model; -public class Order { +import java.io.Serializable; + +public class Order implements Serializable { private int ID; private String ProductId; diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/Model/Request.java b/Foodify/app/src/main/java/com/waterbase/foodify/Model/Request.java index 7a8943e..41b6816 100644 --- a/Foodify/app/src/main/java/com/waterbase/foodify/Model/Request.java +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Model/Request.java @@ -1,26 +1,29 @@ package com.waterbase.foodify.Model; +import java.io.Serializable; import java.util.List; -public class Request { +public class Request implements Serializable { private String phone; private String name; private String address; private String total; private String status; private String comment; + private String paymentStatus; private List foods; // list of food order public Request() { } - public Request(String phone, String name, String address, String total, String status, String comment, List foods) { + public Request(String phone, String name, String address, String total, String status, String comment, String paymentStatus, List foods) { this.phone = phone; this.name = name; this.address = address; this.total = total; this.status = status; this.comment = comment; + this.paymentStatus = paymentStatus; this.foods = foods; } @@ -72,6 +75,14 @@ public class Request { this.comment = comment; } + public String getPaymentStatus() { + return paymentStatus; + } + + public void setPaymentStatus(String paymentStatus) { + this.paymentStatus = paymentStatus; + } + public List getFoods() { return foods; } diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/OrderDetail.java b/Foodify/app/src/main/java/com/waterbase/foodify/OrderDetail.java new file mode 100644 index 0000000..2a51289 --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/OrderDetail.java @@ -0,0 +1,264 @@ +package com.waterbase.foodify; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.StrictMode; +import android.text.TextUtils; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.material.button.MaterialButton; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.Query; +import com.google.firebase.database.ValueEventListener; +import com.waterbase.foodify.Common.Common; +import com.waterbase.foodify.Database.Database; +import com.waterbase.foodify.Model.MyResponse; +import com.waterbase.foodify.Model.Notification; +import com.waterbase.foodify.Model.Order; +import com.waterbase.foodify.Model.Request; +import com.waterbase.foodify.Model.Sender; +import com.waterbase.foodify.Model.Token; +import com.waterbase.foodify.Remote.APIService; +import com.waterbase.foodify.ViewHolder.OrderDetailAdapter; +import com.waterbase.foodify.Zalopay.Api.CreateOrder; + +import org.json.JSONObject; + +import java.util.ArrayList; +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 OrderDetail extends AppCompatActivity { + + TextView order_address, order_total, order_comment; + Request request; + RecyclerView lstFoods; + RecyclerView.LayoutManager layoutManager; + MaterialButton btnPay, btnZaloPay; + + FirebaseDatabase database; + DatabaseReference requests; + + APIService mService; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_order_detail); + + String comment = ""; + + setTitle("Thông tin chi tiết đơn hàng"); + + StrictMode.ThreadPolicy policy = new + StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + + // ZaloPay SDK Init + ZaloPaySDK.init(2554, Environment.SANDBOX); + + order_address = (TextView) findViewById(R.id.order_address); + order_total = (TextView) findViewById(R.id.order_total); + order_comment = (TextView) findViewById(R.id.order_comment); + + btnPay = findViewById(R.id.btnPay); + btnZaloPay = findViewById(R.id.btnZaloPay); + + lstFoods = (RecyclerView) findViewById(R.id.lstFoods); + lstFoods.setHasFixedSize(true); + layoutManager = new LinearLayoutManager(this); + lstFoods.setLayoutManager(layoutManager); + + //Firebase + database = FirebaseDatabase.getInstance(); + requests = database.getReference("Requests"); + + mService = Common.getFCMService(); + + if (getIntent() != null) + request = (Request) getIntent().getSerializableExtra("requests"); + + //Set value + order_total.setText("Tổng cộng: " + request.getTotal()); + order_address.setText("Địa chỉ giao: " + request.getAddress()); + if (TextUtils.isEmpty(request.getComment())) + comment = "Không có"; + else + comment = request.getComment(); + order_comment.setText("Ghi chú: " + comment); + + OrderDetailAdapter adapter = new OrderDetailAdapter(request.getFoods(), this); + adapter.notifyDataSetChanged(); + lstFoods.setAdapter(adapter); + + btnPay.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + //Summit to Firebase + //We will using System.CurrentMilli to key + String order_number = String.valueOf(System.currentTimeMillis()); + requests.child(order_number).setValue(request); + + sendNotificationOrder(order_number); + + //Delete Cart + new Database(getBaseContext()).cleanCart(); + Toast.makeText(OrderDetail.this, "Đặt hàng thành công!", Toast.LENGTH_SHORT).show(); + startActivity(new Intent(OrderDetail.this, Home.class)); + finish(); + } + }); + + btnZaloPay.setOnClickListener(new View.OnClickListener() { + @RequiresApi(api = Build.VERSION_CODES.O) + @SuppressLint("SetTextI18n") + @Override + public void onClick(View v) { + + String totalPrice = request.getTotal().substring(0, request.getTotal().length() - 2).replace(".", ""); //Remove character " đ" + + CreateOrder orderApi = new CreateOrder(); + + try { + JSONObject data = orderApi.createOrder(totalPrice); + Log.d("Amount", totalPrice); + String code = data.getString("return_code"); + + if (code.equals("1")) { + String token = data.getString("zp_trans_token"); + ZaloPaySDK.getInstance().payOrder(OrderDetail.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() { + request.setPaymentStatus("Đã thanh toán!"); + //Summit to Firebase + //We will using System.CurrentMilli to key + String order_number = String.valueOf(System.currentTimeMillis()); + requests.child(order_number).setValue(request); + + sendNotificationOrder(order_number); + + //Delete Cart + new Database(getBaseContext()).cleanCart(); + Toast.makeText(OrderDetail.this, "Đặt hàng thành công!", Toast.LENGTH_SHORT).show(); + startActivity(new Intent(OrderDetail.this, Home.class)); + finish(); + } + + }); + } + + @Override + public void onPaymentCanceled(String zpTransToken, String appTransID) { + Toast.makeText(OrderDetail.this, "Bạn đã huỷ thanh toán!", Toast.LENGTH_SHORT).show(); + } + + @Override + public void onPaymentError(ZaloPayError zaloPayError, String zpTransToken, String appTransID) { + Toast.makeText(OrderDetail.this, "Đã xảy ra lỗi hệ thống! Xin vui lòng thử lại sau!", Toast.LENGTH_SHORT).show(); + } + }); + } + + } catch (Exception e) { + Log.e("ERROR", "" + e.getMessage()); + } + } + }); + + // calling the action bar + ActionBar actionBar = getSupportActionBar(); + + // showing the back button in action bar + actionBar.setDisplayHomeAsUpEnabled(true); + } + + private void sendNotificationOrder(String order_number) { + DatabaseReference tokens = FirebaseDatabase.getInstance().getReference("Tokens"); + Query data = tokens.orderByChild("serverToken").equalTo(true); //Get all node with isServerToken is true; + data.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot dataSnapshot) { + for (DataSnapshot postSnapShot : dataSnapshot.getChildren()) { + + Token serverToken = postSnapShot.getValue(Token.class); + + //Create raw payload to send + Notification notification = new Notification("Foodify", "Bạn có 1 đơn hàng mới " + order_number); + Sender content = new Sender(serverToken.getToken(), notification); + + mService.sendNotification(content) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + + //Only run when get result + if (response.code() == 200) { + if (response.body().success == 1) { + new Database(getBaseContext()).cleanCart(); + Toast.makeText(OrderDetail.this, "Đặt hàng thành công!", Toast.LENGTH_SHORT).show(); + finish(); + } else { + Toast.makeText(OrderDetail.this, "Hệ thống bị lỗi. Vui lòng thử lại sau!", Toast.LENGTH_SHORT).show(); + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("ERROR", t.getMessage()); + } + }); + } + } + + @Override + public void onCancelled(DatabaseError databaseError) { + + } + }); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + ZaloPaySDK.getInstance().onResult(intent); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + this.finish(); + return true; + } + return super.onOptionsItemSelected(item); + } +} \ No newline at end of file diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/CartAdapter.java b/Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/CartAdapter.java index a31c804..a91ea3e 100644 --- a/Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/CartAdapter.java +++ b/Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/CartAdapter.java @@ -94,7 +94,7 @@ public class CartAdapter extends RecyclerView.Adapter{ holder.btn_quantity.setOnValueChangeListener(new ElegantNumberButton.OnValueChangeListener() { @Override public void onValueChange(ElegantNumberButton view, int oldValue, int newValue) { - Order order = listData.get(position); + Order order = listData.get(holder.getAdapterPosition()); order.setQuantity(String.valueOf(newValue)); new Database(cart).updateCart(order); diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/OrderDetailAdapter.java b/Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/OrderDetailAdapter.java new file mode 100644 index 0000000..96e7caa --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/ViewHolder/OrderDetailAdapter.java @@ -0,0 +1,83 @@ +package com.waterbase.foodify.ViewHolder; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + + +import com.squareup.picasso.Picasso; +import com.waterbase.foodify.Cart; +import com.waterbase.foodify.Model.Order; +import com.waterbase.foodify.OrderDetail; +import com.waterbase.foodify.R; + +import java.text.NumberFormat; +import java.util.List; +import java.util.Locale; + +class MyViewHolder extends RecyclerView.ViewHolder { + + public TextView name, quantity, price, discount; + public ImageView cart_image; + + public MyViewHolder(@NonNull View itemView) { + super(itemView); + name = (TextView) itemView.findViewById(R.id.product_name); + quantity = (TextView) itemView.findViewById(R.id.product_quantity); + price = (TextView) itemView.findViewById(R.id.product_price); + discount = (TextView) itemView.findViewById(R.id.product_discount); + cart_image = (ImageView) itemView.findViewById(R.id.cart_image); + } +} + +public class OrderDetailAdapter extends RecyclerView.Adapter{ + + List myOrders; + private OrderDetail orderDetail; + + public OrderDetailAdapter(List myOrders, OrderDetail orderDetail) { + this.myOrders = myOrders; + this.orderDetail = orderDetail; + } + + @NonNull + @Override + public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.order_detail_layout, parent, false); + return new MyViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { + double totalPrice; + Order order = myOrders.get(position); + + Picasso.with(orderDetail.getBaseContext()) + .load(myOrders.get(position).getImage()) + .resize(70,70) + .centerCrop() + .into(holder.cart_image); + + totalPrice = Double.parseDouble(order.getQuantity()) * Double.parseDouble(order.getPrice()); + Locale locale = new Locale("vi", "VN"); + NumberFormat fmt = NumberFormat.getCurrencyInstance(locale); + + + holder.name.setText(String.format("Món ăn: %s", order.getProductName())); + holder.quantity.setText(String.format("Số lượng: %s", order.getQuantity())); + holder.price.setText("Giá: " + fmt.format(totalPrice)); + holder.discount.setText(String.format("Giảm giá: %s", order.getDiscount())); + + } + + @Override + public int getItemCount() { + return myOrders.size(); + } +} diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/CreateOrder.java b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/CreateOrder.java new file mode 100644 index 0000000..1c56f97 --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/CreateOrder.java @@ -0,0 +1,70 @@ +package com.waterbase.foodify.Zalopay.Api; + +import com.waterbase.foodify.Zalopay.Constant.AppInfo; +import com.waterbase.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 = "Merchant pay for order #" + 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/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/HttpProvider.java b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/HttpProvider.java new file mode 100644 index 0000000..d0ae765 --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Api/HttpProvider.java @@ -0,0 +1,58 @@ +package com.waterbase.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/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Constant/AppInfo.java b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Constant/AppInfo.java new file mode 100644 index 0000000..569bdf0 --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Constant/AppInfo.java @@ -0,0 +1,7 @@ +package com.waterbase.foodify.Zalopay.Constant; + +public class AppInfo { + public static final int APP_ID = 2554; + public static final String MAC_KEY = "sdngKKJmqEMzvh5QQcdD2A9XBSKUNaYn"; + public static final String URL_CREATE_ORDER = "https://sb-openapi.zalopay.vn/v2/create"; +} diff --git a/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HMacUtil.java b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HMacUtil.java new file mode 100644 index 0000000..b619607 --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HMacUtil.java @@ -0,0 +1,93 @@ +package com.waterbase.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/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HexStringUtil.java b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HexStringUtil.java new file mode 100644 index 0000000..22d01bc --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/HMac/HexStringUtil.java @@ -0,0 +1,69 @@ +package com.waterbase.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/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/Helpers.java b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/Helpers.java new file mode 100644 index 0000000..c194235 --- /dev/null +++ b/Foodify/app/src/main/java/com/waterbase/foodify/Zalopay/Helper/Helpers.java @@ -0,0 +1,35 @@ +package com.waterbase.foodify.Zalopay.Helper; + +import android.annotation.SuppressLint; + +import com.waterbase.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)); + } +} diff --git a/Foodify/app/src/main/res/drawable/zalopayfix.png b/Foodify/app/src/main/res/drawable/zalopayfix.png new file mode 100644 index 0000000000000000000000000000000000000000..7b5c824adaadb63f73f5655bfff33b43f1fa32f4 GIT binary patch literal 7081 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 - + android:layout_height="match_parent" + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Foodify/app/src/main/res/layout/order_detail_layout.xml b/Foodify/app/src/main/res/layout/order_detail_layout.xml new file mode 100644 index 0000000..0a195bc --- /dev/null +++ b/Foodify/app/src/main/res/layout/order_detail_layout.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file