demo transaksi

This commit is contained in:
2026-04-01 11:55:47 +07:00
parent 619d758027
commit 7417222c79
166 changed files with 3111 additions and 5265 deletions

View File

@@ -1,30 +1,4 @@
apply plugin: 'com.android.application'
apply plugin: 'realm-android'
android {
namespace "id.ontime.driver"
signingConfigs {
debug {
// Use default debug keystore on this machine
}
}
compileSdk 34
defaultConfig {
applicationId "id.ontime.driver"
minSdkVersion 23
targetSdkVersion 34
versionCode 10
versionName '1.1.0'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
productFlavors {
}
:
buildTypes {
release {
signingConfig signingConfigs.debug

View File

@@ -491,8 +491,7 @@ public class LoginActivity extends AppCompatActivity {
}
request.setPassword(password.getText().toString());
// Enforce FCM token: login ONLY proceeds when we have a non-empty Firebase token.
// 1) Try to use token that was already stored in Realm (BaseApp / MessagingService.onNewToken).
// Best effort FCM token: use cached token first, then try fresh token.
io.realm.Realm realm = BaseApp.getInstance(this).getRealmInstance();
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
@@ -501,7 +500,7 @@ public class LoginActivity extends AppCompatActivity {
return;
}
// 2) If not yet stored, request a fresh token from FirebaseMessaging.
// If token is still warming up, continue login without blocking user.
try {
FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(task -> {
@@ -518,15 +517,11 @@ public class LoginActivity extends AppCompatActivity {
doLoginRequest(request, emailText);
} else {
// Token is required: stop login and show message.
progresshide();
notif("Firebase token not ready, please check your network and try again.");
doLoginRequest(request, emailText);
}
});
} catch (IllegalStateException e) {
// Firebase not initialized: block login instead of proceeding without token.
progresshide();
notif("Firebase is not initialized, cannot login. Please restart the app.");
doLoginRequest(request, emailText);
}
}
@@ -539,7 +534,8 @@ public class LoginActivity extends AppCompatActivity {
progresshide();
if (response.isSuccessful()) {
LoginResponseJson body = response.body();
if (body != null && body.getMessage().equalsIgnoreCase("found")) {
if (body != null && body.getMessage().equalsIgnoreCase("found")
&& body.getData() != null && !body.getData().isEmpty()) {
User user = body.getData().get(0);
saveUser(user);
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
@@ -549,6 +545,8 @@ public class LoginActivity extends AppCompatActivity {
} else {
notif(getString(R.string.phoneemailwrong));
}
} else {
notif(getString(R.string.phoneemailwrong));
}
}

View File

@@ -73,7 +73,9 @@ public class BaseApp extends Application {
// FCM v1: async token fetch and topic subscribe (reference: test app).
try {
FirebaseApp app = FirebaseApp.initializeApp(this);
FirebaseApp app = FirebaseApp.getApps(this).isEmpty()
? FirebaseApp.initializeApp(this)
: FirebaseApp.getInstance();
if (app != null) {
FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(task -> {

View File

@@ -8,7 +8,7 @@ buildscript {
dependencies {
// Android Gradle Plugin compatible with Gradle 8.x and JDK 21
classpath 'com.android.tools.build:gradle:8.5.0'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.gms:google-services:4.4.2'
// Realm plugin variant that uses the new AGP transformer API
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'

View File

@@ -1,20 +1,30 @@
apply plugin: 'com.android.application'
apply plugin: 'realm-android'
// Native apps expect `ontimekeystore.jks`, but it's not committed in this repo.
// If it's missing, fall back to the standard Android debug keystore.
def ontimeKeystoreFile = file('ontimekeystore.jks')
def defaultDebugKeystoreFile = file("${System.getProperty('user.home')}/.android/debug.keystore")
def hasOntimeKeystore = ontimeKeystoreFile.exists()
def signingStoreFile = hasOntimeKeystore ? ontimeKeystoreFile : defaultDebugKeystoreFile
def signingStorePassword = hasOntimeKeystore ? '123456@ontime' : 'android'
def signingKeyAlias = hasOntimeKeystore ? 'ontimekeystore' : 'androiddebugkey'
def signingKeyPassword = hasOntimeKeystore ? '123456@ontime' : 'android'
android {
namespace "id.ontime.merchant"
signingConfigs {
config {
storeFile file('ontimekeystore.jks')
storePassword '123456@ontime'
keyAlias 'ontimekeystore'
keyPassword '123456@ontime'
storeFile signingStoreFile
storePassword signingStorePassword
keyAlias signingKeyAlias
keyPassword signingKeyPassword
}
debug {
storeFile file('ontimekeystore.jks')
storePassword '123456@ontime'
keyAlias 'ontimekeystore'
keyPassword '123456@ontime'
storeFile signingStoreFile
storePassword signingStorePassword
keyAlias signingKeyAlias
keyPassword signingKeyPassword
}
}
compileSdk 34

View File

@@ -491,7 +491,7 @@ public class LoginActivity extends AppCompatActivity {
}
request.setPassword(password.getText().toString());
// FCM token is mandatory for login: use cached token if available, otherwise fetch a fresh one.
// Best effort FCM token: use cached token if available, otherwise fetch fresh token.
io.realm.Realm realm = BaseApp.getInstance(this).getRealmInstance();
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
@@ -513,8 +513,7 @@ public class LoginActivity extends AppCompatActivity {
});
performLoginRequest(request, emailText);
} else {
progresshide();
notif("Firebase token not ready, please check your network and try again.");
performLoginRequest(request, emailText);
}
});
}
@@ -528,7 +527,8 @@ public class LoginActivity extends AppCompatActivity {
progresshide();
if (response.isSuccessful()) {
LoginResponseJson body = response.body();
if (body != null && body.getMessage().equalsIgnoreCase("found")) {
if (body != null && body.getMessage().equalsIgnoreCase("found")
&& body.getData() != null && !body.getData().isEmpty()) {
User user = body.getData().get(0);
saveUser(user);
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
@@ -538,6 +538,8 @@ public class LoginActivity extends AppCompatActivity {
} else {
notif(getString(R.string.phoneemailwrong));
}
} else {
notif(getString(R.string.phoneemailwrong));
}
}

View File

@@ -42,7 +42,9 @@ public class BaseApp extends Application {
realmInstance = Realm.getDefaultInstance();
try {
FirebaseApp app = FirebaseApp.initializeApp(this);
FirebaseApp app = FirebaseApp.getApps(this).isEmpty()
? FirebaseApp.initializeApp(this)
: FirebaseApp.getInstance();
if (app != null) {
FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(task -> {

View File

@@ -9,7 +9,7 @@ buildscript {
// Android Gradle Plugin compatible with Gradle 8.x and JDK 21
classpath 'com.android.tools.build:gradle:8.5.0'
classpath 'com.jakewharton:butterknife-gradle-plugin:9.0.0-rc2'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.gms:google-services:4.4.2'
// Realm plugin variant that uses the new AGP transformer API
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'

View File

@@ -3,15 +3,25 @@ apply plugin: 'realm-android'
apply plugin: 'kotlin-android'
//apply plugin: 'kotlin-android-extensions'
// Native apps previously hard-required `ontimekeystore.jks` (not committed in this repo).
// Fallback to the standard Android debug keystore so builds can still produce APKs locally.
def ontimeKeystoreFile = file('ontimekeystore.jks')
def defaultDebugKeystoreFile = file("${System.getProperty('user.home')}/.android/debug.keystore")
def hasOntimeKeystore = ontimeKeystoreFile.exists()
def signingStoreFile = hasOntimeKeystore ? ontimeKeystoreFile : defaultDebugKeystoreFile
def signingStorePassword = hasOntimeKeystore ? '123456@ontime' : 'android'
def signingKeyAlias = hasOntimeKeystore ? 'ontimekeystore' : 'androiddebugkey'
def signingKeyPassword = hasOntimeKeystore ? '123456@ontime' : 'android'
android {
namespace "id.ontime.customer"
signingConfigs {
debug {
// Use local ontime keystore (make sure this file exists in the app module)
storeFile file('ontimekeystore.jks')
storePassword '123456@ontime'
keyAlias 'ontimekeystore'
keyPassword '123456@ontime'
storeFile signingStoreFile
storePassword signingStorePassword
keyAlias signingKeyAlias
keyPassword signingKeyPassword
}
}
compileSdk 34

View File

@@ -546,7 +546,7 @@ public class LoginActivity extends AppCompatActivity {
}
request.setPassword(password.getText().toString());
// FCM token is required: prefer cached token from Realm, otherwise fetch a fresh one.
// Best effort FCM token: prefer cached token, otherwise fetch a fresh one.
Realm realm = BaseApp.getInstance(this).getRealmInstance();
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
@@ -569,15 +569,13 @@ public class LoginActivity extends AppCompatActivity {
});
doLoginRequest(request, emailText);
} else {
progresshide();
Log.e(TAG, "FCM getToken failed: " + (task.getException() != null ? task.getException().getMessage() : "empty token"));
notif(getString(R.string.fcm_token_required));
Log.w(TAG, "FCM getToken not ready, continue login without token");
doLoginRequest(request, emailText);
}
});
} catch (IllegalStateException e) {
progresshide();
Log.e(TAG, "FirebaseMessaging not available; FCM token is required", e);
notif(getString(R.string.fcm_token_required));
Log.w(TAG, "FirebaseMessaging unavailable during login, continue without token", e);
doLoginRequest(request, emailText);
}
}
@@ -620,19 +618,24 @@ public class LoginActivity extends AppCompatActivity {
notif(showMsg);
}
} else {
String errMsg = "Login failed";
String errMsg = getString(R.string.phoneemailwrong);
try {
if (response.errorBody() != null) {
String errBody = response.errorBody().string();
Log.e(TAG, "Login error body: code=" + code + " body=" + (errBody != null ? errBody : "null"));
if (errBody != null && !errBody.isEmpty()) errMsg = errBody;
if (errBody != null && !errBody.isEmpty()) {
String low = errBody.toLowerCase();
if (!(low.contains("wrong") || low.contains("salah") || low.contains("not found") || low.contains("unauthorized"))) {
errMsg = errBody;
}
}
} else {
Log.e(TAG, "Login error: code=" + code + " no error body");
}
} catch (Exception e) {
Log.e(TAG, "Login error reading body", e);
}
if (code == 401) errMsg = getString(R.string.phoneemailwrong);
if (code == 400 || code == 401 || code == 403) errMsg = getString(R.string.phoneemailwrong);
notif(errMsg);
}
}

View File

@@ -837,16 +837,14 @@ public class RegisterActivity extends AppCompatActivity {
request.setCountrycode(countryCode.getText().toString());
request.setChecked(check);
// FCM v1: get token async then register (token is mandatory)
// FCM v1: best effort token fetch before register.
try {
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
if (task.isSuccessful() && task.getResult() != null && !task.getResult().isEmpty()) {
request.setToken(task.getResult());
} else {
progresshide();
Log.e("RegisterActivity", "FCM getToken failed for register: " + (task.getException() != null ? task.getException().getMessage() : "empty token"));
notif(getString(R.string.fcm_token_required));
return;
Log.w("RegisterActivity", "FCM getToken not ready, continue register without token");
request.setToken("");
}
UserService service = ServiceGenerator.createService(UserService.class, request.getEmail(), request.getPassword());
service.register(request).enqueue(new Callback<RegisterResponseJson>() {
@@ -884,9 +882,42 @@ public class RegisterActivity extends AppCompatActivity {
});
});
} catch (IllegalStateException e) {
Log.w("RegisterActivity", "FirebaseMessaging unavailable during register, continue without token", e);
request.setToken("");
UserService service = ServiceGenerator.createService(UserService.class, request.getEmail(), request.getPassword());
service.register(request).enqueue(new Callback<RegisterResponseJson>() {
@Override
public void onResponse(@NonNull Call<RegisterResponseJson> call, @NonNull Response<RegisterResponseJson> response) {
progresshide();
Log.e("RegisterActivity", "FirebaseMessaging not available; FCM token is required for register", e);
notif(getString(R.string.fcm_token_required));
if (response.isSuccessful()) {
if (Objects.requireNonNull(response.body()).getMessage().equalsIgnoreCase("next")) {
Nextbtn(viewFlipper);
} else if (response.body().getMessage().equalsIgnoreCase("success")) {
User user = response.body().getData().get(0);
saveUser(user);
registerPanic();
Intent intent = new Intent(RegisterActivity.this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
} else {
notif(response.body().getMessage());
}
} else {
notif("error");
}
}
@Override
public void onFailure(@NonNull Call<RegisterResponseJson> call, @NonNull Throwable t) {
progresshide();
t.printStackTrace();
notif("error!");
}
});
}
}

View File

@@ -43,7 +43,9 @@ public class BaseApp extends Application {
// FCM v1: async token fetch and topic subscribe (reference: test app LoginActivity).
try {
FirebaseApp app = FirebaseApp.initializeApp(this);
FirebaseApp app = FirebaseApp.getApps(this).isEmpty()
? FirebaseApp.initializeApp(this)
: FirebaseApp.getInstance();
if (app != null) {
FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(task -> {

View File

@@ -10,7 +10,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:8.5.0'
classpath 'com.jakewharton:butterknife-gradle-plugin:9.0.0-rc2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.gms:google-services:4.4.2'
// Realm plugin variant that uses the new AGP transformer API
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -44,6 +44,8 @@ $config['fcm_credentials_json'] = '';
$config['fcm_credentials_path'] = FCPATH . 'ngojol-trial-firebase-adminsdk-lc81n-00c9e935db.json';
$config['fcm_limit_per_hour'] = 100;
$config['fcm_limit_per_day'] = 500;
// When true, Pelanggan/Merchant/Driver login does not require an FCM device token (e.g. API tests).
$config['fcm_login_allow_no_device_token'] = false;
$config['maps_limit_per_hour'] = 1000;
$config['maps_limit_per_day'] = 5000;
@@ -59,6 +61,10 @@ define('FCM_CREDENTIALS_JSON', $config['fcm_credentials_json']);
define('FCM_CREDENTIALS_PATH', $config['fcm_credentials_path']);
define('FCM_LIMIT_PER_HOUR', $config['fcm_limit_per_hour']);
define('FCM_LIMIT_PER_DAY', $config['fcm_limit_per_day']);
define(
'FCM_LOGIN_ALLOW_NO_DEVICE_TOKEN',
filter_var($config['fcm_login_allow_no_device_token'], FILTER_VALIDATE_BOOLEAN)
);
define('MAPS_LIMIT_PER_HOUR', $config['maps_limit_per_hour']);
define('MAPS_LIMIT_PER_DAY', $config['maps_limit_per_day']);

View File

@@ -6,9 +6,14 @@ defined('BASEPATH') or exit('No direct script access allowed');
| Legacy FCM config DO NOT USE
|--------------------------------------------------------------------------
| Push notifications use FCM HTTP v1 only (see application/helpers/fcm_v1_helper.php).
| Set FCM_CREDENTIALS_PATH or FCM_CREDENTIALS_JSON and FCM_PROJECT_ID in the environment.
| This file exists so the deprecated application/libraries/Fcm.php does not
| load a missing config; the legacy library must not be used (no legacy API key).
| Service account path/project id live in application/config/config.php (constants
| FCM_CREDENTIALS_PATH, FCM_CREDENTIALS_JSON, FCM_PROJECT_ID). This file exists so the
| deprecated application/libraries/Fcm.php does not load a missing config; do not
| use the legacy library (no legacy API key).
|
| Mobile login (Pelanggan/login, Merchant/login, Driver/login) expects a device token
| from the APK (Firebase getToken). To allow clients without a token (e.g. API tests),
| set $config['fcm_login_allow_no_device_token'] = true in config.php.
*/
$config['fcm_api_key'] = '';
$config['fcm_api_send_address'] = 'https://fcm.googleapis.com/fcm/send';

View File

@@ -11,14 +11,15 @@ class appnotification extends CI_Controller
is_logged_in();
$this->load->model('notification_model', 'notif');
$this->load->model('Pelanggan_model', 'pelanggan_model');
$this->load->library('form_validation');
}
public function index()
{
$data['pelanggan_list'] = $this->pelanggan_model->get_all_for_notification_picker();
$this->load->view('includes/header');
$this->load->view('appnotification/index');
$this->load->view('appnotification/index', $data);
$this->load->view('includes/footer');
}
@@ -33,10 +34,57 @@ class appnotification extends CI_Controller
return;
}
$topic = $this->input->post('topic');
$title = $this->input->post('title');
$message = $this->input->post('message');
$send_target = $this->input->post('send_target');
if ($send_target === 'users') {
$user_ids = $this->input->post('user_ids');
if (!is_array($user_ids)) {
$user_ids = array();
}
$user_ids = array_unique(array_filter(array_map('intval', $user_ids)));
if (empty($user_ids)) {
$this->session->set_flashdata('error', 'Pilih minimal satu pengguna (user).');
redirect('appnotification/index');
return;
}
$sent = 0;
$skipped = 0;
foreach ($user_ids as $uid) {
if ($uid <= 0) {
continue;
}
$row = $this->pelanggan_model->get_notification_row_by_id($uid);
if (!$row) {
$skipped++;
continue;
}
$tok = isset($row->token) ? trim((string) $row->token) : '';
if ($tok === '' || !fcm_v1_is_valid_device_token($tok)) {
$skipped++;
continue;
}
if ($this->notif->send_notif($title, $message, $tok)) {
$sent++;
} else {
$skipped++;
}
}
if ($sent > 0) {
$msg = 'Notifikasi terkirim ke ' . $sent . ' pengguna.';
if ($skipped > 0) {
$msg .= ' ' . $skipped . ' dilewati (tanpa token FCM valid atau gagal kirim).';
}
$this->session->set_flashdata('send', $msg);
} else {
$this->session->set_flashdata('error', 'Tidak ada notifikasi terkirim. Pastikan pengguna memiliki token FCM valid (login dari aplikasi).');
}
redirect('appnotification/index');
return;
}
$topic = $this->input->post('topic');
$ok = $this->notif->send_notif($title, $message, $topic);
if ($ok) {
$this->session->set_flashdata('send', 'Notifikasi berhasil dikirim');

View File

@@ -76,10 +76,24 @@ class Driver extends REST_Controller
$data = file_get_contents("php://input");
$decoded_data = json_decode($data);
if (!$decoded_data || !isset($decoded_data->password)) {
$this->response(array('code' => '400', 'message' => 'Invalid request', 'data' => []), 200);
return;
}
if (function_exists('fcm_v1_validate_login_device_token_from_app')) {
$fcm_err = fcm_v1_validate_login_device_token_from_app($decoded_data);
if (is_array($fcm_err)) {
$this->response(
array('code' => $fcm_err['code'], 'message' => $fcm_err['message'], 'data' => []),
200
);
return;
}
}
// Only save reg_id (FCM token) when valid. Invalid/placeholder tokens are updated by relogin.
$token_from_regid = isset($decoded_data->reg_id) ? trim((string) $decoded_data->reg_id) : '';
$token_from_token = isset($decoded_data->token) ? trim((string) $decoded_data->token) : '';
$token = $token_from_regid !== '' ? $token_from_regid : $token_from_token;
$token = function_exists('fcm_v1_device_token_from_request')
? fcm_v1_device_token_from_request($decoded_data)
: '';
$reg_id = array();
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
$reg_id['reg_id'] = $token;
@@ -149,8 +163,10 @@ class Driver extends REST_Controller
);
$ins = $this->Driver_model->my_location($data);
// When driver sends valid FCM token (reg_id) with location, update so they receive order requests. Invalid/placeholder tokens are updated by relogin.
$reg_id = isset($decoded_data->reg_id) ? trim((string) $decoded_data->reg_id) : '';
// When driver sends valid FCM token with location, update so they receive order requests.
$reg_id = function_exists('fcm_v1_device_token_from_request')
? fcm_v1_device_token_from_request($decoded_data)
: (isset($decoded_data->reg_id) ? trim((string) $decoded_data->reg_id) : '');
if ($reg_id !== '' && isset($decoded_data->id_driver) && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($reg_id)) {
$this->Driver_model->update_driver_reg_id($decoded_data->id_driver, $reg_id);
}
@@ -372,6 +388,22 @@ class Driver extends REST_Controller
}
}
/**
* Persist driver accept exchange on transaksi row (admin order detail API log).
*/
private function log_driver_accept_api($id_transaksi, $raw_body, $response_message)
{
$id_transaksi = (int) $id_transaksi;
if ($id_transaksi <= 0) {
return;
}
$line = json_encode(array(
'driver_request_raw' => $raw_body,
'backend_response' => $response_message,
), JSON_UNESCAPED_UNICODE);
$this->Pelanggan_model->append_transaksi_driver_request_log($id_transaksi, $line);
}
function accept_post()
{
if (!isset($_SERVER['PHP_AUTH_USER'])) {
@@ -383,6 +415,7 @@ class Driver extends REST_Controller
$data = file_get_contents("php://input");
$dec_data = json_decode($data);
log_message('debug', 'accept_post: payload=' . $data);
$tid_for_log = (is_object($dec_data) && isset($dec_data->id_transaksi)) ? (int) $dec_data->id_transaksi : 0;
$data_req = array(
'id_driver' => $dec_data->id,
@@ -390,13 +423,13 @@ class Driver extends REST_Controller
);
$condition = array(
'id_driver' => $dec_data->id,
'status' => '1'
'id_driver' => $dec_data->id
);
$cek_login = $this->Driver_model->get_status_driver($condition);
log_message('debug', 'accept_post: get_status_driver rows=' . $cek_login->num_rows());
if ($cek_login->num_rows() > 0) {
$driver_status = $cek_login->num_rows() > 0 ? (string) $cek_login->row('status') : '';
log_message('debug', 'accept_post: get_status_driver rows=' . $cek_login->num_rows() . ' status=' . $driver_status);
if ($cek_login->num_rows() > 0 && ($driver_status === '1' || $driver_status === '4')) {
$acc_req = $this->Driver_model->accept_request($data_req);
log_message('debug', 'accept_post: accept_request result=' . json_encode($acc_req));
@@ -405,6 +438,7 @@ class Driver extends REST_Controller
'message' => 'berhasil',
'data' => 'berhasil'
);
$this->log_driver_accept_api($tid_for_log, $data, $message);
$this->response($message, 200);
} else {
if ($acc_req['data'] == 'canceled') {
@@ -412,12 +446,14 @@ class Driver extends REST_Controller
'message' => 'canceled',
'data' => 'canceled'
);
$this->log_driver_accept_api($tid_for_log, $data, $message);
$this->response($message, 200);
} else {
$message = array(
'message' => 'unknown fail',
'data' => 'canceled'
);
$this->log_driver_accept_api($tid_for_log, $data, $message);
$this->response($message, 200);
}
}
@@ -426,6 +462,7 @@ class Driver extends REST_Controller
'message' => 'unknown fail',
'data' => 'canceled'
);
$this->log_driver_accept_api($tid_for_log, $data, $message);
$this->response($message, 200);
}
}

View File

@@ -65,9 +65,31 @@ class Merchant extends REST_Controller
$data = file_get_contents("php://input");
$decoded_data = json_decode($data);
$no_telepon_val =
isset($decoded_data->no_telepon) ? trim((string)$decoded_data->no_telepon) : '';
$email_val = isset($decoded_data->email)
? trim((string)$decoded_data->email)
: '';
if (!$decoded_data || !isset($decoded_data->password) || ($no_telepon_val === '' && $email_val === '')) {
$this->response(array('code' => '400', 'message' => 'Invalid request', 'data' => []), 200);
return;
}
$login_by_phone = $no_telepon_val !== '';
if (function_exists('fcm_v1_validate_login_device_token_from_app')) {
$fcm_err = fcm_v1_validate_login_device_token_from_app($decoded_data);
if (is_array($fcm_err)) {
$this->response(
array('code' => $fcm_err['code'], 'message' => $fcm_err['message'], 'data' => []),
200
);
return;
}
}
// Only save FCM token when valid (relogin overwrites invalid/placeholder tokens).
$token = isset($decoded_data->token) ? trim((string) $decoded_data->token) : '';
$token = (isset($decoded_data->reg_id) && trim((string) $decoded_data->reg_id) !== '') ? trim((string) $decoded_data->reg_id) : $token;
$token = function_exists('fcm_v1_device_token_from_request')
? fcm_v1_device_token_from_request($decoded_data)
: '';
$reg_id = array();
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
$reg_id['token_merchant'] = $token;
@@ -75,10 +97,18 @@ class Merchant extends REST_Controller
$condition = array(
'password' => sha1($decoded_data->password),
'telepon_mitra' => $decoded_data->no_telepon,
//'token' => $decoded_data->token
);
$check_banned = $this->Merchantapi_model->check_banned($decoded_data->no_telepon);
if ($login_by_phone) {
$condition['telepon_mitra'] = $no_telepon_val;
} else {
$condition['email_mitra'] = $email_val;
}
$check_banned = $login_by_phone
? $this->Merchantapi_model->check_banned($no_telepon_val)
: $this->Merchantapi_model->check_banned_by_email($email_val);
if ($check_banned) {
$message = array(
'message' => 'banned',
@@ -90,7 +120,19 @@ class Merchant extends REST_Controller
$message = array();
if ($cek_login->num_rows() > 0) {
if (!empty($reg_id)) {
$this->Merchantapi_model->edit_profile_token($reg_id, $decoded_data->no_telepon);
$loginRow = $cek_login->row();
$phone_for_token = '';
if ($login_by_phone) {
$phone_for_token = $no_telepon_val;
} else if (!empty($loginRow) && isset($loginRow->telepon_merchant) && $loginRow->telepon_merchant !== '') {
$phone_for_token = $loginRow->telepon_merchant;
} else if (!empty($loginRow) && isset($loginRow->telepon_mitra) && $loginRow->telepon_mitra !== '') {
$phone_for_token = $loginRow->telepon_mitra;
}
if ($phone_for_token !== '') {
$this->Merchantapi_model->edit_profile_token($reg_id, $phone_for_token);
}
}
$get_pelanggan = $this->Merchantapi_model->get_data_merchant($condition);
$message = array(
@@ -102,7 +144,7 @@ class Merchant extends REST_Controller
} else {
$message = array(
'code' => '404',
'message' => 'nomor hp atau password salah!',
'message' => 'nomor hp/email atau password salah!',
'data' => []
);
$this->response($message, 200);
@@ -451,11 +493,12 @@ class Merchant extends REST_Controller
$regid = $this->wallet->getregid($iduser);
$tokenmerchant = $this->wallet->gettokenmerchant($iduser);
if ($token == NULL and $tokenmerchant == NULL and $regid != NULL) {
$topic = null;
if ($token == NULL and $tokenmerchant == NULL and $regid != NULL && !empty(trim((string) $regid['reg_id']))) {
$topic = $regid['reg_id'];
} else if ($regid == NULL and $tokenmerchant == NULL and $token != NULL) {
} else if ($regid == NULL and $tokenmerchant == NULL and $token != NULL && !empty(trim((string) $token['token']))) {
$topic = $token['token'];
} else if ($regid == NULL and $token == NULL and $tokenmerchant != NULL) {
} else if ($regid == NULL and $token == NULL and $tokenmerchant != NULL && !empty(trim((string) $tokenmerchant['token_merchant']))) {
$topic = $tokenmerchant['token_merchant'];
}
@@ -463,11 +506,10 @@ class Merchant extends REST_Controller
$message = 'Permintaan berhasil dikirim';
$saldo = $this->wallet->getsaldo($iduser);
$this->wallet->ubahsaldo($iduser, $amount, $saldo);
//$this->wallet->ubahstatuswithdrawbyid($id);
if ($topic !== null) {
$this->wallet->send_notif($title, $message, $topic);
}
/* END EDIT */
$message = array(

View File

@@ -18,6 +18,71 @@ class Pelanggan extends REST_Controller
date_default_timezone_set('Asia/Jakarta');
}
/**
* Structured API request log for order-related endpoints.
*/
private function log_order_api_request($endpoint, $rawBody)
{
log_message('debug', '[ORDER_API][' . $endpoint . '][REQUEST] ' . $rawBody);
}
/**
* Structured API response log for order-related endpoints.
*/
private function log_order_api_response($endpoint, $responsePayload)
{
log_message('debug', '[ORDER_API][' . $endpoint . '][RESPONSE] ' . json_encode($responsePayload));
}
/**
* Log candidate drivers targeted by customer order flow.
*/
private function log_order_driver_targets($endpoint, $idTransaksi, $driverList)
{
$ids = array();
if (is_array($driverList)) {
foreach ($driverList as $d) {
if (is_object($d) && isset($d->id)) {
$ids[] = (string) $d->id;
} else if (is_array($d) && isset($d['id'])) {
$ids[] = (string) $d['id'];
}
}
}
log_message(
'debug',
'[ORDER_API][' . $endpoint . '][DRIVER_TARGETS] id_transaksi=' . $idTransaksi .
' total=' . count($ids) . ' driver_ids=' . implode(',', $ids)
);
}
/**
* Store raw request/response on transaksi row for dashboard detail (requires DB columns).
*/
private function save_order_creation_logs($id_transaksi, $raw_request, $response_payload, $driver_targets)
{
$id_transaksi = (int) $id_transaksi;
if ($id_transaksi <= 0) {
return;
}
$rows = array();
if (is_array($driver_targets)) {
foreach ($driver_targets as $d) {
$rows[] = json_decode(json_encode($d), true);
}
}
$backend_driver = json_encode(array(
'note' => 'Candidate drivers from get_data_driver_histroy after order create; client apps may use Firebase/FCM for dispatch.',
'candidate_drivers' => $rows,
), JSON_UNESCAPED_UNICODE);
$this->Pelanggan_model->save_transaksi_api_log(
$id_transaksi,
$raw_request,
json_encode($response_payload, JSON_UNESCAPED_UNICODE),
$backend_driver
);
}
function index_get()
{
$this->response("Api for Ontime!", 200);
@@ -120,9 +185,20 @@ class Pelanggan extends REST_Controller
$this->response(array('code' => '400', 'message' => 'Invalid request', 'data' => []), 200);
return;
}
if (function_exists('fcm_v1_validate_login_device_token_from_app')) {
$fcm_err = fcm_v1_validate_login_device_token_from_app($decoded_data);
if (is_array($fcm_err)) {
$this->response(
array('code' => $fcm_err['code'], 'message' => $fcm_err['message'], 'data' => []),
200
);
return;
}
}
// Only save FCM token when valid (relogin overwrites invalid/placeholder tokens).
$token = isset($decoded_data->token) ? trim((string) $decoded_data->token) : '';
$token = (isset($decoded_data->reg_id) && trim((string) $decoded_data->reg_id) !== '') ? trim((string) $decoded_data->reg_id) : $token;
$token = function_exists('fcm_v1_device_token_from_request')
? fcm_v1_device_token_from_request($decoded_data)
: '';
$reg_id = array();
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
$reg_id['token'] = $token;
@@ -236,7 +312,9 @@ class Pelanggan extends REST_Controller
// Generate a deterministic placeholder based on email so the column is never empty.
// This placeholder is intentionally SHORT / starting with "R" + digits so
// fcm_v1_is_valid_device_token() will treat it as invalid for push.
$incomingToken = isset($dec_data->token) ? trim((string) $dec_data->token) : '';
$incomingToken = function_exists('fcm_v1_device_token_from_request')
? fcm_v1_device_token_from_request($dec_data)
: (isset($dec_data->token) ? trim((string) $dec_data->token) : '');
if ($incomingToken === '') {
$emailForToken = isset($dec_data->email) ? strtolower(trim((string) $dec_data->email)) : '';
if ($emailForToken !== '') {
@@ -1007,6 +1085,7 @@ class Pelanggan extends REST_Controller
function request_transaksi_post()
{
$endpoint = 'request_transaksi_post';
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header("WWW-Authenticate: Basic realm=\"Private Area\"");
header("HTTP/1.0 401 Unauthorized");
@@ -1014,18 +1093,19 @@ class Pelanggan extends REST_Controller
} else {
$cek = $this->Pelanggan_model->check_banned_user($_SERVER['PHP_AUTH_USER']);
if ($cek) {
log_message('debug', 'request_transaksi_post: banned user ' . $_SERVER['PHP_AUTH_USER']);
$message = array(
'message' => 'fail',
'data' => 'Status User Banned'
);
log_message('debug', '[ORDER_API][' . $endpoint . '] banned user ' . $_SERVER['PHP_AUTH_USER']);
$this->log_order_api_response($endpoint, $message);
$this->response($message, 200);
}
}
$data = file_get_contents("php://input");
$dec_data = json_decode($data);
log_message('debug', 'request_transaksi_post: payload=' . $data);
$this->log_order_api_request($endpoint, $data);
$data_req = array(
'id_pelanggan' => $dec_data->id_pelanggan,
@@ -1046,9 +1126,23 @@ class Pelanggan extends REST_Controller
);
$request = $this->Pelanggan_model->insert_transaksi($data_req);
$idTransaksiNum = 0;
if ($request['status'] && !empty($request['data'])) {
foreach ($request['data'] as $row) {
if (is_object($row) && isset($row->id)) {
$idTransaksiNum = (int) $row->id;
break;
}
}
}
if ($request['status']) {
if (isset($request['data'][0]->id)) {
log_message('debug', 'request_transaksi_post: success id_transaksi=' . $request['data'][0]->id . ' id_pelanggan=' . $dec_data->id_pelanggan . ' fitur=' . $dec_data->order_fitur);
$idTransaksi = $idTransaksiNum > 0 ? $idTransaksiNum : 'unknown';
$driverTargets = $idTransaksiNum > 0
? $this->Pelanggan_model->get_data_driver_histroy($idTransaksiNum)->result()
: array();
$this->log_order_driver_targets($endpoint, $idTransaksi, $driverTargets);
if ($idTransaksiNum > 0) {
log_message('debug', 'request_transaksi_post: success id_transaksi=' . $idTransaksiNum . ' id_pelanggan=' . $dec_data->id_pelanggan . ' fitur=' . $dec_data->order_fitur);
} else {
log_message('debug', 'request_transaksi_post: success (no id in data) payload=' . json_encode($request['data']));
}
@@ -1056,6 +1150,8 @@ class Pelanggan extends REST_Controller
'message' => 'success',
'data' => $request['data']
);
$this->log_order_api_response($endpoint, $message);
$this->save_order_creation_logs($idTransaksiNum, $data, $message, $driverTargets);
$this->response($message, 200);
} else {
log_message('error', 'request_transaksi_post: insert_transaksi fail data=' . json_encode($request['data']));
@@ -1063,12 +1159,14 @@ class Pelanggan extends REST_Controller
'message' => 'fail',
'data' => $request['data']
);
$this->log_order_api_response($endpoint, $message);
$this->response($message, 200);
}
}
function check_status_transaksi_post()
{
$endpoint = 'check_status_transaksi_post';
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header("WWW-Authenticate: Basic realm=\"Private Area\"");
header("HTTP/1.0 401 Unauthorized");
@@ -1077,14 +1175,19 @@ class Pelanggan extends REST_Controller
$data = file_get_contents("php://input");
$dec_data = json_decode($data);
log_message('debug', 'check_status_transaksi_post: payload=' . $data);
$this->log_order_api_request($endpoint, $data);
$dataTrans = array(
'id_transaksi' => $dec_data->id_transaksi
);
$getStatus = $this->Pelanggan_model->check_status($dataTrans);
log_message('debug', 'check_status_transaksi_post: result=' . json_encode($getStatus));
$this->log_order_driver_targets(
$endpoint,
isset($dec_data->id_transaksi) ? $dec_data->id_transaksi : 'unknown',
isset($getStatus['list_driver']) ? $getStatus['list_driver'] : array()
);
$this->log_order_api_response($endpoint, $getStatus);
$this->response($getStatus, 200);
}
@@ -1321,6 +1424,7 @@ class Pelanggan extends REST_Controller
function request_transaksi_send_post()
{
$endpoint = 'request_transaksi_send_post';
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header("WWW-Authenticate: Basic realm=\"Private Area\"");
header("HTTP/1.0 401 Unauthorized");
@@ -1332,12 +1436,15 @@ class Pelanggan extends REST_Controller
'message' => 'fail',
'data' => 'Status User Banned'
);
log_message('debug', '[ORDER_API][' . $endpoint . '] banned user ' . $_SERVER['PHP_AUTH_USER']);
$this->log_order_api_response($endpoint, $message);
$this->response($message, 200);
}
}
$data = file_get_contents("php://input");
$dec_data = json_decode($data);
$this->log_order_api_request($endpoint, $data);
$data_req = array(
'id_pelanggan' => $dec_data->id_pelanggan,
@@ -1368,16 +1475,29 @@ class Pelanggan extends REST_Controller
$request = $this->Pelanggan_model->insert_transaksi_send($data_req, $dataDetail);
if ($request['status']) {
$resultRows = $request['data']->result();
$message = array(
'message' => 'success',
'data' => $request['data']->result()
'data' => $resultRows
);
$idTransaksiNum = 0;
if (!empty($resultRows[0]) && is_object($resultRows[0]) && isset($resultRows[0]->id)) {
$idTransaksiNum = (int) $resultRows[0]->id;
}
$idTransaksi = $idTransaksiNum > 0 ? $idTransaksiNum : 'unknown';
$driverTargets = $idTransaksiNum > 0
? $this->Pelanggan_model->get_data_driver_histroy($idTransaksiNum)->result()
: array();
$this->log_order_driver_targets($endpoint, $idTransaksi, $driverTargets);
$this->log_order_api_response($endpoint, $message);
$this->save_order_creation_logs($idTransaksiNum, $data, $message, $driverTargets);
$this->response($message, 200);
} else {
$message = array(
'message' => 'fail',
'data' => []
);
$this->log_order_api_response($endpoint, $message);
$this->response($message, 200);
}
}
@@ -1457,6 +1577,7 @@ class Pelanggan extends REST_Controller
function inserttransaksimerchant_post()
{
$endpoint = 'inserttransaksimerchant_post';
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header("WWW-Authenticate: Basic realm=\"Private Area\"");
header("HTTP/1.0 401 Unauthorized");
@@ -1468,12 +1589,15 @@ class Pelanggan extends REST_Controller
'message' => 'fail',
'data' => 'Status User Banned'
);
log_message('debug', '[ORDER_API][' . $endpoint . '] banned user ' . $_SERVER['PHP_AUTH_USER']);
$this->log_order_api_response($endpoint, $message);
$this->response($message, 200);
}
}
$data = file_get_contents("php://input");
$dec_data = json_decode($data);
$this->log_order_api_request($endpoint, $data);
$data_transaksi = array(
'id_pelanggan' => $dec_data->id_pelanggan,
@@ -1531,9 +1655,14 @@ class Pelanggan extends REST_Controller
$message = array(
'message' => 'success',
'data' => $result['data'],
);
$tid = isset($result['id_transaksi']) ? (int) $result['id_transaksi'] : 0;
$driverTargets = $tid > 0
? $this->Pelanggan_model->get_data_driver_histroy($tid)->result()
: array();
$this->log_order_driver_targets($endpoint, $tid ?: 'unknown', $driverTargets);
$this->log_order_api_response($endpoint, $message);
$this->save_order_creation_logs($tid, $data, $message, $driverTargets);
$this->response($message, 200);
} else {
$message = array(
@@ -1541,6 +1670,7 @@ class Pelanggan extends REST_Controller
'data' => []
);
$this->log_order_api_response($endpoint, $message);
$this->response($message, 200);
}
} else {
@@ -1549,6 +1679,7 @@ class Pelanggan extends REST_Controller
'data' => []
);
$this->log_order_api_response($endpoint, $message);
$this->response($message, 200);
}
}

View File

@@ -3,8 +3,9 @@ defined('BASEPATH') or exit('No direct script access allowed');
/**
* FCM HTTP v1 API helper (Firebase Cloud Messaging).
* Uses OAuth2 access token from service account credentials (env: FCM_CREDENTIALS_PATH or FCM_CREDENTIALS_JSON).
* Requires FCM_PROJECT_ID. Optional: FCM_LIMIT_PER_HOUR, FCM_LIMIT_PER_DAY for quota (used by callers).
* OAuth2 access token from service account: FCM_CREDENTIALS_PATH or FCM_CREDENTIALS_JSON
* and FCM_PROJECT_ID (set in application/config/config.php).
* Optional: FCM_LIMIT_PER_HOUR, FCM_LIMIT_PER_DAY for quota (used by callers).
*/
if (!function_exists('fcm_v1_get_credentials')) {
@@ -264,3 +265,80 @@ if (!function_exists('fcm_v1_send')) {
return $resp;
}
}
if (!function_exists('fcm_v1_device_token_from_request')) {
/**
* FCM device registration token from API JSON body (mobile APK).
* First non-empty among: firebase_token, fcm_token, reg_id, token.
*
* @param object|null $decoded_data json_decode() result
* @return string Trimmed token or empty string
*/
function fcm_v1_device_token_from_request($decoded_data)
{
if (!$decoded_data || !is_object($decoded_data)) {
return '';
}
foreach (array('firebase_token', 'fcm_token', 'reg_id', 'token') as $key) {
if (!isset($decoded_data->$key)) {
continue;
}
$v = trim((string) $decoded_data->$key);
if ($v !== '') {
return $v;
}
}
return '';
}
}
if (!function_exists('fcm_v1_login_allow_missing_device_token')) {
/**
* When true, Pelanggan/Merchant/Driver login skips FCM token presence validation (e.g. Postman / legacy).
* Controlled by FCM_LOGIN_ALLOW_NO_DEVICE_TOKEN in application/config/config.php.
*/
function fcm_v1_login_allow_missing_device_token()
{
if (defined('FCM_LOGIN_ALLOW_NO_DEVICE_TOKEN')) {
return (bool) FCM_LOGIN_ALLOW_NO_DEVICE_TOKEN;
}
return false;
}
}
if (!function_exists('fcm_v1_validate_login_device_token_from_app')) {
/**
* Enforces that the APK sent a Firebase-generated device token on login.
* Token is produced only on-device (FirebaseMessaging.getToken); backend never mints it.
*
* @param object|null $decoded_data json_decode body
* @return array|null Null if OK or enforcement disabled; else array with keys code, message for API response
*/
function fcm_v1_validate_login_device_token_from_app($decoded_data)
{
if (fcm_v1_login_allow_missing_device_token()) {
return null;
}
if (!function_exists('fcm_v1_device_token_from_request')) {
return null;
}
$raw = fcm_v1_device_token_from_request($decoded_data);
if ($raw === '') {
return array(
'code' => '400',
'message' =>
'Wajib kirim token FCM dari aplikasi (firebase_token, fcm_token, token, atau reg_id). '
. 'Token dibuat oleh Firebase di perangkat setelah Firebase.initializeApp dan izin notifikasi.',
);
}
if (!function_exists('fcm_v1_is_valid_device_token') || !fcm_v1_is_valid_device_token($raw)) {
return array(
'code' => '400',
'message' =>
'Token FCM tidak valid. Pastikan aplikasi berhasil memanggil Firebase Messaging getToken() '
. 'di perangkat (bukan string buatan server).',
);
}
return null;
}
}

View File

@@ -14,7 +14,7 @@ defined('BASEPATH') or exit('No direct script access allowed');
* UNIQUE KEY uk_key_period (key_name, period_type, period_value)
* );
*
* Config via env: FCM_LIMIT_PER_HOUR, FCM_LIMIT_PER_DAY, MAPS_LIMIT_PER_DAY, etc.
* Callers pass limits; backend defaults often use FCM_LIMIT_PER_HOUR / FCM_LIMIT_PER_DAY from config.php.
*/
if (!function_exists('quota_limiter_allow')) {

View File

@@ -15,6 +15,16 @@ class Merchantapi_model extends CI_model
}
}
public function check_banned_by_email($email)
{
$stat = $this->db->query("SELECT id_mitra FROM mitra WHERE status_mitra='3' AND email_mitra='$email'");
if ($stat->num_rows() == 1) {
return true;
} else {
return false;
}
}
public function check_exist($email, $phone)
{
$cek = $this->db->query("SELECT id_mitra FROM mitra where email_mitra = '$email' AND telepon_mitra='$phone'");

View File

@@ -64,11 +64,7 @@ class notification_model extends CI_model
public function send_notif($title, $message, $target)
{
if ($this->is_token_empty($target)) {
log_message('debug', 'send_notif: skip, no token');
return true;
}
if (!$this->is_valid_fcm_token($target)) {
log_message('debug', 'send_notif: skip, invalid/placeholder FCM token');
log_message('debug', 'send_notif: skip, empty target');
return true;
}
if (!function_exists('fcm_v1_validate_token')) {
@@ -87,8 +83,12 @@ class notification_model extends CI_model
'title' => $title,
'body' => $message,
);
if ($this->is_valid_fcm_token($target)) {
return $this->send_generic_to_token($target, $data, $options);
}
// Values like pelanggan, driver, mitra, ouride are FCM topic names (broadcast).
return $this->send_generic_to_topic(trim((string) $target), $data, $options);
}
public function send_notif_topup($title, $id, $message, $method, $token)
{

View File

@@ -309,7 +309,7 @@ class Pelanggan_model extends CI_model
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s, fitur f
WHERE ld.id_driver = d.id
AND f.id_fitur = $fitur
AND ld.status = '1'
AND ld.status IN ('1','4')
AND dj.id = d.job
AND d.job = f.driver_job
AND d.status = '1'
@@ -334,9 +334,9 @@ class Pelanggan_model extends CI_model
*/
private function log_driver_diagnostic($type, $lat, $lng, $fitur, $lat_min, $lat_max, $lng_min, $lng_max, $radius_km)
{
$r_online = $this->db->query("SELECT COUNT(*) as c FROM config_driver WHERE status='1'")->row();
$r_online = $this->db->query("SELECT COUNT(*) as c FROM config_driver WHERE status IN ('1','4')")->row();
$cnt_online = $r_online ? (int) $r_online->c : 0;
$r_box = $this->db->query("SELECT COUNT(*) as c FROM config_driver ld JOIN driver d ON ld.id_driver=d.id WHERE ld.status='1' AND d.status='1' AND ld.latitude BETWEEN " . (float)$lat_min . " AND " . (float)$lat_max . " AND ld.longitude BETWEEN " . (float)$lng_min . " AND " . (float)$lng_max)->row();
$r_box = $this->db->query("SELECT COUNT(*) as c FROM config_driver ld JOIN driver d ON ld.id_driver=d.id WHERE ld.status IN ('1','4') AND d.status='1' AND ld.latitude BETWEEN " . (float)$lat_min . " AND " . (float)$lat_max . " AND ld.longitude BETWEEN " . (float)$lng_min . " AND " . (float)$lng_max)->row();
$cnt_in_box = $r_box ? (int) $r_box->c : 0;
$fitur_row = $this->db->query("SELECT driver_job, jarak_minimum, wallet_minimum FROM fitur WHERE id_fitur=" . (int)$fitur)->row();
$driver_job = $fitur_row ? $fitur_row->driver_job : null;
@@ -381,7 +381,7 @@ class Pelanggan_model extends CI_model
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s, fitur f
WHERE ld.id_driver = d.id
AND f.id_fitur = $fitur
AND ld.status = '1'
AND ld.status IN ('1','4')
AND dj.id = d.job
AND d.job = f.driver_job
AND d.status = '1'
@@ -1849,5 +1849,80 @@ class Pelanggan_model extends CI_model
$this->db->update('history_transaksi', $data);
return true;
}
/**
* Customers for admin FCM screen: label with fullnama / phone / email plus id.
*/
public function get_all_for_notification_picker()
{
$this->db->select('id, fullnama, email, no_telepon, token');
$this->db->from('pelanggan');
$this->db->order_by('fullnama', 'ASC');
$this->db->order_by('id', 'ASC');
return $this->db->get()->result();
}
/**
* One row for sending push: registration token and display name.
*/
public function get_notification_row_by_id($id)
{
$this->db->select('id, fullnama, token');
$this->db->from('pelanggan');
$this->db->where('id', (int) $id);
return $this->db->get()->row();
}
/**
* Truncate for MEDIUMTEXT logging (safety cap).
*/
private function truncate_api_log_value($value, $max_bytes = 1048576)
{
$s = is_string($value) ? $value : (string) $value;
if (strlen($s) > $max_bytes) {
return substr($s, 0, $max_bytes) . "\n...[truncated]";
}
return $s;
}
/**
* Persist raw pelanggan order API + backend snapshot for driver candidates (admin detail).
*/
public function save_transaksi_api_log($id_transaksi, $raw_request, $response_json, $backend_to_driver_json)
{
$id_transaksi = (int) $id_transaksi;
if ($id_transaksi <= 0) {
return;
}
$this->db->where('id', $id_transaksi);
$this->db->update('transaksi', array(
'api_log_pelanggan_request' => $this->truncate_api_log_value($raw_request),
'api_log_pelanggan_response' => $this->truncate_api_log_value($response_json),
'api_log_backend_to_driver' => $this->truncate_api_log_value($backend_to_driver_json),
));
}
/**
* Append driver → backend raw body (e.g. accept_post JSON) for this transaction.
*/
public function append_transaksi_driver_request_log($id_transaksi, $raw_inbound)
{
$id_transaksi = (int) $id_transaksi;
if ($id_transaksi <= 0) {
return;
}
$raw_inbound = $this->truncate_api_log_value($raw_inbound, 262144);
$this->db->select('api_log_driver_request');
$this->db->where('id', $id_transaksi);
$row = $this->db->get('transaksi')->row();
$prev = ($row && isset($row->api_log_driver_request) && $row->api_log_driver_request !== null)
? (string) $row->api_log_driver_request : '';
$stamp = date('c');
$line = '[' . $stamp . "] accept/driver inbound:\n" . $raw_inbound;
$merged = $prev === '' ? $line : $prev . "\n\n" . $line;
$merged = $this->truncate_api_log_value($merged, 1048576);
$this->db->where('id', $id_transaksi);
$this->db->update('transaksi', array('api_log_driver_request' => $merged));
}
}

View File

@@ -17,14 +17,55 @@
<?php endif; ?>
<?= form_open_multipart('appnotification/send'); ?>
<div class="form-group">
<label for="newscategory">Kirim Ke</label>
<select class="js-example-basic-single" style="width:100%" name='topic'>
<label>Tipe kirim</label>
<div class="d-flex flex-wrap align-items-center" style="gap:1rem;">
<label class="mb-0 font-weight-normal">
<input type="radio" name="send_target" value="topic" checked onchange="toggleAppnotifTarget()"> Topik (broadcast)
</label>
<label class="mb-0 font-weight-normal">
<input type="radio" name="send_target" value="users" onchange="toggleAppnotifTarget()"> Pengguna terpilih (by ID)
</label>
</div>
</div>
<div class="form-group" id="wrap-topic">
<label for="topic">Kirim ke topik</label>
<select id="topic" class="js-example-basic-single" style="width:100%" name="topic">
<option value="pelanggan">User</option>
<option value="driver">Driver</option>
<option value="mitra">Merchant Partner</option>
<option value="ouride">Semua</option>
</select>
</div>
<div class="form-group" id="wrap-users" style="display:none;">
<label for="user_ids">Pilih pengguna <span class="text-muted">(nama dan ID)</span></label>
<select id="user_ids" class="js-example-basic-multiple" name="user_ids[]" multiple="multiple" style="width:100%" data-placeholder="Cari nama atau ID…">
<?php if (!empty($pelanggan_list)) : ?>
<?php foreach ($pelanggan_list as $u) : ?>
<?php
$label = $u->fullnama;
if ($label === null || trim((string) $label) === '') {
$label = $u->no_telepon ?: $u->email ?: ('User #' . (int) $u->id);
}
$option_text = $label . ' (ID: ' . (int) $u->id . ')';
?>
<option value="<?= (int) $u->id ?>"><?= htmlspecialchars($option_text, ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
<small class="form-text text-muted">Hanya pengguna yang pernah login dari aplikasi dengan token FCM valid yang menerima push.</small>
</div>
<script>
function toggleAppnotifTarget() {
var r = document.querySelector('input[name="send_target"]:checked');
var topic = document.getElementById('wrap-topic');
var users = document.getElementById('wrap-users');
if (!r || !topic || !users) return;
var isTopic = r.value === 'topic';
topic.style.display = isTopic ? 'block' : 'none';
users.style.display = isTopic ? 'none' : 'block';
}
document.addEventListener('DOMContentLoaded', toggleAppnotifTarget);
</script>
<div class="form-group">
<label for="title">Judul</label>
<input type="text" class="form-control" placeholder="notification" name="title" required>

View File

@@ -233,6 +233,36 @@
<?= $transaksi['catatan'] ?></p>
<hr>
<?php
$log_pel_req = isset($transaksi['api_log_pelanggan_request']) ? $transaksi['api_log_pelanggan_request'] : '';
$log_pel_res = isset($transaksi['api_log_pelanggan_response']) ? $transaksi['api_log_pelanggan_response'] : '';
$log_bk_drv = isset($transaksi['api_log_backend_to_driver']) ? $transaksi['api_log_backend_to_driver'] : '';
$log_drv = isset($transaksi['api_log_driver_request']) ? $transaksi['api_log_driver_request'] : '';
$has_api_log = (trim((string) $log_pel_req) !== '' || trim((string) $log_pel_res) !== '' || trim((string) $log_bk_drv) !== '' || trim((string) $log_drv) !== '');
?>
<?php if ($has_api_log) { ?>
<div class="text-left mt-4 mb-4">
<h5 class="mb-3">Log API pesanan</h5>
<?php if (trim((string) $log_pel_req) !== '') { ?>
<h6 class="text-muted small text-uppercase">Pelanggan → backend (raw request)</h6>
<pre class="p-3 bg-light border rounded small" style="max-height:240px;overflow:auto;"><?= htmlspecialchars($log_pel_req, ENT_QUOTES, 'UTF-8') ?></pre>
<?php } ?>
<?php if (trim((string) $log_pel_res) !== '') { ?>
<h6 class="text-muted small text-uppercase mt-3">Backend → pelanggan (raw response)</h6>
<pre class="p-3 bg-light border rounded small" style="max-height:240px;overflow:auto;"><?= htmlspecialchars($log_pel_res, ENT_QUOTES, 'UTF-8') ?></pre>
<?php } ?>
<?php if (trim((string) $log_bk_drv) !== '') { ?>
<h6 class="text-muted small text-uppercase mt-3">Backend → driver (snapshot / kandidat)</h6>
<pre class="p-3 bg-light border rounded small" style="max-height:240px;overflow:auto;"><?= htmlspecialchars($log_bk_drv, ENT_QUOTES, 'UTF-8') ?></pre>
<?php } ?>
<?php if (trim((string) $log_drv) !== '') { ?>
<h6 class="text-muted small text-uppercase mt-3">Driver ↔ backend (accept: raw + response JSON)</h6>
<pre class="p-3 bg-light border rounded small" style="max-height:280px;overflow:auto;"><?= htmlspecialchars($log_drv, ENT_QUOTES, 'UTF-8') ?></pre>
<?php } ?>
</div>
<hr>
<?php } ?>
<?php if ($transaksi['home'] == 4) { ?>
<div class="container-fluid mt-5 d-flex justify-content-center w-100">
<div class="table-responsive w-100">

View File

@@ -0,0 +1,8 @@
-- Run once on the application database so order detail can show API traces.
-- MySQL / MariaDB
ALTER TABLE `transaksi`
ADD COLUMN `api_log_pelanggan_request` MEDIUMTEXT NULL COMMENT 'Raw pelanggan app JSON body on order create',
ADD COLUMN `api_log_pelanggan_response` MEDIUMTEXT NULL COMMENT 'JSON response body returned to pelanggan',
ADD COLUMN `api_log_backend_to_driver` MEDIUMTEXT NULL COMMENT 'Backend snapshot: candidate drivers JSON',
ADD COLUMN `api_log_driver_request` MEDIUMTEXT NULL COMMENT 'Driver accept: raw request + backend response JSON lines';

167
build_all.sh Executable file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Prefer Java 17 for old Gradle builds to avoid "Could not determine java version from '21.x'"
JAVA17_CANDIDATE="/usr/lib/jvm/java-17-openjdk-amd64"
if [[ -d "${JAVA17_CANDIDATE}" ]]; then
echo "Using Java 17 from ${JAVA17_CANDIDATE}"
export JAVA_HOME="${JAVA17_CANDIDATE}"
export ORG_GRADLE_JAVA_HOME="${JAVA17_CANDIDATE}"
export PATH="${JAVA17_CANDIDATE}/bin:${PATH}"
else
echo "⚠ Java 17 JDK not found at ${JAVA17_CANDIDATE}. Using system default Java:"
fi
echo "JAVA_HOME=${JAVA_HOME:-<not set>}"
echo "ORG_GRADLE_JAVA_HOME=${ORG_GRADLE_JAVA_HOME:-<not set>}"
java -version || echo "Java not found on PATH builds will likely fail."
echo
OUT_DIR="${ROOT_DIR}/apk_output"
mkdir -p "${OUT_DIR}"
native_apps=(
"OnTime_User_live"
"OnTime_Driver_live"
"OnTime_Merchant_live"
)
flutter_candidates=(
"flutter_user_clone"
"flutter_merchant_clone"
"flutter_driver_clone"
"ontime_user_flutter"
"ontime_merchant_flutter"
"ontime_driver_flutter"
)
declare -a successes=()
declare -a failures=()
build_flutter_apk() {
local app_dir="$1"
echo "========================================"
echo "Building Flutter app in: ${app_dir}"
echo "========================================"
cd "${ROOT_DIR}/${app_dir}"
if ! flutter clean; then
echo "❌ flutter clean failed for ${app_dir}"
failures+=("${app_dir} (flutter clean)")
return 1
fi
if ! flutter pub get; then
echo "❌ flutter pub get failed for ${app_dir}"
failures+=("${app_dir} (flutter pub get)")
return 1
fi
if ! flutter build apk --release; then
echo "❌ flutter build apk failed for ${app_dir}"
failures+=("${app_dir} (flutter build apk)")
return 1
fi
local apk_path="${ROOT_DIR}/${app_dir}/build/app/outputs/flutter-apk/app-release.apk"
local out_name="${app_dir}_flutter-release.apk"
if [[ -f "${apk_path}" ]]; then
cp "${apk_path}" "${OUT_DIR}/${out_name}"
echo "📦 Copied to: ${OUT_DIR}/${out_name}"
else
echo "❌ Flutter release APK not found at: ${apk_path}"
failures+=("${app_dir} (apk missing)")
return 1
fi
echo "✅ Finished building ${app_dir}"
echo
successes+=("${app_dir} (flutter)")
return 0
}
build_native_apk() {
local app_dir="$1"
echo "========================================"
echo "Building native Android app in: ${app_dir}"
echo "========================================"
cd "${ROOT_DIR}/${app_dir}"
if ! ./gradlew "assembleRelease" --no-daemon; then
echo "❌ Gradle build failed for ${app_dir}"
failures+=("${app_dir} (gradle assembleRelease)")
return 1
fi
local apk_path="${ROOT_DIR}/${app_dir}/app/build/outputs/apk/release/app-release.apk"
local out_name="${app_dir}_native-release.apk"
if [[ -f "${apk_path}" ]]; then
cp "${apk_path}" "${OUT_DIR}/${out_name}"
echo "📦 Copied to: ${OUT_DIR}/${out_name}"
else
echo "❌ Native release APK not found at: ${apk_path}"
failures+=("${app_dir} (apk missing)")
return 1
fi
echo "✅ Finished building ${app_dir}"
echo
successes+=("${app_dir} (java)")
return 0
}
main() {
local found_flutter=0
for app in "${flutter_candidates[@]}"; do
if [[ -d "${ROOT_DIR}/${app}" ]]; then
found_flutter=1
build_flutter_apk "${app}" || true
fi
done
if [[ "${found_flutter}" -eq 0 ]]; then
echo "⚠️ No Flutter app directories found from configured candidates."
echo
fi
for app in "${native_apps[@]}"; do
if [[ -d "${ROOT_DIR}/${app}" ]]; then
build_native_apk "${app}" || true
else
echo "⚠️ Skipping ${app}: directory not found at ${ROOT_DIR}/${app}"
failures+=("${app} (directory missing)")
fi
done
echo
echo "========================================"
echo "Build Summary"
echo "========================================"
echo "✅ Success count: ${#successes[@]}"
for item in "${successes[@]}"; do
echo " - ${item}"
done
echo "❌ Failure count: ${#failures[@]}"
for item in "${failures[@]}"; do
echo " - ${item}"
done
echo
echo "APK output directory: ${OUT_DIR}"
if [[ "${#failures[@]}" -gt 0 ]]; then
exit 1
fi
echo "🎉 All builds completed successfully."
}
main "$@"

View File

@@ -0,0 +1,23 @@
# Flutter Conversion (Initial)
Created new Flutter app folders:
- `flutter_driver_clone`
- `flutter_user_clone`
- `flutter_merchant_clone`
Each app currently includes:
- `pubspec.yaml`
- `lib/main.dart` with a login screen layout modeled from Java UI
Run each app:
1. `cd flutter_driver_clone && flutter pub get && flutter run`
2. `cd flutter_user_clone && flutter pub get && flutter run`
3. `cd flutter_merchant_clone && flutter pub get && flutter run`
Note:
- The structure and visual composition are matched to the Java login screens first.
- To make the interface exactly identical on all screens, the next step is converting each remaining XML screen (`register`, `main`, `wallet`, `chat`, etc.) one-by-one into Flutter widgets.

View File

@@ -0,0 +1,17 @@
# flutter_driver_clone
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -3,10 +3,11 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
namespace = "com.example.ontime_user_flutter"
namespace = "id.ontime.driver"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -21,7 +22,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.ontime_user_flutter"
applicationId = "id.ontime.driver"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion

View File

@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="ontime_merchant_flutter"
android:label="OnTime Driver"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

@@ -1,4 +1,4 @@
package com.example.ontime_user_flutter
package id.ontime.driver
import io.flutter.embedding.android.FlutterActivity

View File

@@ -21,6 +21,7 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
include(":app")

View File

@@ -0,0 +1,300 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'ontime_auth.dart';
const String _kLoginPath = 'driver/login';
const String _kDial = '62';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const DriverCloneApp());
}
class DriverCloneApp extends StatelessWidget {
const DriverCloneApp({super.key});
static const Color primary = Color(0xFF5BC8DA);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'OnTime Driver Clone',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: primary),
scaffoldBackgroundColor: Colors.white,
useMaterial3: true,
),
home: const DriverLoginPage(),
);
}
}
class DriverLoginPage extends StatefulWidget {
const DriverLoginPage({super.key});
@override
State<DriverLoginPage> createState() => _DriverLoginPageState();
}
class _DriverLoginPageState extends State<DriverLoginPage> {
final _phone = TextEditingController();
final _password = TextEditingController();
bool _busy = false;
@override
void dispose() {
_phone.dispose();
_password.dispose();
super.dispose();
}
Future<void> _signIn() async {
final pass = _password.text;
final digits = _phone.text.trim();
if (pass.isEmpty) {
_toast('Isi password');
return;
}
if (digits.isEmpty) {
_toast('Isi nomor telepon');
return;
}
setState(() => _busy = true);
try {
await FirebaseMessaging.instance.requestPermission();
final token = await FirebaseMessaging.instance.getToken();
if (token == null || token.length < 50) {
_toast(
'Token FCM belum siap. Izinkan notifikasi & pastikan google-services.json / API key Firebase valid.',
);
return;
}
final result = await OntimeAuth.login(
path: _kLoginPath,
password: pass,
phoneDigits: digits,
dialCodeWithoutPlus: _kDial,
fcmToken: token,
);
if (!mounted) return;
if (result.success) {
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (_) => const _HomePlaceholder(title: 'OnTime Driver'),
),
);
} else {
_toast(result.message);
}
} catch (e) {
if (mounted) _toast('$e');
} finally {
if (mounted) setState(() => _busy = false);
}
}
void _toast(String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 92),
child: Column(
children: [
const SizedBox(height: 20),
const SizedBox(
height: 250,
child: Center(
child: Icon(Icons.local_shipping, size: 120, color: DriverCloneApp.primary),
),
),
Container(
margin: const EdgeInsets.fromLTRB(15, 0, 15, 15),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [BoxShadow(color: Color(0x22000000), blurRadius: 8, offset: Offset(0, 4))],
),
child: Column(
children: [
const SizedBox(height: 20),
_PhoneField(controller: _phone),
const SizedBox(height: 10),
_PasswordField(controller: _password),
const SizedBox(height: 20),
const Align(
alignment: Alignment.center,
child: Text('Lupa Password?', style: TextStyle(color: DriverCloneApp.primary, fontSize: 14)),
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _busy ? null : _signIn,
style: ElevatedButton.styleFrom(
backgroundColor: DriverCloneApp.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: _busy
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Text('Sign In', style: TextStyle(fontSize: 18)),
),
),
const SizedBox(height: 16),
const Text(
'Dengan masuk, Anda menyetujui kebijakan privasi.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13),
),
],
),
),
],
),
),
),
Positioned(
top: 15,
left: 15,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFFDDDDDD)),
),
child: const Icon(Icons.arrow_back, size: 20),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 80,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Belum punya akun? ', style: TextStyle(fontSize: 15)),
Text('Sign Up', style: TextStyle(fontSize: 18, color: DriverCloneApp.primary, fontWeight: FontWeight.w600)),
],
),
SizedBox(height: 8),
Text(
'Note: Jika sudah mendaftar akun baru, tunggu konfirmasi admin untuk info lebih lanjut.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 11),
),
],
),
),
),
],
),
),
);
}
}
class _HomePlaceholder extends StatelessWidget {
const _HomePlaceholder({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: const Center(child: Text('Login berhasil')),
);
}
}
class _PhoneField extends StatelessWidget {
const _PhoneField({required this.controller});
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return Container(
height: 50,
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFC4C4C4)), borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
const SizedBox(width: 80, child: Center(child: Text('+62', style: TextStyle(color: DriverCloneApp.primary, fontSize: 18)))),
Container(width: 1, margin: const EdgeInsets.symmetric(vertical: 6), color: const Color(0xFFC4C4C4)),
Expanded(
child: TextField(
controller: controller,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
hintText: 'Nomor Telepon',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 10),
),
),
),
],
),
);
}
}
class _PasswordField extends StatelessWidget {
const _PasswordField({required this.controller});
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return Container(
height: 50,
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFC4C4C4)), borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
const SizedBox(width: 80, child: Icon(Icons.lock_outline, color: DriverCloneApp.primary)),
const VerticalDivider(width: 1, thickness: 1, indent: 6, endIndent: 6),
Expanded(
child: TextField(
controller: controller,
obscureText: true,
decoration: const InputDecoration(
hintText: 'Password',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 10),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,94 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class LoginResult {
LoginResult({required this.success, required this.message});
final bool success;
final String message;
}
/// Matches native `ServiceGenerator.createService` + login POST (Basic auth + JSON body).
class OntimeAuth {
OntimeAuth._();
static const String baseUrl = 'https://apitest.semestaterpadu.my.id/api/';
static Future<LoginResult> login({
required String path,
required String password,
String email = '',
required String phoneDigits,
required String dialCodeWithoutPlus,
required String fcmToken,
}) async {
final cleanedDial = dialCodeWithoutPlus.replaceAll('+', '').trim();
final digitsOnly = phoneDigits.replaceAll(RegExp(r'\D'), '');
final local = digitsOnly.replaceFirst(RegExp(r'^0+'), '');
final fullPhone = digitsOnly.startsWith(cleanedDial)
? digitsOnly
: '$cleanedDial$local';
final useEmail = email.trim().isNotEmpty;
final basicUser = useEmail ? email.trim() : fullPhone;
final body = <String, dynamic>{
'password': password,
'token': fcmToken,
if (useEmail) ...{
'email': email.trim(),
'no_telepon': '',
} else ...{
'email': null,
'no_telepon': fullPhone,
},
};
final uri = Uri.parse('$baseUrl$path');
final auth = base64Encode(utf8.encode('$basicUser:$password'));
final resp = await http
.post(
uri,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Basic $auth',
},
body: jsonEncode(body),
)
.timeout(const Duration(seconds: 60));
Map<String, dynamic>? json;
try {
if (resp.body.isNotEmpty) {
final decoded = jsonDecode(resp.body);
if (decoded is Map<String, dynamic>) json = decoded;
}
} catch (_) {}
final msg = json?['message']?.toString() ?? '';
final data = json?['data'];
final hasData = data is List && data.isNotEmpty;
final ok = resp.statusCode >= 200 &&
resp.statusCode < 300 &&
msg.toLowerCase() == 'found' &&
hasData;
if (ok) return LoginResult(success: true, message: msg);
if (msg.isNotEmpty) {
return LoginResult(success: false, message: msg);
}
if (resp.statusCode == 401) {
return LoginResult(success: false, message: 'no hp atau password salah!');
}
if (resp.body.isNotEmpty && resp.body.length < 400) {
return LoginResult(success: false, message: resp.body);
}
return LoginResult(
success: false,
message: 'Login gagal (HTTP ${resp.statusCode})',
);
}
}

View File

@@ -0,0 +1,306 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
url: "https://pub.dev"
source: hosted
version: "1.3.59"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
url: "https://pub.dev"
source: hosted
version: "3.15.2"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
url: "https://pub.dev"
source: hosted
version: "2.24.1"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
url: "https://pub.dev"
source: hosted
version: "15.2.10"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
url: "https://pub.dev"
source: hosted
version: "4.6.10"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
url: "https://pub.dev"
source: hosted
version: "3.10.10"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks:
dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.22.0"

View File

@@ -0,0 +1,22 @@
name: flutter_driver_clone
description: OnTime Driver Flutter conversion
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
http: ^1.2.2
firebase_core: ^3.8.1
firebase_messaging: ^15.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true

View File

@@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_driver_clone/main.dart';
void main() {
testWidgets('app launches', (WidgetTester tester) async {
await tester.pumpWidget(const DriverCloneApp());
await tester.pumpAndSettle();
expect(find.text('Sign In'), findsOneWidget);
});
}

View File

@@ -0,0 +1,17 @@
# flutter_merchant_clone
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -3,10 +3,11 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
namespace = "com.example.ontime_driver_flutter"
namespace = "id.ontime.merchant"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -21,7 +22,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.ontime_driver_flutter"
applicationId = "id.ontime.merchant"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion

View File

@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="ontime_user_flutter"
android:label="id.ontime.merchant"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

@@ -1,4 +1,4 @@
package com.example.ontime_driver_flutter
package id.ontime.merchant
import io.flutter.embedding.android.FlutterActivity

View File

@@ -21,6 +21,7 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
include(":app")

View File

@@ -0,0 +1,286 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'ontime_auth.dart';
const String _kLoginPath = 'merchant/login';
const String _kDial = '62';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MerchantCloneApp());
}
class MerchantCloneApp extends StatelessWidget {
const MerchantCloneApp({super.key});
static const Color primary = Color(0xFF5BC8DA);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'id.ontime.merchant',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: primary),
useMaterial3: true,
),
home: const MerchantLoginPage(),
);
}
}
class MerchantLoginPage extends StatefulWidget {
const MerchantLoginPage({super.key});
@override
State<MerchantLoginPage> createState() => _MerchantLoginPageState();
}
class _MerchantLoginPageState extends State<MerchantLoginPage> {
final _phone = TextEditingController();
final _password = TextEditingController();
bool _busy = false;
@override
void dispose() {
_phone.dispose();
_password.dispose();
super.dispose();
}
Future<void> _signIn() async {
final pass = _password.text;
final digits = _phone.text.trim();
if (pass.isEmpty) {
_toast('Isi password');
return;
}
if (digits.isEmpty) {
_toast('Isi nomor telepon');
return;
}
setState(() => _busy = true);
try {
await FirebaseMessaging.instance.requestPermission();
final token = await FirebaseMessaging.instance.getToken();
if (token == null || token.length < 50) {
_toast(
'Token FCM belum siap. Izinkan notifikasi & pastikan google-services.json / API key Firebase valid.',
);
return;
}
final result = await OntimeAuth.login(
path: _kLoginPath,
password: pass,
phoneDigits: digits,
dialCodeWithoutPlus: _kDial,
fcmToken: token,
);
if (!mounted) return;
if (result.success) {
_toast('Login sukses, token FCM Firebase sudah dikirim ke backend (FCM v1).');
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (_) => const _HomePlaceholder(title: 'id.ontime.merchant'),
),
);
} else {
_toast(result.message);
}
} catch (e) {
if (mounted) _toast('$e');
} finally {
if (mounted) setState(() => _busy = false);
}
}
void _toast(String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 92),
child: Column(
children: [
const SizedBox(height: 20),
const SizedBox(
height: 250,
child: Center(
child: Icon(Icons.storefront, size: 120, color: MerchantCloneApp.primary),
),
),
Container(
margin: const EdgeInsets.fromLTRB(15, 0, 15, 20),
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: const [BoxShadow(color: Color(0x22000000), blurRadius: 8, offset: Offset(0, 4))],
),
child: Column(
children: [
_MerchantInput(
hint: 'Nomor Telepon',
isIcon: false,
controller: _phone,
obscure: false,
),
const SizedBox(height: 10),
_MerchantInput(
hint: 'Password',
isIcon: true,
controller: _password,
obscure: true,
),
const SizedBox(height: 20),
const Text('Lupa Password?', style: TextStyle(color: MerchantCloneApp.primary, fontSize: 14)),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _busy ? null : _signIn,
style: ElevatedButton.styleFrom(
backgroundColor: MerchantCloneApp.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: _busy
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Text('Sign In', style: TextStyle(fontSize: 18)),
),
),
const SizedBox(height: 16),
const Text(
'Dengan masuk, Anda menyetujui kebijakan privasi.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13),
),
],
),
),
],
),
),
),
const Positioned(
top: 15,
left: 15,
child: CircleAvatar(
radius: 20,
backgroundColor: Colors.white,
child: Icon(Icons.arrow_back, color: Colors.black),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 80,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Belum punya akun? ', style: TextStyle(fontSize: 14)),
Text('Daftar disini', style: TextStyle(fontSize: 16, color: MerchantCloneApp.primary, fontWeight: FontWeight.w600)),
],
),
SizedBox(height: 8),
Text(
'Note: Jika sudah mendaftar akun baru, tunggu konfirmasi admin untuk info lebih lanjut.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
],
),
),
),
],
),
),
);
}
}
class _HomePlaceholder extends StatelessWidget {
const _HomePlaceholder({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: const Center(child: Text('Login berhasil')),
);
}
}
class _MerchantInput extends StatelessWidget {
const _MerchantInput({
required this.hint,
required this.isIcon,
required this.controller,
required this.obscure,
});
final String hint;
final bool isIcon;
final TextEditingController controller;
final bool obscure;
@override
Widget build(BuildContext context) {
return Container(
height: 50,
decoration: BoxDecoration(border: Border.all(color: const Color(0xFFC4C4C4)), borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
SizedBox(
width: 80,
child: Center(
child: isIcon
? const Icon(Icons.lock_outline, color: MerchantCloneApp.primary)
: const Text('+62', style: TextStyle(color: MerchantCloneApp.primary, fontSize: 18)),
),
),
Container(width: 1, margin: const EdgeInsets.symmetric(vertical: 6), color: const Color(0xFFC4C4C4)),
Expanded(
child: TextField(
controller: controller,
obscureText: obscure,
keyboardType: isIcon ? TextInputType.visiblePassword : TextInputType.phone,
decoration: InputDecoration(
hintText: hint,
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 10),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class LoginResult {
LoginResult({required this.success, required this.message});
final bool success;
final String message;
}
/// Matches native `ServiceGenerator.createService` + login POST (Basic auth + JSON body).
class OntimeAuth {
OntimeAuth._();
static const String baseUrl = 'https://apitest.semestaterpadu.my.id/api/';
static Future<LoginResult> login({
required String path,
required String password,
String email = '',
required String phoneDigits,
required String dialCodeWithoutPlus,
required String fcmToken,
}) async {
final cleanedDial = dialCodeWithoutPlus.replaceAll('+', '').trim();
final digitsOnly = phoneDigits.replaceAll(RegExp(r'\D'), '');
final local = digitsOnly.replaceFirst(RegExp(r'^0+'), '');
final fullPhone = digitsOnly.startsWith(cleanedDial)
? digitsOnly
: '$cleanedDial$local';
final useEmail = email.trim().isNotEmpty;
final basicUser = useEmail ? email.trim() : fullPhone;
final body = <String, dynamic>{
'password': password,
// Send all common aliases expected by backend FCM v1 helper.
'firebase_token': fcmToken,
'fcm_token': fcmToken,
'reg_id': fcmToken,
'token': fcmToken,
if (useEmail) ...{
'email': email.trim(),
'no_telepon': '',
} else ...{
'email': null,
'no_telepon': fullPhone,
},
};
final uri = Uri.parse('$baseUrl$path');
final auth = base64Encode(utf8.encode('$basicUser:$password'));
final resp = await http
.post(
uri,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Basic $auth',
},
body: jsonEncode(body),
)
.timeout(const Duration(seconds: 60));
Map<String, dynamic>? json;
try {
if (resp.body.isNotEmpty) {
final decoded = jsonDecode(resp.body);
if (decoded is Map<String, dynamic>) json = decoded;
}
} catch (_) {}
final msg = json?['message']?.toString() ?? '';
final data = json?['data'];
final hasData = data is List && data.isNotEmpty;
final ok = resp.statusCode >= 200 &&
resp.statusCode < 300 &&
msg.toLowerCase() == 'found' &&
hasData;
if (ok) return LoginResult(success: true, message: msg);
if (msg.isNotEmpty) {
return LoginResult(success: false, message: msg);
}
if (resp.statusCode == 401) {
return LoginResult(success: false, message: 'no hp atau password salah!');
}
if (resp.body.isNotEmpty && resp.body.length < 400) {
return LoginResult(success: false, message: resp.body);
}
return LoginResult(
success: false,
message: 'Login gagal (HTTP ${resp.statusCode})',
);
}
}

View File

@@ -0,0 +1,306 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11
url: "https://pub.dev"
source: hosted
version: "1.3.59"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5"
url: "https://pub.dev"
source: hosted
version: "3.15.2"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37"
url: "https://pub.dev"
source: hosted
version: "2.24.1"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
url: "https://pub.dev"
source: hosted
version: "15.2.10"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
url: "https://pub.dev"
source: hosted
version: "4.6.10"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
url: "https://pub.dev"
source: hosted
version: "3.10.10"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks:
dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.22.0"

View File

@@ -0,0 +1,22 @@
name: flutter_merchant_clone
description: OnTime Merchant Flutter conversion
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
http: ^1.2.2
firebase_core: ^3.8.1
firebase_messaging: ^15.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true

View File

@@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_merchant_clone/main.dart';
void main() {
testWidgets('app launches', (WidgetTester tester) async {
await tester.pumpWidget(const MerchantCloneApp());
await tester.pumpAndSettle();
expect(find.text('Sign In'), findsOneWidget);
});
}

View File

@@ -0,0 +1,17 @@
# flutter_user_clone
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -3,10 +3,11 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
namespace = "com.example.ontime_merchant_flutter"
namespace = "id.ontime.customer"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -21,7 +22,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.ontime_merchant_flutter"
applicationId = "id.ontime.customer"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion

View File

@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:label="ontime_driver_flutter"
android:label="OnTime Customer"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

View File

@@ -1,4 +1,4 @@
package com.example.ontime_merchant_flutter
package id.ontime.customer
import io.flutter.embedding.android.FlutterActivity

Some files were not shown because too many files have changed in this diff Show More