first working version

This commit is contained in:
Achmad Zaenuri 2025-04-16 07:33:55 +07:00
commit 4eabdd81d2
57 changed files with 2831 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
BDKI POC

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

40
.idea/appInsightsSettings.xml generated Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Android Vitals">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="com.allbestsistem.asikk" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="SEVEN_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

6
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

10
.idea/deploymentTargetSelector.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

18
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="ms-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

10
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="ms-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

4
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

41
app/build.gradle Normal file
View File

@ -0,0 +1,41 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace 'com.example.bdkipoc'
compileSdk 35
defaultConfig {
applicationId "com.example.bdkipoc"
minSdk 21
targetSdk 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
implementation libs.cardview
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,26 @@
package com.example.bdkipoc;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.example.bdkipoc", appContext.getPackageName());
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BDKIPOC"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".TransactionActivity"
android:exported="false" />
<activity
android:name=".PaymentActivity"
android:exported="false" />
<activity android:name=".QrisResultActivity" />
</application>
</manifest>

View File

@ -0,0 +1,41 @@
package com.example.bdkipoc;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// Set up click listeners for the cards
CardView paymentCard = findViewById(R.id.card_payment);
CardView transactionsCard = findViewById(R.id.card_transactions);
paymentCard.setOnClickListener(v -> {
// Launch payment activity
startActivity(new android.content.Intent(MainActivity.this, PaymentActivity.class));
});
transactionsCard.setOnClickListener(v -> {
// Launch transactions activity
startActivity(new android.content.Intent(MainActivity.this, TransactionActivity.class));
});
}
}

View File

