demo transaksi
@@ -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 {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
|
|||||||
@@ -491,8 +491,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
request.setPassword(password.getText().toString());
|
request.setPassword(password.getText().toString());
|
||||||
|
|
||||||
// Enforce FCM token: login ONLY proceeds when we have a non-empty Firebase token.
|
// Best effort FCM token: use cached token first, then try fresh token.
|
||||||
// 1) Try to use token that was already stored in Realm (BaseApp / MessagingService.onNewToken).
|
|
||||||
io.realm.Realm realm = BaseApp.getInstance(this).getRealmInstance();
|
io.realm.Realm realm = BaseApp.getInstance(this).getRealmInstance();
|
||||||
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
|
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
|
||||||
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
|
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
|
||||||
@@ -501,7 +500,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) If not yet stored, request a fresh token from FirebaseMessaging.
|
// If token is still warming up, continue login without blocking user.
|
||||||
try {
|
try {
|
||||||
FirebaseMessaging.getInstance().getToken()
|
FirebaseMessaging.getInstance().getToken()
|
||||||
.addOnCompleteListener(task -> {
|
.addOnCompleteListener(task -> {
|
||||||
@@ -518,15 +517,11 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
doLoginRequest(request, emailText);
|
doLoginRequest(request, emailText);
|
||||||
} else {
|
} else {
|
||||||
// Token is required: stop login and show message.
|
doLoginRequest(request, emailText);
|
||||||
progresshide();
|
|
||||||
notif("Firebase token not ready, please check your network and try again.");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
// Firebase not initialized: block login instead of proceeding without token.
|
doLoginRequest(request, emailText);
|
||||||
progresshide();
|
|
||||||
notif("Firebase is not initialized, cannot login. Please restart the app.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +534,8 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
progresshide();
|
progresshide();
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
LoginResponseJson body = response.body();
|
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);
|
User user = body.getData().get(0);
|
||||||
saveUser(user);
|
saveUser(user);
|
||||||
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
|
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
|
||||||
@@ -549,6 +545,8 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
} else {
|
} else {
|
||||||
notif(getString(R.string.phoneemailwrong));
|
notif(getString(R.string.phoneemailwrong));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
notif(getString(R.string.phoneemailwrong));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ public class BaseApp extends Application {
|
|||||||
|
|
||||||
// FCM v1: async token fetch and topic subscribe (reference: test app).
|
// FCM v1: async token fetch and topic subscribe (reference: test app).
|
||||||
try {
|
try {
|
||||||
FirebaseApp app = FirebaseApp.initializeApp(this);
|
FirebaseApp app = FirebaseApp.getApps(this).isEmpty()
|
||||||
|
? FirebaseApp.initializeApp(this)
|
||||||
|
: FirebaseApp.getInstance();
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
FirebaseMessaging.getInstance().getToken()
|
FirebaseMessaging.getInstance().getToken()
|
||||||
.addOnCompleteListener(task -> {
|
.addOnCompleteListener(task -> {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// Android Gradle Plugin compatible with Gradle 8.x and JDK 21
|
// Android Gradle Plugin compatible with Gradle 8.x and JDK 21
|
||||||
classpath 'com.android.tools.build:gradle:8.5.0'
|
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
|
// Realm plugin variant that uses the new AGP transformer API
|
||||||
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'
|
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'realm-android'
|
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 {
|
android {
|
||||||
namespace "id.ontime.merchant"
|
namespace "id.ontime.merchant"
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
config {
|
config {
|
||||||
storeFile file('ontimekeystore.jks')
|
storeFile signingStoreFile
|
||||||
storePassword '123456@ontime'
|
storePassword signingStorePassword
|
||||||
keyAlias 'ontimekeystore'
|
keyAlias signingKeyAlias
|
||||||
keyPassword '123456@ontime'
|
keyPassword signingKeyPassword
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
storeFile file('ontimekeystore.jks')
|
storeFile signingStoreFile
|
||||||
storePassword '123456@ontime'
|
storePassword signingStorePassword
|
||||||
keyAlias 'ontimekeystore'
|
keyAlias signingKeyAlias
|
||||||
keyPassword '123456@ontime'
|
keyPassword signingKeyPassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
request.setPassword(password.getText().toString());
|
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();
|
io.realm.Realm realm = BaseApp.getInstance(this).getRealmInstance();
|
||||||
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
|
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
|
||||||
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
|
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
|
||||||
@@ -513,8 +513,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
performLoginRequest(request, emailText);
|
performLoginRequest(request, emailText);
|
||||||
} else {
|
} else {
|
||||||
progresshide();
|
performLoginRequest(request, emailText);
|
||||||
notif("Firebase token not ready, please check your network and try again.");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -528,7 +527,8 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
progresshide();
|
progresshide();
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
LoginResponseJson body = response.body();
|
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);
|
User user = body.getData().get(0);
|
||||||
saveUser(user);
|
saveUser(user);
|
||||||
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
|
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
|
||||||
@@ -538,6 +538,8 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
} else {
|
} else {
|
||||||
notif(getString(R.string.phoneemailwrong));
|
notif(getString(R.string.phoneemailwrong));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
notif(getString(R.string.phoneemailwrong));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ public class BaseApp extends Application {
|
|||||||
realmInstance = Realm.getDefaultInstance();
|
realmInstance = Realm.getDefaultInstance();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FirebaseApp app = FirebaseApp.initializeApp(this);
|
FirebaseApp app = FirebaseApp.getApps(this).isEmpty()
|
||||||
|
? FirebaseApp.initializeApp(this)
|
||||||
|
: FirebaseApp.getInstance();
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
FirebaseMessaging.getInstance().getToken()
|
FirebaseMessaging.getInstance().getToken()
|
||||||
.addOnCompleteListener(task -> {
|
.addOnCompleteListener(task -> {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ buildscript {
|
|||||||
// Android Gradle Plugin compatible with Gradle 8.x and JDK 21
|
// Android Gradle Plugin compatible with Gradle 8.x and JDK 21
|
||||||
classpath 'com.android.tools.build:gradle:8.5.0'
|
classpath 'com.android.tools.build:gradle:8.5.0'
|
||||||
classpath 'com.jakewharton:butterknife-gradle-plugin:9.0.0-rc2'
|
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
|
// Realm plugin variant that uses the new AGP transformer API
|
||||||
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'
|
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,25 @@ apply plugin: 'realm-android'
|
|||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
//apply plugin: 'kotlin-android-extensions'
|
//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 {
|
android {
|
||||||
namespace "id.ontime.customer"
|
namespace "id.ontime.customer"
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
// Use local ontime keystore (make sure this file exists in the app module)
|
// Use local ontime keystore (make sure this file exists in the app module)
|
||||||
storeFile file('ontimekeystore.jks')
|
storeFile signingStoreFile
|
||||||
storePassword '123456@ontime'
|
storePassword signingStorePassword
|
||||||
keyAlias 'ontimekeystore'
|
keyAlias signingKeyAlias
|
||||||
keyPassword '123456@ontime'
|
keyPassword signingKeyPassword
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
request.setPassword(password.getText().toString());
|
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();
|
Realm realm = BaseApp.getInstance(this).getRealmInstance();
|
||||||
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
|
FirebaseToken storedToken = realm.where(FirebaseToken.class).findFirst();
|
||||||
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
|
if (storedToken != null && storedToken.getTokenId() != null && !storedToken.getTokenId().isEmpty()) {
|
||||||
@@ -569,15 +569,13 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
doLoginRequest(request, emailText);
|
doLoginRequest(request, emailText);
|
||||||
} else {
|
} else {
|
||||||
progresshide();
|
Log.w(TAG, "FCM getToken not ready, continue login without token");
|
||||||
Log.e(TAG, "FCM getToken failed: " + (task.getException() != null ? task.getException().getMessage() : "empty token"));
|
doLoginRequest(request, emailText);
|
||||||
notif(getString(R.string.fcm_token_required));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
progresshide();
|
Log.w(TAG, "FirebaseMessaging unavailable during login, continue without token", e);
|
||||||
Log.e(TAG, "FirebaseMessaging not available; FCM token is required", e);
|
doLoginRequest(request, emailText);
|
||||||
notif(getString(R.string.fcm_token_required));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,19 +618,24 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
notif(showMsg);
|
notif(showMsg);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String errMsg = "Login failed";
|
String errMsg = getString(R.string.phoneemailwrong);
|
||||||
try {
|
try {
|
||||||
if (response.errorBody() != null) {
|
if (response.errorBody() != null) {
|
||||||
String errBody = response.errorBody().string();
|
String errBody = response.errorBody().string();
|
||||||
Log.e(TAG, "Login error body: code=" + code + " body=" + (errBody != null ? errBody : "null"));
|
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 {
|
} else {
|
||||||
Log.e(TAG, "Login error: code=" + code + " no error body");
|
Log.e(TAG, "Login error: code=" + code + " no error body");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Login error reading body", 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);
|
notif(errMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -837,16 +837,14 @@ public class RegisterActivity extends AppCompatActivity {
|
|||||||
request.setCountrycode(countryCode.getText().toString());
|
request.setCountrycode(countryCode.getText().toString());
|
||||||
request.setChecked(check);
|
request.setChecked(check);
|
||||||
|
|
||||||
// FCM v1: get token async then register (token is mandatory)
|
// FCM v1: best effort token fetch before register.
|
||||||
try {
|
try {
|
||||||
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
|
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
|
||||||
if (task.isSuccessful() && task.getResult() != null && !task.getResult().isEmpty()) {
|
if (task.isSuccessful() && task.getResult() != null && !task.getResult().isEmpty()) {
|
||||||
request.setToken(task.getResult());
|
request.setToken(task.getResult());
|
||||||
} else {
|
} else {
|
||||||
progresshide();
|
Log.w("RegisterActivity", "FCM getToken not ready, continue register without token");
|
||||||
Log.e("RegisterActivity", "FCM getToken failed for register: " + (task.getException() != null ? task.getException().getMessage() : "empty token"));
|
request.setToken("");
|
||||||
notif(getString(R.string.fcm_token_required));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
UserService service = ServiceGenerator.createService(UserService.class, request.getEmail(), request.getPassword());
|
UserService service = ServiceGenerator.createService(UserService.class, request.getEmail(), request.getPassword());
|
||||||
service.register(request).enqueue(new Callback<RegisterResponseJson>() {
|
service.register(request).enqueue(new Callback<RegisterResponseJson>() {
|
||||||
@@ -884,9 +882,42 @@ public class RegisterActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException e) {
|
} catch (IllegalStateException e) {
|
||||||
progresshide();
|
Log.w("RegisterActivity", "FirebaseMessaging unavailable during register, continue without token", e);
|
||||||
Log.e("RegisterActivity", "FirebaseMessaging not available; FCM token is required for register", e);
|
request.setToken("");
|
||||||
notif(getString(R.string.fcm_token_required));
|
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();
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ public class BaseApp extends Application {
|
|||||||
|
|
||||||
// FCM v1: async token fetch and topic subscribe (reference: test app LoginActivity).
|
// FCM v1: async token fetch and topic subscribe (reference: test app LoginActivity).
|
||||||
try {
|
try {
|
||||||
FirebaseApp app = FirebaseApp.initializeApp(this);
|
FirebaseApp app = FirebaseApp.getApps(this).isEmpty()
|
||||||
|
? FirebaseApp.initializeApp(this)
|
||||||
|
: FirebaseApp.getInstance();
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
FirebaseMessaging.getInstance().getToken()
|
FirebaseMessaging.getInstance().getToken()
|
||||||
.addOnCompleteListener(task -> {
|
.addOnCompleteListener(task -> {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ buildscript {
|
|||||||
classpath 'com.android.tools.build:gradle:8.5.0'
|
classpath 'com.android.tools.build:gradle:8.5.0'
|
||||||
classpath 'com.jakewharton:butterknife-gradle-plugin:9.0.0-rc2'
|
classpath 'com.jakewharton:butterknife-gradle-plugin:9.0.0-rc2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
|
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
|
// Realm plugin variant that uses the new AGP transformer API
|
||||||
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'
|
classpath 'io.realm:realm-gradle-plugin:10.14.0-transformer-api'
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ $config['fcm_credentials_json'] = '';
|
|||||||
$config['fcm_credentials_path'] = FCPATH . 'ngojol-trial-firebase-adminsdk-lc81n-00c9e935db.json';
|
$config['fcm_credentials_path'] = FCPATH . 'ngojol-trial-firebase-adminsdk-lc81n-00c9e935db.json';
|
||||||
$config['fcm_limit_per_hour'] = 100;
|
$config['fcm_limit_per_hour'] = 100;
|
||||||
$config['fcm_limit_per_day'] = 500;
|
$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_hour'] = 1000;
|
||||||
$config['maps_limit_per_day'] = 5000;
|
$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_CREDENTIALS_PATH', $config['fcm_credentials_path']);
|
||||||
define('FCM_LIMIT_PER_HOUR', $config['fcm_limit_per_hour']);
|
define('FCM_LIMIT_PER_HOUR', $config['fcm_limit_per_hour']);
|
||||||
define('FCM_LIMIT_PER_DAY', $config['fcm_limit_per_day']);
|
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_HOUR', $config['maps_limit_per_hour']);
|
||||||
define('MAPS_LIMIT_PER_DAY', $config['maps_limit_per_day']);
|
define('MAPS_LIMIT_PER_DAY', $config['maps_limit_per_day']);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ defined('BASEPATH') or exit('No direct script access allowed');
|
|||||||
| Legacy FCM config – DO NOT USE
|
| Legacy FCM config – DO NOT USE
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Push notifications use FCM HTTP v1 only (see application/helpers/fcm_v1_helper.php).
|
| 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.
|
| Service account path/project id live in application/config/config.php (constants
|
||||||
| This file exists so the deprecated application/libraries/Fcm.php does not
|
| FCM_CREDENTIALS_PATH, FCM_CREDENTIALS_JSON, FCM_PROJECT_ID). This file exists so the
|
||||||
| load a missing config; the legacy library must not be used (no legacy API key).
|
| 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_key'] = '';
|
||||||
$config['fcm_api_send_address'] = 'https://fcm.googleapis.com/fcm/send';
|
$config['fcm_api_send_address'] = 'https://fcm.googleapis.com/fcm/send';
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ class appnotification extends CI_Controller
|
|||||||
is_logged_in();
|
is_logged_in();
|
||||||
|
|
||||||
$this->load->model('notification_model', 'notif');
|
$this->load->model('notification_model', 'notif');
|
||||||
|
$this->load->model('Pelanggan_model', 'pelanggan_model');
|
||||||
$this->load->library('form_validation');
|
$this->load->library('form_validation');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
$data['pelanggan_list'] = $this->pelanggan_model->get_all_for_notification_picker();
|
||||||
$this->load->view('includes/header');
|
$this->load->view('includes/header');
|
||||||
$this->load->view('appnotification/index');
|
$this->load->view('appnotification/index', $data);
|
||||||
$this->load->view('includes/footer');
|
$this->load->view('includes/footer');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,10 +34,57 @@ class appnotification extends CI_Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$topic = $this->input->post('topic');
|
|
||||||
$title = $this->input->post('title');
|
$title = $this->input->post('title');
|
||||||
$message = $this->input->post('message');
|
$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);
|
$ok = $this->notif->send_notif($title, $message, $topic);
|
||||||
if ($ok) {
|
if ($ok) {
|
||||||
$this->session->set_flashdata('send', 'Notifikasi berhasil dikirim');
|
$this->session->set_flashdata('send', 'Notifikasi berhasil dikirim');
|
||||||
|
|||||||
@@ -76,10 +76,24 @@ class Driver extends REST_Controller
|
|||||||
|
|
||||||
$data = file_get_contents("php://input");
|
$data = file_get_contents("php://input");
|
||||||
$decoded_data = json_decode($data);
|
$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.
|
// 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 = function_exists('fcm_v1_device_token_from_request')
|
||||||
$token_from_token = isset($decoded_data->token) ? trim((string) $decoded_data->token) : '';
|
? fcm_v1_device_token_from_request($decoded_data)
|
||||||
$token = $token_from_regid !== '' ? $token_from_regid : $token_from_token;
|
: '';
|
||||||
$reg_id = array();
|
$reg_id = array();
|
||||||
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
|
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
|
||||||
$reg_id['reg_id'] = $token;
|
$reg_id['reg_id'] = $token;
|
||||||
@@ -149,8 +163,10 @@ class Driver extends REST_Controller
|
|||||||
);
|
);
|
||||||
$ins = $this->Driver_model->my_location($data);
|
$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.
|
// When driver sends valid FCM token with location, update so they receive order requests.
|
||||||
$reg_id = isset($decoded_data->reg_id) ? trim((string) $decoded_data->reg_id) : '';
|
$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)) {
|
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);
|
$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()
|
function accept_post()
|
||||||
{
|
{
|
||||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
@@ -383,6 +415,7 @@ class Driver extends REST_Controller
|
|||||||
$data = file_get_contents("php://input");
|
$data = file_get_contents("php://input");
|
||||||
$dec_data = json_decode($data);
|
$dec_data = json_decode($data);
|
||||||
log_message('debug', 'accept_post: payload=' . $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(
|
$data_req = array(
|
||||||
'id_driver' => $dec_data->id,
|
'id_driver' => $dec_data->id,
|
||||||
@@ -390,13 +423,13 @@ class Driver extends REST_Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
$condition = array(
|
$condition = array(
|
||||||
'id_driver' => $dec_data->id,
|
'id_driver' => $dec_data->id
|
||||||
'status' => '1'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$cek_login = $this->Driver_model->get_status_driver($condition);
|
$cek_login = $this->Driver_model->get_status_driver($condition);
|
||||||
log_message('debug', 'accept_post: get_status_driver rows=' . $cek_login->num_rows());
|
$driver_status = $cek_login->num_rows() > 0 ? (string) $cek_login->row('status') : '';
|
||||||
if ($cek_login->num_rows() > 0) {
|
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);
|
$acc_req = $this->Driver_model->accept_request($data_req);
|
||||||
log_message('debug', 'accept_post: accept_request result=' . json_encode($acc_req));
|
log_message('debug', 'accept_post: accept_request result=' . json_encode($acc_req));
|
||||||
@@ -405,6 +438,7 @@ class Driver extends REST_Controller
|
|||||||
'message' => 'berhasil',
|
'message' => 'berhasil',
|
||||||
'data' => 'berhasil'
|
'data' => 'berhasil'
|
||||||
);
|
);
|
||||||
|
$this->log_driver_accept_api($tid_for_log, $data, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
} else {
|
} else {
|
||||||
if ($acc_req['data'] == 'canceled') {
|
if ($acc_req['data'] == 'canceled') {
|
||||||
@@ -412,12 +446,14 @@ class Driver extends REST_Controller
|
|||||||
'message' => 'canceled',
|
'message' => 'canceled',
|
||||||
'data' => 'canceled'
|
'data' => 'canceled'
|
||||||
);
|
);
|
||||||
|
$this->log_driver_accept_api($tid_for_log, $data, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
} else {
|
} else {
|
||||||
$message = array(
|
$message = array(
|
||||||
'message' => 'unknown fail',
|
'message' => 'unknown fail',
|
||||||
'data' => 'canceled'
|
'data' => 'canceled'
|
||||||
);
|
);
|
||||||
|
$this->log_driver_accept_api($tid_for_log, $data, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,6 +462,7 @@ class Driver extends REST_Controller
|
|||||||
'message' => 'unknown fail',
|
'message' => 'unknown fail',
|
||||||
'data' => 'canceled'
|
'data' => 'canceled'
|
||||||
);
|
);
|
||||||
|
$this->log_driver_accept_api($tid_for_log, $data, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,31 @@ class Merchant extends REST_Controller
|
|||||||
|
|
||||||
$data = file_get_contents("php://input");
|
$data = file_get_contents("php://input");
|
||||||
$decoded_data = json_decode($data);
|
$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).
|
// Only save FCM token when valid (relogin overwrites invalid/placeholder tokens).
|
||||||
$token = isset($decoded_data->token) ? trim((string) $decoded_data->token) : '';
|
$token = function_exists('fcm_v1_device_token_from_request')
|
||||||
$token = (isset($decoded_data->reg_id) && trim((string) $decoded_data->reg_id) !== '') ? trim((string) $decoded_data->reg_id) : $token;
|
? fcm_v1_device_token_from_request($decoded_data)
|
||||||
|
: '';
|
||||||
$reg_id = array();
|
$reg_id = array();
|
||||||
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
|
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
|
||||||
$reg_id['token_merchant'] = $token;
|
$reg_id['token_merchant'] = $token;
|
||||||
@@ -75,10 +97,18 @@ class Merchant extends REST_Controller
|
|||||||
|
|
||||||
$condition = array(
|
$condition = array(
|
||||||
'password' => sha1($decoded_data->password),
|
'password' => sha1($decoded_data->password),
|
||||||
'telepon_mitra' => $decoded_data->no_telepon,
|
|
||||||
//'token' => $decoded_data->token
|
//'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) {
|
if ($check_banned) {
|
||||||
$message = array(
|
$message = array(
|
||||||
'message' => 'banned',
|
'message' => 'banned',
|
||||||
@@ -90,7 +120,19 @@ class Merchant extends REST_Controller
|
|||||||
$message = array();
|
$message = array();
|
||||||
if ($cek_login->num_rows() > 0) {
|
if ($cek_login->num_rows() > 0) {
|
||||||
if (!empty($reg_id)) {
|
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);
|
$get_pelanggan = $this->Merchantapi_model->get_data_merchant($condition);
|
||||||
$message = array(
|
$message = array(
|
||||||
@@ -102,7 +144,7 @@ class Merchant extends REST_Controller
|
|||||||
} else {
|
} else {
|
||||||
$message = array(
|
$message = array(
|
||||||
'code' => '404',
|
'code' => '404',
|
||||||
'message' => 'nomor hp atau password salah!',
|
'message' => 'nomor hp/email atau password salah!',
|
||||||
'data' => []
|
'data' => []
|
||||||
);
|
);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
@@ -450,24 +492,24 @@ class Merchant extends REST_Controller
|
|||||||
$token = $this->wallet->gettoken($iduser);
|
$token = $this->wallet->gettoken($iduser);
|
||||||
$regid = $this->wallet->getregid($iduser);
|
$regid = $this->wallet->getregid($iduser);
|
||||||
$tokenmerchant = $this->wallet->gettokenmerchant($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'];
|
$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'];
|
$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'];
|
$topic = $tokenmerchant['token_merchant'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$title = 'Sukses';
|
$title = 'Sukses';
|
||||||
$message = 'Permintaan berhasil dikirim';
|
$message = 'Permintaan berhasil dikirim';
|
||||||
$saldo = $this->wallet->getsaldo($iduser);
|
$saldo = $this->wallet->getsaldo($iduser);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$this->wallet->ubahsaldo($iduser, $amount, $saldo);
|
$this->wallet->ubahsaldo($iduser, $amount, $saldo);
|
||||||
//$this->wallet->ubahstatuswithdrawbyid($id);
|
if ($topic !== null) {
|
||||||
$this->wallet->send_notif($title, $message, $topic);
|
$this->wallet->send_notif($title, $message, $topic);
|
||||||
|
}
|
||||||
|
|
||||||
/* END EDIT */
|
/* END EDIT */
|
||||||
$message = array(
|
$message = array(
|
||||||
|
|||||||
@@ -18,6 +18,71 @@ class Pelanggan extends REST_Controller
|
|||||||
date_default_timezone_set('Asia/Jakarta');
|
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()
|
function index_get()
|
||||||
{
|
{
|
||||||
$this->response("Api for Ontime!", 200);
|
$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);
|
$this->response(array('code' => '400', 'message' => 'Invalid request', 'data' => []), 200);
|
||||||
return;
|
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).
|
// Only save FCM token when valid (relogin overwrites invalid/placeholder tokens).
|
||||||
$token = isset($decoded_data->token) ? trim((string) $decoded_data->token) : '';
|
$token = function_exists('fcm_v1_device_token_from_request')
|
||||||
$token = (isset($decoded_data->reg_id) && trim((string) $decoded_data->reg_id) !== '') ? trim((string) $decoded_data->reg_id) : $token;
|
? fcm_v1_device_token_from_request($decoded_data)
|
||||||
|
: '';
|
||||||
$reg_id = array();
|
$reg_id = array();
|
||||||
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
|
if ($token !== '' && function_exists('fcm_v1_is_valid_device_token') && fcm_v1_is_valid_device_token($token)) {
|
||||||
$reg_id['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.
|
// Generate a deterministic placeholder based on email so the column is never empty.
|
||||||
// This placeholder is intentionally SHORT / starting with "R" + digits so
|
// This placeholder is intentionally SHORT / starting with "R" + digits so
|
||||||
// fcm_v1_is_valid_device_token() will treat it as invalid for push.
|
// 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 === '') {
|
if ($incomingToken === '') {
|
||||||
$emailForToken = isset($dec_data->email) ? strtolower(trim((string) $dec_data->email)) : '';
|
$emailForToken = isset($dec_data->email) ? strtolower(trim((string) $dec_data->email)) : '';
|
||||||
if ($emailForToken !== '') {
|
if ($emailForToken !== '') {
|
||||||
@@ -1007,6 +1085,7 @@ class Pelanggan extends REST_Controller
|
|||||||
|
|
||||||
function request_transaksi_post()
|
function request_transaksi_post()
|
||||||
{
|
{
|
||||||
|
$endpoint = 'request_transaksi_post';
|
||||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
||||||
header("HTTP/1.0 401 Unauthorized");
|
header("HTTP/1.0 401 Unauthorized");
|
||||||
@@ -1014,18 +1093,19 @@ class Pelanggan extends REST_Controller
|
|||||||
} else {
|
} else {
|
||||||
$cek = $this->Pelanggan_model->check_banned_user($_SERVER['PHP_AUTH_USER']);
|
$cek = $this->Pelanggan_model->check_banned_user($_SERVER['PHP_AUTH_USER']);
|
||||||
if ($cek) {
|
if ($cek) {
|
||||||
log_message('debug', 'request_transaksi_post: banned user ' . $_SERVER['PHP_AUTH_USER']);
|
|
||||||
$message = array(
|
$message = array(
|
||||||
'message' => 'fail',
|
'message' => 'fail',
|
||||||
'data' => 'Status User Banned'
|
'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);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = file_get_contents("php://input");
|
$data = file_get_contents("php://input");
|
||||||
$dec_data = json_decode($data);
|
$dec_data = json_decode($data);
|
||||||
log_message('debug', 'request_transaksi_post: payload=' . $data);
|
$this->log_order_api_request($endpoint, $data);
|
||||||
|
|
||||||
$data_req = array(
|
$data_req = array(
|
||||||
'id_pelanggan' => $dec_data->id_pelanggan,
|
'id_pelanggan' => $dec_data->id_pelanggan,
|
||||||
@@ -1046,9 +1126,23 @@ class Pelanggan extends REST_Controller
|
|||||||
);
|
);
|
||||||
|
|
||||||
$request = $this->Pelanggan_model->insert_transaksi($data_req);
|
$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 ($request['status']) {
|
||||||
if (isset($request['data'][0]->id)) {
|
$idTransaksi = $idTransaksiNum > 0 ? $idTransaksiNum : 'unknown';
|
||||||
log_message('debug', 'request_transaksi_post: success id_transaksi=' . $request['data'][0]->id . ' id_pelanggan=' . $dec_data->id_pelanggan . ' fitur=' . $dec_data->order_fitur);
|
$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 {
|
} else {
|
||||||
log_message('debug', 'request_transaksi_post: success (no id in data) payload=' . json_encode($request['data']));
|
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',
|
'message' => 'success',
|
||||||
'data' => $request['data']
|
'data' => $request['data']
|
||||||
);
|
);
|
||||||
|
$this->log_order_api_response($endpoint, $message);
|
||||||
|
$this->save_order_creation_logs($idTransaksiNum, $data, $message, $driverTargets);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
} else {
|
} else {
|
||||||
log_message('error', 'request_transaksi_post: insert_transaksi fail data=' . json_encode($request['data']));
|
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',
|
'message' => 'fail',
|
||||||
'data' => $request['data']
|
'data' => $request['data']
|
||||||
);
|
);
|
||||||
|
$this->log_order_api_response($endpoint, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_status_transaksi_post()
|
function check_status_transaksi_post()
|
||||||
{
|
{
|
||||||
|
$endpoint = 'check_status_transaksi_post';
|
||||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
||||||
header("HTTP/1.0 401 Unauthorized");
|
header("HTTP/1.0 401 Unauthorized");
|
||||||
@@ -1077,14 +1175,19 @@ class Pelanggan extends REST_Controller
|
|||||||
|
|
||||||
$data = file_get_contents("php://input");
|
$data = file_get_contents("php://input");
|
||||||
$dec_data = json_decode($data);
|
$dec_data = json_decode($data);
|
||||||
log_message('debug', 'check_status_transaksi_post: payload=' . $data);
|
$this->log_order_api_request($endpoint, $data);
|
||||||
|
|
||||||
$dataTrans = array(
|
$dataTrans = array(
|
||||||
'id_transaksi' => $dec_data->id_transaksi
|
'id_transaksi' => $dec_data->id_transaksi
|
||||||
);
|
);
|
||||||
|
|
||||||
$getStatus = $this->Pelanggan_model->check_status($dataTrans);
|
$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);
|
$this->response($getStatus, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1321,6 +1424,7 @@ class Pelanggan extends REST_Controller
|
|||||||
|
|
||||||
function request_transaksi_send_post()
|
function request_transaksi_send_post()
|
||||||
{
|
{
|
||||||
|
$endpoint = 'request_transaksi_send_post';
|
||||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
||||||
header("HTTP/1.0 401 Unauthorized");
|
header("HTTP/1.0 401 Unauthorized");
|
||||||
@@ -1332,12 +1436,15 @@ class Pelanggan extends REST_Controller
|
|||||||
'message' => 'fail',
|
'message' => 'fail',
|
||||||
'data' => 'Status User Banned'
|
'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);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = file_get_contents("php://input");
|
$data = file_get_contents("php://input");
|
||||||
$dec_data = json_decode($data);
|
$dec_data = json_decode($data);
|
||||||
|
$this->log_order_api_request($endpoint, $data);
|
||||||
|
|
||||||
$data_req = array(
|
$data_req = array(
|
||||||
'id_pelanggan' => $dec_data->id_pelanggan,
|
'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);
|
$request = $this->Pelanggan_model->insert_transaksi_send($data_req, $dataDetail);
|
||||||
if ($request['status']) {
|
if ($request['status']) {
|
||||||
|
$resultRows = $request['data']->result();
|
||||||
$message = array(
|
$message = array(
|
||||||
'message' => 'success',
|
'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);
|
$this->response($message, 200);
|
||||||
} else {
|
} else {
|
||||||
$message = array(
|
$message = array(
|
||||||
'message' => 'fail',
|
'message' => 'fail',
|
||||||
'data' => []
|
'data' => []
|
||||||
);
|
);
|
||||||
|
$this->log_order_api_response($endpoint, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1457,6 +1577,7 @@ class Pelanggan extends REST_Controller
|
|||||||
|
|
||||||
function inserttransaksimerchant_post()
|
function inserttransaksimerchant_post()
|
||||||
{
|
{
|
||||||
|
$endpoint = 'inserttransaksimerchant_post';
|
||||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||||
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
header("WWW-Authenticate: Basic realm=\"Private Area\"");
|
||||||
header("HTTP/1.0 401 Unauthorized");
|
header("HTTP/1.0 401 Unauthorized");
|
||||||
@@ -1468,12 +1589,15 @@ class Pelanggan extends REST_Controller
|
|||||||
'message' => 'fail',
|
'message' => 'fail',
|
||||||
'data' => 'Status User Banned'
|
'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);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = file_get_contents("php://input");
|
$data = file_get_contents("php://input");
|
||||||
$dec_data = json_decode($data);
|
$dec_data = json_decode($data);
|
||||||
|
$this->log_order_api_request($endpoint, $data);
|
||||||
|
|
||||||
$data_transaksi = array(
|
$data_transaksi = array(
|
||||||
'id_pelanggan' => $dec_data->id_pelanggan,
|
'id_pelanggan' => $dec_data->id_pelanggan,
|
||||||
@@ -1531,9 +1655,14 @@ class Pelanggan extends REST_Controller
|
|||||||
$message = array(
|
$message = array(
|
||||||
'message' => 'success',
|
'message' => 'success',
|
||||||
'data' => $result['data'],
|
'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);
|
$this->response($message, 200);
|
||||||
} else {
|
} else {
|
||||||
$message = array(
|
$message = array(
|
||||||
@@ -1541,6 +1670,7 @@ class Pelanggan extends REST_Controller
|
|||||||
'data' => []
|
'data' => []
|
||||||
|
|
||||||
);
|
);
|
||||||
|
$this->log_order_api_response($endpoint, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1549,6 +1679,7 @@ class Pelanggan extends REST_Controller
|
|||||||
'data' => []
|
'data' => []
|
||||||
|
|
||||||
);
|
);
|
||||||
|
$this->log_order_api_response($endpoint, $message);
|
||||||
$this->response($message, 200);
|
$this->response($message, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ defined('BASEPATH') or exit('No direct script access allowed');
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* FCM HTTP v1 API helper (Firebase Cloud Messaging).
|
* FCM HTTP v1 API helper (Firebase Cloud Messaging).
|
||||||
* Uses OAuth2 access token from service account credentials (env: FCM_CREDENTIALS_PATH or FCM_CREDENTIALS_JSON).
|
* OAuth2 access token from service account: 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).
|
* 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')) {
|
if (!function_exists('fcm_v1_get_credentials')) {
|
||||||
@@ -264,3 +265,80 @@ if (!function_exists('fcm_v1_send')) {
|
|||||||
return $resp;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ defined('BASEPATH') or exit('No direct script access allowed');
|
|||||||
* UNIQUE KEY uk_key_period (key_name, period_type, period_value)
|
* 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')) {
|
if (!function_exists('quota_limiter_allow')) {
|
||||||
|
|||||||
@@ -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)
|
public function check_exist($email, $phone)
|
||||||
{
|
{
|
||||||
$cek = $this->db->query("SELECT id_mitra FROM mitra where email_mitra = '$email' AND telepon_mitra='$phone'");
|
$cek = $this->db->query("SELECT id_mitra FROM mitra where email_mitra = '$email' AND telepon_mitra='$phone'");
|
||||||
|
|||||||
@@ -64,11 +64,7 @@ class notification_model extends CI_model
|
|||||||
public function send_notif($title, $message, $target)
|
public function send_notif($title, $message, $target)
|
||||||
{
|
{
|
||||||
if ($this->is_token_empty($target)) {
|
if ($this->is_token_empty($target)) {
|
||||||
log_message('debug', 'send_notif: skip, no token');
|
log_message('debug', 'send_notif: skip, empty target');
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!$this->is_valid_fcm_token($target)) {
|
|
||||||
log_message('debug', 'send_notif: skip, invalid/placeholder FCM token');
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!function_exists('fcm_v1_validate_token')) {
|
if (!function_exists('fcm_v1_validate_token')) {
|
||||||
@@ -87,7 +83,11 @@ class notification_model extends CI_model
|
|||||||
'title' => $title,
|
'title' => $title,
|
||||||
'body' => $message,
|
'body' => $message,
|
||||||
);
|
);
|
||||||
return $this->send_generic_to_token($target, $data, $options);
|
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)
|
public function send_notif_topup($title, $id, $message, $method, $token)
|
||||||
|
|||||||
@@ -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
|
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s, fitur f
|
||||||
WHERE ld.id_driver = d.id
|
WHERE ld.id_driver = d.id
|
||||||
AND f.id_fitur = $fitur
|
AND f.id_fitur = $fitur
|
||||||
AND ld.status = '1'
|
AND ld.status IN ('1','4')
|
||||||
AND dj.id = d.job
|
AND dj.id = d.job
|
||||||
AND d.job = f.driver_job
|
AND d.job = f.driver_job
|
||||||
AND d.status = '1'
|
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)
|
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;
|
$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;
|
$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();
|
$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;
|
$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
|
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s, fitur f
|
||||||
WHERE ld.id_driver = d.id
|
WHERE ld.id_driver = d.id
|
||||||
AND f.id_fitur = $fitur
|
AND f.id_fitur = $fitur
|
||||||
AND ld.status = '1'
|
AND ld.status IN ('1','4')
|
||||||
AND dj.id = d.job
|
AND dj.id = d.job
|
||||||
AND d.job = f.driver_job
|
AND d.job = f.driver_job
|
||||||
AND d.status = '1'
|
AND d.status = '1'
|
||||||
@@ -1849,5 +1849,80 @@ class Pelanggan_model extends CI_model
|
|||||||
$this->db->update('history_transaksi', $data);
|
$this->db->update('history_transaksi', $data);
|
||||||
return true;
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,55 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?= form_open_multipart('appnotification/send'); ?>
|
<?= form_open_multipart('appnotification/send'); ?>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="newscategory">Kirim Ke</label>
|
<label>Tipe kirim</label>
|
||||||
<select class="js-example-basic-single" style="width:100%" name='topic'>
|
<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="pelanggan">User</option>
|
||||||
<option value="driver">Driver</option>
|
<option value="driver">Driver</option>
|
||||||
<option value="mitra">Merchant Partner</option>
|
<option value="mitra">Merchant Partner</option>
|
||||||
<option value="ouride">Semua</option>
|
<option value="ouride">Semua</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label for="title">Judul</label>
|
<label for="title">Judul</label>
|
||||||
<input type="text" class="form-control" placeholder="notification" name="title" required>
|
<input type="text" class="form-control" placeholder="notification" name="title" required>
|
||||||
|
|||||||
@@ -233,6 +233,36 @@
|
|||||||
<?= $transaksi['catatan'] ?></p>
|
<?= $transaksi['catatan'] ?></p>
|
||||||
<hr>
|
<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) { ?>
|
<?php if ($transaksi['home'] == 4) { ?>
|
||||||
<div class="container-fluid mt-5 d-flex justify-content-center w-100">
|
<div class="container-fluid mt-5 d-flex justify-content-center w-100">
|
||||||
<div class="table-responsive w-100">
|
<div class="table-responsive w-100">
|
||||||
|
|||||||
8
backendpanel/sql/alter_transaksi_api_log.sql
Normal 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
@@ -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 "$@"
|
||||||
|
|
||||||
23
flutter_conversion_notes.md
Normal 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.
|
||||||
17
flutter_driver_clone/README.md
Normal 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.
|
||||||
@@ -3,10 +3,11 @@ plugins {
|
|||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.ontime_user_flutter"
|
namespace = "id.ontime.driver"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// 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.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:label="ontime_merchant_flutter"
|
android:label="OnTime Driver"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ontime_user_flutter
|
package id.ontime.driver
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -21,6 +21,7 @@ plugins {
|
|||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" 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")
|
include(":app")
|
||||||
300
flutter_driver_clone/lib/main.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
flutter_driver_clone/lib/ontime_auth.dart
Normal 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})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
306
flutter_driver_clone/pubspec.lock
Normal 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"
|
||||||
22
flutter_driver_clone/pubspec.yaml
Normal 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
|
||||||
11
flutter_driver_clone/test/widget_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
17
flutter_merchant_clone/README.md
Normal 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.
|
||||||
@@ -3,10 +3,11 @@ plugins {
|
|||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.ontime_driver_flutter"
|
namespace = "id.ontime.merchant"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// 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.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:label="ontime_user_flutter"
|
android:label="id.ontime.merchant"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ontime_driver_flutter
|
package id.ontime.merchant
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -21,6 +21,7 @@ plugins {
|
|||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" 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")
|
include(":app")
|
||||||
286
flutter_merchant_clone/lib/main.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
flutter_merchant_clone/lib/ontime_auth.dart
Normal 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})',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
306
flutter_merchant_clone/pubspec.lock
Normal 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"
|
||||||
22
flutter_merchant_clone/pubspec.yaml
Normal 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
|
||||||
11
flutter_merchant_clone/test/widget_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
17
flutter_user_clone/README.md
Normal 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.
|
||||||
@@ -3,10 +3,11 @@ plugins {
|
|||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
id("com.google.gms.google-services")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.ontime_merchant_flutter"
|
namespace = "id.ontime.customer"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// 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.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<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
|
<application
|
||||||
android:label="ontime_driver_flutter"
|
android:label="OnTime Customer"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.ontime_merchant_flutter
|
package id.ontime.customer
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |