build.gradle
// Add the Retrofit library
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// Add the Volley library
implementation("com.android.volley:volley:1.2.0")
// Add the OkHttp library
implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("com.github.bumptech.glide:glide:4.16.0")
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" /> <!-- Permissions for Android 12L (API level 32) or lower -->
<application
...
android:usesCleartextTraffic="true"
...
>
</application>
activity_product_details.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="<http://schemas.android.com/apk/res/android>"
xmlns:app="<http://schemas.android.com/apk/res-auto>"
xmlns:tools="<http://schemas.android.com/tools>"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/arrow_left"
app:title="Product Details" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Product Image -->
<ImageView
android:id="@+id/product_image"
android:layout_width="match_parent"
android:layout_height="250dp"
android:scaleType="centerCrop" />
<!-- Product Name -->
<TextView
android:id="@+id/product_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:paddingTop="8dp" />
<!-- Product Description -->
<TextView
android:id="@+id/product_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:paddingTop="4dp" />
<!-- Product Price -->
<TextView
android:id="@+id/product_price"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/black"
android:paddingTop="8dp" />
<!-- Product Category -->
<com.google.android.material.chip.Chip
android:id="@+id/product_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="true"
android:paddingTop="4dp" />
<!-- Date Added -->
<TextView
android:id="@+id/product_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:paddingTop="4dp"/>
<!-- Make Order Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/make_order"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Make Order"/>
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
package com.example.gasswift.models;
public class Order {
private String id;
private String productId;
private String type; // "product" or "refill"
private String userId;
private int amountPaid;
private long timestamp;
public Order() {
}
public Order(String id, String productId, String type, String userId, int amountPaid, long timestamp) {
this.id = id;
this.productId = productId;
this.type = type;
this.userId = userId;
this.amountPaid = amountPaid;
this.timestamp = timestamp;
}
public String getId() {
return id;
}
public String getProductId() {
return productId;
}
public String getType() {
return type;
}
public String getUserId() {
return userId;
}
public int getAmountPaid() {
return amountPaid;
}
public long getTimestamp() {
return timestamp;
}
}
ProductDetailsActivity.java
package com.example.gasswift;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.bumptech.glide.Glide;
import com.example.gasswift.models.Order;
import com.example.gasswift.models.Product;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.chip.Chip;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
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.ValueEventListener;
import org.json.JSONObject;
import java.io.IOException;
import java.util.Objects;
import java.util.regex.Pattern;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ProductDetailsActivity extends AppCompatActivity {
private ImageView productImage;
private TextView productName, productDescription, productPrice, productDate;
private Chip productCategory;
private MaterialButton makeOrderButton;
private DatabaseReference databaseReference;
private String productId;
private int productAmount;
private String appName="Gas Swift - by Linda";
private static final String API_URL = "<https://api-mpesa.kuwesakenya.com/pay>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_product_details);
initializeUI();
retrieveProductId();
fetchProductDetails();
makeOrderButton.setOnClickListener(v -> showPaymentDialog());
}
/**
* Initializes UI components.
*/
private void initializeUI() {
productImage = findViewById(R.id.product_image);
productName = findViewById(R.id.product_name);
productDescription = findViewById(R.id.product_description);
productPrice = findViewById(R.id.product_price);
productCategory = findViewById(R.id.product_category);
productDate = findViewById(R.id.product_date);
makeOrderButton = findViewById(R.id.make_order);
MaterialToolbar toolbar = findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v -> finish());
databaseReference = FirebaseDatabase.getInstance().getReference("products");
}
/**
* Retrieves the product ID from the intent.
*/
private void retrieveProductId() {
productId = getIntent().getStringExtra("productId");
if (productId == null) {
showToast("Error: Product not found");
finish();
}
}
/**
* Fetches product details from Firebase.
*/
private void fetchProductDetails() {
databaseReference.child(productId).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (snapshot.exists()) {
Product product = snapshot.getValue(Product.class);
if (product != null) {
updateUIWithProductDetails(product);
}
} else {
showToast("Product not found");
finish();
}
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
showToast("Failed to load product");
}
});
}
/**
* Updates UI with product details.
*/
private void updateUIWithProductDetails(Product product) {
productName.setText(product.getName());
productDescription.setText(product.getDescription());
productPrice.setText("Price: KSh. " + product.getPrice());
productCategory.setText(product.getCategory());
productDate.setText("Added on: " + product.getFormattedDate());
try {
productAmount = Integer.parseInt(product.getPrice());
} catch (NumberFormatException e) {
productAmount = 0;
}
Glide.with(this)
.load(product.getImageUrl())
.placeholder(R.drawable.placeholder)
.into(productImage);
}
/**
* Displays a dialog for the user to enter their phone number.
*/
private void showPaymentDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Enter Phone Number");
// Create EditText programmatically
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_PHONE);
input.setHint("07********");
// Set padding for better UI
int padding = (int) (16 * getResources().getDisplayMetrics().density);
input.setPadding(padding, padding, padding, padding);
input.setPadding(padding, padding, padding, padding);
builder.setView(input);
builder.setPositiveButton("Pay", (dialog, which) -> {
String phoneNumber = input.getText().toString().trim();
if (isValidPhoneNumber(phoneNumber)) {
makePayment(formatPhoneNumber(phoneNumber));
} else {
showToast("Invalid phone number! Use format 07********");
}
});
builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
builder.show();
}
/**
* Validates if the phone number is in the format 07********.
*/
private boolean isValidPhoneNumber(String phoneNumber) {
return Pattern.matches("^07\\\\d{8}$", phoneNumber);
}
/**
* Converts phone number to international format (2547********).
*/
private String formatPhoneNumber(String phoneNumber) {
return "254" + phoneNumber.substring(1);
}
/**
* Initiates the payment request.
*/
private void makePayment(String phoneNumber) {
ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Processing payment...");
progressDialog.setCancelable(false);
progressDialog.show();
new Thread(() -> {
try {
OkHttpClient client = new OkHttpClient();
// Create JSON request payload
JSONObject jsonObject = new JSONObject();
jsonObject.put("phoneNumber", phoneNumber);
jsonObject.put("amount", productAmount);
jsonObject.put("accountReference", appName);
jsonObject.put("transactionDesc", "Payment for " + productName.getText().toString());
RequestBody body = RequestBody.create(jsonObject.toString(), MediaType.get("application/json"));
Request request = new Request.Builder()
.url(API_URL)
.post(body)
.addHeader("Content-Type", "application/json")
.build();
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
String responseData = Objects.requireNonNull(response.body()).string();
Log.d("PaymentResponse", responseData);
// Generate a unique order ID
String orderId = FirebaseDatabase.getInstance().getReference("orders").push().getKey();
// Create and save order in Firebase
if (orderId != null) {
saveOrderToFirebase(orderId, phoneNumber);
}
runOnUiThread(() -> {
progressDialog.dismiss();
paymentComplete(responseData);
});
} else {
runOnUiThread(() -> {
progressDialog.dismiss();
paymentFailed("Error: " + response.message());
});
}
} catch (Exception e) {
e.printStackTrace();
runOnUiThread(() -> {
progressDialog.dismiss();
paymentFailed("Payment failed: " + e.getMessage());
});
}
}).start();
}
private void saveOrderToFirebase(String orderId, String phoneNumber) {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if (user == null) {
showToast("User not logged in!");
return;
}
String userId = user.getUid();
DatabaseReference ordersRef = FirebaseDatabase.getInstance().getReference("orders");
// Create an order object
Order order = new Order(orderId, productId, "product", userId, productAmount, System.currentTimeMillis());
// Save order to Firebase
ordersRef.child(orderId).setValue(order)
.addOnSuccessListener(aVoid -> showToast("Order saved successfully!"))
.addOnFailureListener(e -> showToast("Failed to save order: " + e.getMessage()));
}
/**
* Handles successful payment response.
*/
private void paymentComplete(String responseData) {
showToast("Payment Successful: " + responseData);
}
/**
* Handles failed payment response.
*/
private void paymentFailed(String errorMessage) {
showToast(errorMessage);
}
/**
* Displays a toast message.
*/
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}