@ -0,0 +1,557 @@
package com.example.bdkipoc;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Random;
import java.util.UUID;
public class PaymentActivity extends AppCompatActivity {
private ProgressBar progressBar;
private Button initiatePaymentButton;
private Button simulatePaymentButton;
private ImageView qrCodeImageView;
private TextView statusTextView;
private EditText editTextAmount;
private TextView referenceIdTextView;
private View paymentDetailsLayout;
private View paymentSuccessLayout;
private Button returnToMainButton;
private String transactionId;
private String transactionUuid;
private String referenceId;
private int amount;
private JSONObject midtransResponse;
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; // Replace with your actual key
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_payment);
// Set up the toolbar
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
getSupportActionBar().setTitle("QRIS Payment");
}
// Initialize views
progressBar = findViewById(R.id.progressBar);
initiatePaymentButton = findViewById(R.id.initiatePaymentButton);
simulatePaymentButton = findViewById(R.id.simulatePaymentButton);
qrCodeImageView = findViewById(R.id.qrCodeImageView);
statusTextView = findViewById(R.id.statusTextView);
editTextAmount = findViewById(R.id.editTextAmount);
referenceIdTextView = findViewById(R.id.referenceIdTextView);
paymentDetailsLayout = findViewById(R.id.paymentDetailsLayout);
paymentSuccessLayout = findViewById(R.id.paymentSuccessLayout);
returnToMainButton = findViewById(R.id.returnToMainButton);
// Generate a random amount between 100,000 and 999,999
amount = new Random().nextInt(900000) + 100000;
// Format and display the amount
editTextAmount.setText("");
editTextAmount.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(editTextAmount, InputMethodManager.SHOW_IMPLICIT);
}
// Generate reference ID
referenceId = "ref-" + generateRandomString(8);
referenceIdTextView.setText(referenceId);
// Set up click listeners
initiatePaymentButton.setOnClickListener(v -> createTransaction());
simulatePaymentButton.setOnClickListener(v -> simulateWebhook());
returnToMainButton.setOnClickListener(v -> finish());
// Initially hide the QR code and payment success views
paymentDetailsLayout.setVisibility(View.GONE);
paymentSuccessLayout.setVisibility(View.GONE);
simulatePaymentButton.setVisibility(View.GONE);
}
private void createTransaction() {
progressBar.setVisibility(View.VISIBLE);
initiatePaymentButton.setEnabled(false);
statusTextView.setText("Creating transaction...");
new CreateTransactionTask().execute();
}
private void displayQrCode(String qrImageUrl) {
new DownloadImageTask().execute(qrImageUrl);
}
private void simulateWebhook() {
progressBar.setVisibility(View.VISIBLE);
simulatePaymentButton.setEnabled(false);
statusTextView.setText("Processing payment...");
new SimulateWebhookTask().execute();
}
private void showSuccessScreen() {
paymentDetailsLayout.setVisibility(View.GONE);
paymentSuccessLayout.setVisibility(View.VISIBLE);
statusTextView.setText("Payment successful!");
progressBar.setVisibility(View.GONE);
}
private String generateRandomString(int length) {
String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
int index = random.nextInt(chars.length());
sb.append(chars.charAt(index));
}
return sb.toString();
}
private String getServerKey() {
// MIDTRANS_AUTH = 'Basic base64string'
String base64 = MIDTRANS_AUTH.replace("Basic ", "");
String decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT).toString();
// Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present.
return decoded.replace(":\n", "");
}
private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
String input = orderId + statusCode + grossAmount + serverKey;
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512");
byte[] messageDigest = md.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
} catch (java.security.NoSuchAlgorithmException e) {
return "";
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> {
private String errorMessage;
@Override
protected Boolean doInBackground(Void... voids) {
try {
// Generate a UUID for the transaction
transactionUuid = UUID.randomUUID().toString();
// Create transaction JSON payload
JSONObject payload = new JSONObject();
payload.put("type", "PAYMENT");
payload.put("channel_category", "RETAIL_OUTLET");
payload.put("channel_code", "QRIS");
payload.put("reference_id", referenceId);
// Read amount from EditText and log it
String amountText = editTextAmount.getText().toString().trim();
Log.d("MidtransCharge", "Raw amount text: " + amountText);
try {
// Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
amount = Integer.parseInt(amountText);
Log.d("MidtransCharge", "Parsed amount: " + amount);
} catch (NumberFormatException e) {
Log.e("MidtransCharge", "Amount parsing error: " + e.getMessage());
errorMessage = "Invalid amount format";
return false;
}
payload.put("amount", amount);
payload.put("cashflow", "MONEY_IN");
payload.put("status", "INIT");
payload.put("device_id", 1);
payload.put("transaction_uuid", transactionUuid);
payload.put("transaction_time_seconds", 0.0);
payload.put("device_code", "PB4K252T00021");
payload.put("merchant_name", "Marcel Panjaitan");
payload.put("mid", "71000026521");
payload.put("tid", "73001500");
// Make the API call
URL url = new URI(BACKEND_BASE + "/transactions").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = payload.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
if (responseCode == 200 || responseCode == 201) {
// Read the response
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
// Parse the response to get transaction ID
JSONObject jsonResponse = new JSONObject(response.toString());
JSONObject data = jsonResponse.getJSONObject("data");
transactionId = String.valueOf(data.getInt("id"));
// Now generate QRIS via Midtrans
return generateQris(amount);
} else {
// Read error response
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
errorMessage = "Error creating transaction: " + response.toString();
return false;
}
} catch (Exception e) {
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
errorMessage = "Unexpected error: " + e.getMessage();
return false;
}
}
private boolean generateQris(int amount) {
try {
// Create QRIS charge JSON payload
JSONObject payload = new JSONObject();
payload.put("payment_type", "qris");
JSONObject transactionDetails = new JSONObject();
transactionDetails.put("order_id", transactionUuid);
transactionDetails.put("gross_amount", amount);
payload.put("transaction_details", transactionDetails);
// Log the request details
Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
Log.d("MidtransCharge", "Accept: application/json");
Log.d("MidtransCharge", "Content-Type: application/json");
Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL);
Log.d("MidtransCharge", "Payload: " + payload.toString());
// Make the API call to Midtrans
URL url = new URI(MIDTRANS_CHARGE_URL).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL);
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = payload.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
if (responseCode == 200 || responseCode == 201) {
InputStream inputStream = conn.getInputStream();
if (inputStream != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
// Parse the response
midtransResponse = new JSONObject(response.toString());
return true;
} else {
Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No input stream available";
return false;
}
} else {
InputStream errorStream = conn.getErrorStream();
if (errorStream != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8"));
StringBuilder errorResponse = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
errorResponse.append(responseLine.trim());
}
Log.e("MidtransCharge", "HTTP " + responseCode + ": " + errorResponse.toString());
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": " + errorResponse.toString();
} else {
Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available");
errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available";
}
return false;
}
} catch (Exception e) {
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
errorMessage = "Unexpected error: " + e.getMessage();
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success && midtransResponse != null) {
try {
// Extract needed values from midtransResponse
JSONObject actions = midtransResponse.getJSONArray("actions").getJSONObject(0);
String qrImageUrl = actions.getString("url");
// Extract transaction_id
String transactionId = midtransResponse.getString("transaction_id");
String transactionTime = midtransResponse.getString("transaction_time");
String acquirer = midtransResponse.getString("acquirer");
String merchantId = midtransResponse.getString("merchant_id");
String exactGrossAmount = midtransResponse.getString("gross_amount");
// Log everything before launching activity
Log.d("MidtransCharge", "Creating QrisResultActivity intent with:");
Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl);
Log.d("MidtransCharge", "amount: " + amount);
Log.d("MidtransCharge", "referenceId: " + referenceId);
Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid);
Log.d("MidtransCharge", "transaction_id: " + transactionId);
Log.d("MidtransCharge", "exactGrossAmount: " + exactGrossAmount);
// Instead of showing QR inline, launch QrisResultActivity
Intent intent = new Intent(PaymentActivity.this, QrisResultActivity.class);
intent.putExtra("qrImageUrl", qrImageUrl);
intent.putExtra("amount", amount);
intent.putExtra("referenceId", referenceId);
intent.putExtra("orderId", transactionUuid); // Order ID
intent.putExtra("transactionId", transactionId); // Actual Midtrans transaction_id
intent.putExtra("grossAmount", exactGrossAmount); // Exact gross amount from response
intent.putExtra("transactionTime", transactionTime); // For timestamp
intent.putExtra("acquirer", acquirer);
intent.putExtra("merchantId", merchantId);
try {
startActivity(intent);
} catch (Exception e) {
Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e);
Toast.makeText(PaymentActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
return;
} catch (JSONException e) {
Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
Toast.makeText(PaymentActivity.this, "Error processing QRIS response", Toast.LENGTH_LONG).show();
}
} else {
String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
initiatePaymentButton.setEnabled(true);
}
progressBar.setVisibility(View.GONE);
}
}
private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
@Override
protected Bitmap doInBackground(String... urls) {
String urlDisplay = urls[0];
Bitmap bitmap = null;
try {
URL url = new URI(urlDisplay).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
java.io.InputStream input = connection.getInputStream();
bitmap = android.graphics.BitmapFactory.decodeStream(input);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
qrCodeImageView.setImageBitmap(result);
} else {
Toast.makeText(PaymentActivity.this, "Error loading QR code image", Toast.LENGTH_LONG).show();
}
}
}
private class SimulateWebhookTask extends AsyncTask<Void, Void, Boolean> {
private String errorMessage;
@Override
protected Boolean doInBackground(Void... voids) {
try {
// Wait a moment to simulate real-world timing
Thread.sleep(1500);
// Get server key and prepare signature
String serverKey = getServerKey();
String grossAmount = String.valueOf(amount) + ".00";
String signatureKey = generateSignature(
transactionUuid,
"200",
grossAmount,
serverKey
);
// Create webhook payload
JSONObject payload = new JSONObject();
payload.put("transaction_type", "on-us");
payload.put("transaction_time", midtransResponse.getString("transaction_time"));
payload.put("transaction_status", "settlement");
payload.put("transaction_id", midtransResponse.getString("transaction_id"));
payload.put("status_message", "midtrans payment notification");
payload.put("status_code", "200");
payload.put("signature_key", signatureKey);
payload.put("settlement_time", midtransResponse.getString("transaction_time"));
payload.put("payment_type", "qris");
payload.put("order_id", transactionUuid);
payload.put("merchant_id", midtransResponse.getString("merchant_id"));
payload.put("issuer", midtransResponse.getString("acquirer"));
payload.put("gross_amount", grossAmount);
payload.put("fraud_status", "accept");
payload.put("currency", "IDR");
payload.put("acquirer", midtransResponse.getString("acquirer"));
payload.put("shopeepay_reference_number", "");
payload.put("reference_id", referenceId);
// Call the webhook URL
URL url = new URI(WEBHOOK_URL).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "application/json");
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = payload.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
if (responseCode == 200 || responseCode == 201) {
// Wait briefly to allow the backend to process
Thread.sleep(2000);
return checkTransactionStatus();
} else {
// Read error response
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
errorMessage = "Error simulating payment: " + response.toString();
return false;
}
} catch (Exception e) {
errorMessage = "Error: " + e.getMessage();
return false;
}
}
private boolean checkTransactionStatus() {
try {
// Check transaction status
URL url = new URI(BACKEND_BASE + "/transactions/" + transactionId).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
// Read the response
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
// Parse the response
JSONObject jsonResponse = new JSONObject(response.toString());
JSONObject data = jsonResponse.getJSONObject("data");
String status = data.getString("status");
return status.equalsIgnoreCase("SUCCESS");
} else {
errorMessage = "Error checking transaction status. HTTP response code: " + responseCode;
return false;
}
} catch (Exception e) {
errorMessage = "Error checking transaction status: " + e.getMessage();
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success) {
showSuccessScreen();
} else {
String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
simulatePaymentButton.setEnabled(true);
}
progressBar.setVisibility(View.GONE);
}
}
}

View File

@ -0,0 +1,287 @@
package com.example.bdkipoc;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
public class QrisResultActivity extends AppCompatActivity {
private ImageView qrImageView;
private TextView amountTextView;
private TextView referenceTextView;
private Button downloadQrisButton;
private Button checkStatusButton;
private TextView statusTextView;
private Button returnMainButton;
private ProgressBar progressBar;
private String orderId;
private String grossAmount;
private String referenceId;
private String transactionId;
private String transactionTime;
private String acquirer;
private String merchantId;
private String backendBase = "https://be-edc.msvc.app";
private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qris_result);
qrImageView = findViewById(R.id.qrImageView);
amountTextView = findViewById(R.id.amountTextView);
referenceTextView = findViewById(R.id.referenceTextView);
downloadQrisButton = findViewById(R.id.downloadQrisButton);
checkStatusButton = findViewById(R.id.checkStatusButton);
statusTextView = findViewById(R.id.statusTextView);
returnMainButton = findViewById(R.id.returnMainButton);
progressBar = findViewById(R.id.progressBar);
Intent intent = getIntent();
String qrImageUrl = intent.getStringExtra("qrImageUrl");
int amount = intent.getIntExtra("amount", 0);
referenceId = intent.getStringExtra("referenceId");
orderId = intent.getStringExtra("orderId");
grossAmount = intent.getStringExtra("grossAmount");
transactionId = intent.getStringExtra("transactionId");
transactionTime = intent.getStringExtra("transactionTime");
acquirer = intent.getStringExtra("acquirer");
merchantId = intent.getStringExtra("merchantId");
if (orderId == null || transactionId == null) {
Log.e("QrisResultFlow", "orderId or transactionId is null! Intent extras: " + intent.getExtras());
android.widget.Toast.makeText(this, "Missing transaction details!", android.widget.Toast.LENGTH_LONG).show();
}
// Get the exact amount from the grossAmount string value instead of the integer
String amountStr = "Amount: " + grossAmount;
amountTextView.setText(amountStr);
referenceTextView.setText("Reference ID: " + referenceId);
// Load QR image
new DownloadImageTask(qrImageView).execute(qrImageUrl);
// Disable check status button initially
checkStatusButton.setEnabled(false);
// Start polling for pending payment log
pollPendingPaymentLog(orderId);
// Download QRIS button
downloadQrisButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
qrImageView.setDrawingCacheEnabled(true);
Bitmap bitmap = qrImageView.getDrawingCache();
if (bitmap != null) {
saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis());
}
qrImageView.setDrawingCacheEnabled(false);
}
});
// Check Payment Status button
checkStatusButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
simulateWebhook();
}
});
// Return to Main Screen button
returnMainButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finishAffinity();
}
});
}
private static class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
ImageView bmImage;
DownloadImageTask(ImageView bmImage) {
this.bmImage = bmImage;
}
protected Bitmap doInBackground(String... urls) {
String urlDisplay = urls[0];
Bitmap bitmap = null;
try {
URL url = new URI(urlDisplay).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.connect();
InputStream input = connection.getInputStream();
bitmap = BitmapFactory.decodeStream(input);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
protected void onPostExecute(Bitmap result) {
if (result != null) {
bmImage.setImageBitmap(result);
}
}
}
// Save bitmap to gallery
private void saveImageToGallery(Bitmap bitmap, String fileName) {
try {
String savedImageURL = android.provider.MediaStore.Images.Media.insertImage(
getContentResolver(), bitmap, fileName, "QRIS Payment QR Code");
if (savedImageURL != null) {
android.widget.Toast.makeText(this, "QRIS saved to gallery", android.widget.Toast.LENGTH_SHORT).show();
} else {
android.widget.Toast.makeText(this, "Failed to save QRIS", android.widget.Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
android.widget.Toast.makeText(this, "Error saving QRIS: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
}
}
private void pollPendingPaymentLog(final String orderId) {
Log.d("QrisResultFlow", "Polling for orderId (transaction_uuid): " + orderId);
progressBar.setVisibility(View.VISIBLE);
new Thread(() -> {
int maxAttempts = 10;
int intervalMs = 1500;
int attempt = 0;
boolean found = false;
while (attempt < maxAttempts && !found) {
try {
String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}";
Log.d("QrisResultFlow", "Polling URL: " + urlStr);
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
JSONObject json = new JSONObject(response.toString());
JSONArray results = json.optJSONArray("results");
if (results != null && results.length() > 0) {
for (int i = 0; i < results.length(); i++) {
JSONObject log = results.getJSONObject(i);
JSONObject reqBody = log.optJSONObject("request_body");
if (reqBody != null && "pending".equals(reqBody.optString("transaction_status"))) {
found = true;
break;
}
}
}
}
} catch (Exception e) {
Log.e("QrisResultFlow", "Polling error: " + e.getMessage(), e);
}
if (!found) {
attempt++;
try { Thread.sleep(intervalMs); } catch (InterruptedException ignored) {}
}
}
final boolean logFound = found;
new Handler(Looper.getMainLooper()).post(() -> {
progressBar.setVisibility(View.GONE);
if (logFound) {
checkStatusButton.setEnabled(true);
android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log found!", android.widget.Toast.LENGTH_SHORT).show();
} else {
android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log NOT found.", android.widget.Toast.LENGTH_LONG).show();
}
});
}).start();
}
// Simulate webhook callback
private void simulateWebhook() {
progressBar.setVisibility(View.VISIBLE);
new Thread(() -> {
try {
JSONObject payload = new JSONObject();
payload.put("transaction_type", "on-us");
payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
payload.put("transaction_status", "settlement");
payload.put("transaction_id", transactionId); // Use the actual transaction_id
payload.put("status_message", "midtrans payment notification");
payload.put("status_code", "200");
payload.put("signature_key", "dummy_signature");
payload.put("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
payload.put("payment_type", "qris");
payload.put("order_id", orderId); // Use order_id
payload.put("merchant_id", merchantId != null ? merchantId : "DUMMY_MERCHANT_ID");
payload.put("issuer", acquirer != null ? acquirer : "gopay");
payload.put("gross_amount", grossAmount); // Use exact gross amount
payload.put("fraud_status", "accept");
payload.put("currency", "IDR");
payload.put("acquirer", acquirer != null ? acquirer : "gopay");
payload.put("shopeepay_reference_number", "");
payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID");
Log.d("QrisResultFlow", "Webhook payload: " + payload.toString());
URL url = new URL(webhookUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
os.write(payload.toString().getBytes());
os.flush();
os.close();
int responseCode = conn.getResponseCode();
BufferedReader br = new BufferedReader(new InputStreamReader(
responseCode < 400 ? conn.getInputStream() : conn.getErrorStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
Log.d("QrisResultFlow", "Webhook response: " + response.toString());
} catch (Exception e) {
Log.e("QrisResultFlow", "Webhook error: " + e.getMessage(), e);
}
new Handler(Looper.getMainLooper()).post(() -> {
progressBar.setVisibility(View.GONE);
// Proceed to show status/result
qrImageView.setVisibility(View.GONE);
amountTextView.setVisibility(View.GONE);
referenceTextView.setVisibility(View.GONE);
downloadQrisButton.setVisibility(View.GONE);
checkStatusButton.setVisibility(View.GONE);
statusTextView.setVisibility(View.VISIBLE);
returnMainButton.setVisibility(View.VISIBLE);
});
}).start();
}
}

View File

@ -0,0 +1,204 @@
package com.example.bdkipoc;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public class TransactionActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private TransactionAdapter adapter;
private ProgressBar progressBar;
private FloatingActionButton refreshButton;
private int page = 0;
private final int limit = 10;
private boolean isLoading = false;
private boolean isLastPage = false;
private List<Transaction> transactionList = new ArrayList<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transaction);
// Set up the toolbar as the action bar
androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Enable the back button in the action bar
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
recyclerView = findViewById(R.id.recyclerView);
progressBar = findViewById(R.id.progressBar);
refreshButton = findViewById(R.id.refreshButton);
adapter = new TransactionAdapter(transactionList);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
refreshButton.setOnClickListener(v -> refreshTransactions());
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (!recyclerView.canScrollVertically(1) && !isLoading && !isLastPage) {
loadTransactions(page + 1);
}
}
});
loadTransactions(0);
}
private void refreshTransactions() {
page = 0;
isLastPage = false;
transactionList.clear();
adapter.notifyDataSetChanged();
loadTransactions(0);
}
private void loadTransactions(int pageToLoad) {
isLoading = true;
progressBar.setVisibility(View.VISIBLE);
new FetchTransactionsTask(pageToLoad).execute();
}
private class FetchTransactionsTask extends AsyncTask<Void, Void, List<Transaction>> {
private int pageToLoad;
private boolean error = false;
private int total = 0;
FetchTransactionsTask(int page) {
this.pageToLoad = page;
}
@Override
protected List<Transaction> doInBackground(Void... voids) {
List<Transaction> result = new ArrayList<>();
try {
String urlString = "https://be-edc.msvc.app/transactions?page=" + pageToLoad + "&limit=" + limit + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=id";
URI uri = new URI(urlString);
URL url = uri.toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("accept", "*/*");
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
JSONObject jsonObject = new JSONObject(response.toString());
JSONObject results = jsonObject.getJSONObject("results");
total = results.getInt("total");
JSONArray data = results.getJSONArray("data");
for (int i = 0; i < data.length(); i++) {
JSONObject t = data.getJSONObject(i);
Transaction tx = new Transaction(
t.getInt("id"),
t.getString("type"),
t.getString("channel_category"),
t.getString("channel_code"),
t.getString("reference_id"),
t.getString("amount"),
t.getString("cashflow"),
t.getString("status"),
t.getString("created_at"),
t.getString("merchant_name")
);
result.add(tx);
}
} else {
error = true;
}
} catch (IOException | JSONException | URISyntaxException e) {
error = true;
}
return result;
}
@Override
protected void onPostExecute(List<Transaction> transactions) {
isLoading = false;
progressBar.setVisibility(View.GONE);
if (error) {
Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
return;
}
if (pageToLoad == 0) {
transactionList.clear();
}
transactionList.addAll(transactions);
adapter.notifyDataSetChanged();
page = pageToLoad;
if (transactionList.size() >= total) {
isLastPage = true;
}
}
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item) {
if (item.getItemId() == android.R.id.home) {
// Handle the back button click
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
static class Transaction {
int id;
String type;
String channelCategory;
String channelCode;
String referenceId;
String amount;
String cashflow;
String status;
String createdAt;
String merchantName;
Transaction(int id, String type, String channelCategory, String channelCode, String referenceId, String amount, String cashflow, String status, String createdAt, String merchantName) {
this.id = id;
this.type = type;
this.channelCategory = channelCategory;
this.channelCode = channelCode;
this.referenceId = referenceId;
this.amount = amount;
this.cashflow = cashflow;
this.status = status;
this.createdAt = createdAt;
this.merchantName = merchantName;
}
}
}

View File

@ -0,0 +1,64 @@
package com.example.bdkipoc;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import java.text.NumberFormat;
import java.util.Locale;
public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.TransactionViewHolder> {
private List<TransactionActivity.Transaction> transactionList;
public TransactionAdapter(List<TransactionActivity.Transaction> transactionList) {
this.transactionList = transactionList;
}
@NonNull
@Override
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
return new TransactionViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
TransactionActivity.Transaction t = transactionList.get(position);
// Format the amount as Indonesian Rupiah
try {
double amountValue = Double.parseDouble(t.amount);
NumberFormat rupiahFormat = NumberFormat.getCurrencyInstance(new Locale.Builder().setLanguage("id").setRegion("ID").build());
holder.amount.setText(rupiahFormat.format(amountValue));
} catch (NumberFormatException e) {
holder.amount.setText("Rp " + t.amount);
}
holder.status.setText(t.status);
holder.referenceId.setText(t.referenceId);
holder.merchantName.setText(t.merchantName);
holder.createdAt.setText(t.createdAt.replace("T", " ").substring(0, 19));
}
@Override
public int getItemCount() {
return transactionList.size();
}
static class TransactionViewHolder extends RecyclerView.ViewHolder {
TextView amount, status, referenceId, merchantName, createdAt;
public TransactionViewHolder(@NonNull View itemView) {
super(itemView);
amount = itemView.findViewById(R.id.textAmount);
status = itemView.findViewById(R.id.textStatus);
referenceId = itemView.findViewById(R.id.textReferenceId);
merchantName = itemView.findViewById(R.id.textMerchantName);
createdAt = itemView.findViewById(R.id.textCreatedAt);
}
}
}

View File

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity">
<TextView
android:id="@+id/title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/main_title"
android:textSize="24sp"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/card_payment"
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_marginTop="64dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardBackgroundColor="@color/light_blue"
app:layout_constraintEnd_toStartOf="@+id/card_transactions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_text">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@android:drawable/ic_menu_send"
android:contentDescription="@string/payment"
app:tint="@color/primary_blue" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/payment2"
android:textColor="@color/primary_blue"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_transactions"
android:layout_width="140dp"
android:layout_height="140dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardBackgroundColor="@color/light_gray"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/card_payment"
app:layout_constraintTop_toTopOf="@+id/card_payment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@android:drawable/ic_menu_recent_history"
android:contentDescription="@string/transactions"
app:tint="@color/accent_teal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/transactions2"
android:textColor="@color/accent_teal"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,227 @@
<?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"
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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="QRIS Payment" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:visibility="gone" />
<TextView
android:id="@+id/statusTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="Ready to make a payment"
android:textSize="18sp" />
<!-- Initial Payment Form -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardBackgroundColor="@color/light_blue"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Amount"
android:textColor="@color/primary_blue"
android:textSize="16sp" />
<EditText
android:id="@+id/editTextAmount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Enter amount"
android:inputType="number"
android:maxLength="12"
android:importantForAutofill="no"
android:singleLine="true"
android:textColor="@color/primary_blue"
android:textSize="24sp"
android:textStyle="bold"
android:gravity="end" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Reference ID"
android:textColor="@color/primary_blue"
android:textSize="16sp" />
<TextView
android:id="@+id/referenceIdTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="ref-abcd1234"
android:textColor="@color/primary_blue"
android:textSize="16sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<Button
android:id="@+id/initiatePaymentButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:backgroundTint="@color/primary_blue"
android:text="Start Payment" />
</LinearLayout>
<!-- QR Code and Payment Details -->
<LinearLayout
android:id="@+id/paymentDetailsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:id="@+id/qrCodeImageView"
android:layout_width="250dp"
android:layout_height="250dp"
android:contentDescription="QRIS Code"
android:scaleType="fitCenter" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="Scan with your banking app or e-wallet to pay"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<Button
android:id="@+id/simulatePaymentButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:backgroundTint="@color/accent_green"
android:text="Confirm Payment" />
</LinearLayout>
<!-- Payment Success -->
<LinearLayout
android:id="@+id/paymentSuccessLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardBackgroundColor="@color/light_gray"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:contentDescription="Success Icon"
android:src="@android:drawable/ic_dialog_info"
android:tint="@color/accent_green" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="Payment Successful!"
android:textColor="@color/accent_green"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="Your transaction has been completed successfully."
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<Button
android:id="@+id/returnToMainButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:backgroundTint="@color/primary_blue"
android:text="Return to Main" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:background="#181824"
>
<TextView
android:id="@+id/amountTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/amount"
android:textColor="#2D5DA1"
android:textSize="20sp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"/>
<TextView
android:id="@+id/referenceTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reference_id"
android:textColor="#2D5DA1"
android:textSize="16sp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:layout_marginTop="8dp"/>
<ImageView
android:id="@+id/qrImageView"
android:layout_width="280dp"
android:layout_height="280dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:contentDescription="@string/qr_code"/>
<Button
android:id="@+id/downloadQrisButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/download_qris"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"/>
<Button
android:id="@+id/checkStatusButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/check_payment_status"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:visibility="gone"/>
<TextView
android:id="@+id/statusTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/payment_status_success"
android:textColor="#2D5DA1"
android:textSize="20sp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:visibility="gone"/>
<Button
android:id="@+id/returnMainButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/return_main"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:visibility="gone"/>
</LinearLayout>

View File

@ -0,0 +1,54 @@
<?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"
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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="Transactions History" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/refreshButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="Refresh"
android:src="@android:drawable/ic_popup_sync"
app:tint="@android:color/white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="@android:color/white"
android:layout_marginBottom="8dp"
android:elevation="2dp">
<TextView
android:id="@+id/textAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Amount"
android:textStyle="bold"
android:textSize="18sp" />
<TextView
android:id="@+id/textStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Status"
android:textColor="#4CAF50"
android:textStyle="bold" />
<TextView
android:id="@+id/textReferenceId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reference ID" />
<TextView
android:id="@+id/textMerchantName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Merchant Name" />
<TextView
android:id="@+id/textCreatedAt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Created At" />
</LinearLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.BDKIPOC" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Banking App Theme Colors -->
<color name="primary_blue">#1976D2</color>
<color name="light_blue">#BBDEFB</color>
<color name="accent_teal">#009688</color>
<color name="accent_orange">#FF5722</color>
<color name="accent_purple">#673AB7</color>
<color name="accent_green">#4CAF50</color>
<color name="light_gray">#F5F5F5</color>
<color name="medium_gray">#E0E0E0</color>
<color name="dark_gray">#757575</color>
</resources>

View File

@ -0,0 +1,16 @@
<resources>
<string name="app_name">BDKI POC</string>
<string name="payment">Payment (QRIS)</string>
<string name="payment2">Payment (QRIS)</string>
<string name="transactions">Transactions</string>
<string name="transactions2">Transactions</string>
<string name="amount">Amount:</string>
<string name="reference_id">Reference ID:</string>
<string name="qr_code">QR Code</string>
<string name="back">Back</string>
<string name="download_qris">Download QRIS</string>
<string name="check_payment_status">Check Payment Status</string>
<string name="payment_status_success">Payment Successful!</string>
<string name="return_main">Return to Main Screen</string>
<string name="main_title">POC</string>
</resources>

View File

@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.BDKIPOC" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.BDKIPOC" parent="Base.Theme.BDKIPOC" />
</resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,17 @@
package com.example.bdkipoc;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

4
build.gradle Normal file
View File

@ -0,0 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}

21
gradle.properties Normal file
View File

@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

24
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,24 @@
[versions]
agp = "8.9.1"
cardview = "1.0.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
material = "1.12.0"
activity = "1.10.1"
constraintlayout = "2.2.1"
[libraries]
cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Wed Apr 16 03:16:46 WIB 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Normal file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

250
poc-payment-flow.md Normal file
View File

@ -0,0 +1,250 @@
# POC Payment Flow
The payment flow is roughly following this scenario (based on NodeJK's POC end-to-end flow):
```javascript
// End-to-end QRIS payment test via Midtrans for https://be-edc.msvc.app/
// Requires: axios, uuid, sleep-promise
// Run: node midtrans-e2e.test.js
require('dotenv').config();
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const sleep = require('sleep-promise');
const crypto = require('crypto');
// --- CONFIG ---
const BACKEND_BASE = 'https://be-edc.msvc.app';
const MIDTRANS_CHARGE_URL = 'https://api.sandbox.midtrans.com/v2/charge';
const MIDTRANS_AUTH = process.env.MIDTRANS_AUTH; // Load from .env for security
const WEBHOOK_URL = 'https://be-edc.msvc.app/webhooks/midtrans';
// Helper to extract serverKey from MIDTRANS_AUTH
function getServerKey() {
// MIDTRANS_AUTH = 'Basic base64string'
const base64 = MIDTRANS_AUTH.replace('Basic ', '');
const decoded = Buffer.from(base64, 'base64').toString('utf8');
// Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present.
return decoded.replace(/:$/, '');
}
// Helper to generate signature key
function generateSignature(orderId, statusCode, grossAmount, serverKey) {
const input = `${orderId}${statusCode}${grossAmount}${serverKey}`;
return crypto.createHash('sha512').update(input).digest('hex');
}
// --- STEP 1: CREATE TRANSACTION ---
async function createTransaction() {
const amount = Math.floor(Math.random() * 900000) + 100000;
const transaction_uuid = uuidv4();
const reference_id = 'ref-' + Math.random().toString(36).substring(2, 10);
const payload = {
type: 'PAYMENT',
channel_category: 'RETAIL_OUTLET',
channel_code: 'QRIS',
reference_id,
amount,
cashflow: 'MONEY_IN',
status: 'INIT',
device_id: 1,
transaction_uuid,
transaction_time_seconds: 0.0,
device_code: 'PB4K252T00021',
merchant_name: 'Marcel Panjaitan',
mid: '71000026521',
tid: '73001500',
};
try {
const res = await axios.post(`${BACKEND_BASE}/transactions`, payload);
console.log('Step 1: Transaction create response:', res.status, res.data);
if (!res.data || !res.data.data || !res.data.data.id) throw new Error('Transaction creation failed');
return { ...payload, id: res.data.data.id };
} catch (err) {
if (err.response) {
console.error('Step 1: Transaction creation error:', err.response.status, err.response.data);
} else {
console.error('Step 1: Transaction creation error:', err.message);
}
throw new Error('Transaction creation failed');
}
}
// --- STEP 2: GENERATE QRIS VIA MIDTRANS ---
async function generateQris(transaction) {
const payload = {
payment_type: 'qris',
transaction_details: {
order_id: transaction.transaction_uuid,
gross_amount: transaction.amount,
},
};
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: MIDTRANS_AUTH,
'X-Override-Notification': WEBHOOK_URL,
};
const res = await axios.post(MIDTRANS_CHARGE_URL, payload, { headers });
if (!res.data || !res.data.transaction_id) throw new Error('Midtrans QRIS charge failed');
console.log('Step 2: Midtrans QRIS charge response:', res.data);
return res.data;
}
// --- STEP 2.5: WAIT FOR PENDING PAYMENT LOG ---
async function waitForPendingPaymentLog(transaction, maxAttempts = 10, intervalMs = 1500) {
const params = {
request_body_search_strict: { order_id: transaction.transaction_uuid }
};
let attempt = 0;
while (attempt < maxAttempts) {
const res = await axios.get(`${BACKEND_BASE}/api-logs`, { params });
let logs = res.data?.results || [];
logs = logs.filter(log => log.request_body?.transaction_status === 'pending');
if (Array.isArray(logs) && logs.length === 1) {
console.log('Step 2.5: Pending payment log found:', logs[0].id);
return logs[0];
}
if (logs.length > 1) {
throw new Error('Multiple pending payment logs found for this transaction!');
}
attempt++;
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error('Pending payment log not found in /api-logs after waiting');
}
// --- STEP 3: SIMULATE WEBHOOK CALLBACK ---
async function simulateWebhook(transaction, midtransResp) {
await sleep(3000);
const serverKey = getServerKey();
const grossAmount = typeof midtransResp.gross_amount === 'string'
? midtransResp.gross_amount
: String(midtransResp.gross_amount.toFixed(2));
const signature_key = generateSignature(
midtransResp.order_id,
'200',
grossAmount,
serverKey
);
const payload = {
transaction_type: 'on-us',
transaction_time: midtransResp.transaction_time,
transaction_status: 'settlement',
transaction_id: midtransResp.transaction_id,
status_message: 'midtrans payment notification',
status_code: '200',
signature_key,
settlement_time: midtransResp.transaction_time,
payment_type: 'qris',
order_id: midtransResp.order_id,
merchant_id: midtransResp.merchant_id,
issuer: midtransResp.acquirer,
gross_amount: grossAmount,
fraud_status: 'accept',
currency: 'IDR',
acquirer: midtransResp.acquirer,
shopeepay_reference_number: '',
reference_id: transaction.reference_id,
};
try {
const res = await axios.post(WEBHOOK_URL, payload);
console.log('Step 3: Webhook callback sent:', payload);
if (!res.data || res.data.status !== 'ok') throw new Error('Webhook callback failed');
} catch (err) {
if (err.response) {
console.error('Step 3: Webhook callback error:', err.response.status, err.response.data);
} else {
console.error('Step 3: Webhook callback error:', err.message);
}
throw new Error('Webhook callback failed');
}
}
// --- STEP 4: CHECK API LOGS ---
async function checkApiLogs(transaction) {
const params = {
request_body_search_strict: { order_id: transaction.transaction_uuid }
};
const res = await axios.get(`${BACKEND_BASE}/api-logs`, { params });
var logs = res.data?.results || [];
logs = logs.filter(log => log.request_body?.transaction_status === 'settlement');
console.log('Filtered API logs response:', logs);
if (!Array.isArray(logs)) {
throw new Error('API logs response is not an array');
}
if (logs.length === 0) throw new Error('Webhook log not found in /api-logs');
if (logs.length > 1) throw new Error('Multiple webhook logs found for this transaction!');
console.log('Step 4: Webhook log found:', logs[0].id);
}
// --- STEP 5: CHECK TRANSACTION STATUS ---
async function checkTransactionStatus(transaction) {
const res = await axios.get(`${BACKEND_BASE}/transactions/${transaction.id}`);
const tx = res.data?.data;
if (!tx || tx.status.toLowerCase() !== 'success') throw new Error('Transaction status not updated to success');
if (parseFloat(tx.amount) !== parseFloat(transaction.amount)) throw new Error('Transaction amount mismatch');
console.log('Step 5: Transaction status is success:', tx.id);
}
// --- RUN ALL STEPS ---
(async () => {
try {
const transaction = await createTransaction();
const midtransResp = await generateQris(transaction);
await waitForPendingPaymentLog(transaction);
await simulateWebhook(transaction, midtransResp);
await checkApiLogs(transaction);
await checkTransactionStatus(transaction);
console.log('\nE2E QRIS payment test PASSED!');
} catch (err) {
console.error('\nE2E QRIS payment test FAILED:', err.message);
process.exit(1);
}
})();
```
The `MIDTRANS_AUTH` value is always `Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM=`.
User should be able to enter "amount" before "STEP 1". Since the app will be run on EDC device, it would be great if we have some keypad to enter the amount (not using Android keypad/keyboard).
After `STEP 2`, the QRIS image should be displayed in the app activity. The sample json response from Midtrans when we call `MIDTRANS_CHARGE_URL` is like this:
```json
{
"status_code": "201",
"status_message": "QRIS transaction is created",
"transaction_id": "1015a919-b03f-450a-bc85-b38202a79a96",
"order_id": "order102",
"merchant_id": "G490526303",
"gross_amount": "789000.00",
"currency": "IDR",
"payment_type": "qris",
"transaction_time": "2021-06-23 15:25:24",
"transaction_status": "pending",
"fraud_status": "accept",
"actions": [
{
"name": "generate-qr-code",
"method": "GET",
"url": "https://api.midtrans.com/v2/qris/1015a919-b03f-450a-bc85-b38202a79a96/qr-code"
}
],
"qr_string": "00020101021226620014COM.GO-JEK.WWW011993600914349052630340210G4905263030303UKE51440014ID.CO.QRIS.WWW0215AID0607336128660303UKE5204341453033605802ID5904Test6007BANDUNG6105402845409789000.0062475036c032f87c-f773-4619-aefa-675e1f06f9210703A016304A623",
"acquirer": "gopay"
}
```
the `actions.url` is a valid QRIS image (it can be viewed using `img src=` if using HTML code), or you can generate the image based on `qr_string` field.
After step 2.5, the app should display somekind of button that will trigger `STEP 3`.
After that, we should show the transaction status in the app activity (which is always a success). Give some button to return to the main screen.

23
settings.gradle Normal file
View File

@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "BDKI POC"
include ':app'