update
This commit is contained in:
@@ -1,11 +1,21 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val keystoreProperties =
|
||||
Properties().apply {
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystorePropertiesFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.mylis"
|
||||
namespace = "com.duidev.mylis"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
@@ -16,7 +26,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.mylis"
|
||||
applicationId = "com.duidev.mylis"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
@@ -25,11 +35,20 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = (keystoreProperties["storeFile"] as String?)?.let {
|
||||
file(it)
|
||||
}
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"project_info": {
|
||||
"project_number": "732589737888",
|
||||
"project_id": "mylis-a4983",
|
||||
"storage_bucket": "mylis-a4983.firebasestorage.app"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:732589737888:android:3f782e07e62ace55a446c5",
|
||||
"android_client_info": {
|
||||
"package_name": "com.duidev.mylis"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAibwJgxVnkVOOIGsjbf2SpW_isJOFTtAg"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.example.mylis
|
||||
package com.duidev.mylis
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -383,7 +383,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -399,7 +399,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -416,7 +416,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -431,7 +431,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -563,7 +563,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -586,7 +586,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -20,11 +20,13 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SessionStore.baseUrl().then((value) {
|
||||
SessionStore.session().then((value) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_baseUrl.text = value;
|
||||
_baseUrl.text = value.baseUrl;
|
||||
_username.text = value.username ?? '';
|
||||
_password.text = value.password ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +45,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final serverUrl = normalizeBaseUrl(_baseUrl.text);
|
||||
await SessionStore.saveBaseUrl(serverUrl);
|
||||
final api = ApiClient(baseUrl: serverUrl);
|
||||
final data = await api.post('api/mobile/login', {
|
||||
'username': _username.text.trim(),
|
||||
@@ -53,7 +54,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
if (token == null || token.isEmpty) {
|
||||
throw ApiException('Token login tidak diterima dari server.');
|
||||
}
|
||||
await SessionStore.saveToken(token);
|
||||
await SessionStore.saveLogin(
|
||||
baseUrl: serverUrl,
|
||||
token: token,
|
||||
username: _username.text.trim(),
|
||||
password: _password.text,
|
||||
);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ class ApiClient {
|
||||
ApiClient({required this.baseUrl, this.token});
|
||||
|
||||
final String baseUrl;
|
||||
final String? token;
|
||||
String? token;
|
||||
|
||||
Uri _uri(String path, [Map<String, String>? query]) {
|
||||
final normalizedBase = normalizeBaseUrl(baseUrl);
|
||||
@@ -25,12 +25,55 @@ class ApiClient {
|
||||
String path,
|
||||
Map<String, dynamic> body,
|
||||
) async {
|
||||
final uri = _uri(path);
|
||||
return _sendWithRenew(
|
||||
path,
|
||||
(client, uri) => client
|
||||
.post(uri, headers: _headers, body: jsonEncode(body))
|
||||
.timeout(const Duration(seconds: 15)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> get(
|
||||
String path, [
|
||||
Map<String, String>? query,
|
||||
]) async {
|
||||
return _sendWithRenew(
|
||||
path,
|
||||
(client, uri) => client
|
||||
.get(uri, headers: _headers)
|
||||
.timeout(const Duration(seconds: 15)),
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _sendWithRenew(
|
||||
String path,
|
||||
Future<http.Response> Function(http.Client client, Uri uri) send, [
|
||||
Map<String, String>? query,
|
||||
]) async {
|
||||
final uri = _uri(path, query);
|
||||
try {
|
||||
return await _sendOnce(uri, send);
|
||||
} on ApiException catch (error) {
|
||||
if (!_canRenew(path, error)) {
|
||||
rethrow;
|
||||
}
|
||||
final renewed = await _renewToken(uri);
|
||||
if (!renewed) {
|
||||
rethrow;
|
||||
}
|
||||
debugPrint('MyLIS session diperbarui, mengulang request: $uri');
|
||||
return _sendOnce(uri, send);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _sendOnce(
|
||||
Uri uri,
|
||||
Future<http.Response> Function(http.Client client, Uri uri) send,
|
||||
) async {
|
||||
final client = _clientFor(uri);
|
||||
try {
|
||||
final response = await client
|
||||
.post(uri, headers: _headers, body: jsonEncode(body))
|
||||
.timeout(const Duration(seconds: 15));
|
||||
final response = await send(client, uri);
|
||||
return _decode(response, uri);
|
||||
} catch (error, stackTrace) {
|
||||
throw _connectionException(uri, error, stackTrace);
|
||||
@@ -39,19 +82,64 @@ class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> get(
|
||||
String path, [
|
||||
Map<String, String>? query,
|
||||
]) async {
|
||||
final uri = _uri(path, query);
|
||||
final client = _clientFor(uri);
|
||||
bool _canRenew(String path, ApiException error) {
|
||||
return token != null &&
|
||||
error.statusCode == 401 &&
|
||||
path != 'api/mobile/login' &&
|
||||
!path.endsWith('/login');
|
||||
}
|
||||
|
||||
Future<bool> _renewToken(Uri failedUri) async {
|
||||
final credentials = await SessionStore.credentials();
|
||||
if (credentials == null) {
|
||||
debugPrint(
|
||||
'MyLIS session expired untuk $failedUri; kredensial tersimpan tidak ada.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
final loginUri = _uri('api/mobile/login');
|
||||
final client = _clientFor(loginUri);
|
||||
try {
|
||||
debugPrint(
|
||||
'MyLIS session expired untuk $failedUri; mencoba renew token.',
|
||||
);
|
||||
final response = await client
|
||||
.get(uri, headers: _headers)
|
||||
.post(
|
||||
loginUri,
|
||||
headers: const {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'username': credentials.username,
|
||||
'password': credentials.password,
|
||||
}),
|
||||
)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
return _decode(response, uri);
|
||||
final data = _decode(response, loginUri);
|
||||
final token = data['token']?.toString() ?? '';
|
||||
if (token.isEmpty) {
|
||||
debugPrint(
|
||||
'MyLIS renew token gagal: respons login tidak berisi token.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
this.token = token;
|
||||
await SessionStore.saveLogin(
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
);
|
||||
return true;
|
||||
} on ApiException catch (error) {
|
||||
debugPrint('MyLIS renew token gagal: ${error.message}');
|
||||
return false;
|
||||
} catch (error, stackTrace) {
|
||||
throw _connectionException(uri, error, stackTrace);
|
||||
debugPrint('MyLIS renew token gagal tidak terduga: $error');
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
return false;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
@@ -95,6 +183,7 @@ class ApiClient {
|
||||
'Server mengembalikan HTTP ${response.statusCode}. '
|
||||
'URL: $uri. Kemungkinan request masuk ke virtual host/proxy yang salah. '
|
||||
'Detail awal respons: $bodyPreview',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,6 +201,7 @@ class ApiClient {
|
||||
throw ApiException(
|
||||
'Respons server tidak bisa dibaca. '
|
||||
'URL: $uri. Status HTTP: ${response.statusCode}.',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,6 +284,7 @@ class ApiClient {
|
||||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
ApiException(this.message);
|
||||
ApiException(this.message, {this.statusCode});
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ part of '../app/app.dart';
|
||||
class SessionStore {
|
||||
static const _tokenKey = 'mylis_token';
|
||||
static const _baseUrlKey = 'mylis_base_url';
|
||||
static const _usernameKey = 'mylis_username';
|
||||
static const _passwordKey = 'mylis_password';
|
||||
|
||||
static Future<String?> token() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -19,6 +21,8 @@ class SessionStore {
|
||||
return SessionData(
|
||||
token: prefs.getString(_tokenKey),
|
||||
baseUrl: prefs.getString(_baseUrlKey) ?? kDefaultBaseUrl,
|
||||
username: prefs.getString(_usernameKey),
|
||||
password: prefs.getString(_passwordKey),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +31,32 @@ class SessionStore {
|
||||
await prefs.setString(_tokenKey, token);
|
||||
}
|
||||
|
||||
static Future<void> saveLogin({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_baseUrlKey, normalizeBaseUrl(baseUrl));
|
||||
await prefs.setString(_tokenKey, token);
|
||||
await prefs.setString(_usernameKey, username);
|
||||
await prefs.setString(_passwordKey, password);
|
||||
}
|
||||
|
||||
static Future<StoredCredentials?> credentials() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final username = prefs.getString(_usernameKey);
|
||||
final password = prefs.getString(_passwordKey);
|
||||
if (username == null ||
|
||||
username.trim().isEmpty ||
|
||||
password == null ||
|
||||
password.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return StoredCredentials(username: username, password: password);
|
||||
}
|
||||
|
||||
static Future<void> saveBaseUrl(String baseUrl) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_baseUrlKey, normalizeBaseUrl(baseUrl));
|
||||
@@ -39,10 +69,24 @@ class SessionStore {
|
||||
}
|
||||
|
||||
class SessionData {
|
||||
const SessionData({this.token, this.baseUrl = kDefaultBaseUrl});
|
||||
const SessionData({
|
||||
this.token,
|
||||
this.baseUrl = kDefaultBaseUrl,
|
||||
this.username,
|
||||
this.password,
|
||||
});
|
||||
|
||||
final String? token;
|
||||
final String baseUrl;
|
||||
final String? username;
|
||||
final String? password;
|
||||
}
|
||||
|
||||
class StoredCredentials {
|
||||
const StoredCredentials({required this.username, required this.password});
|
||||
|
||||
final String username;
|
||||
final String password;
|
||||
}
|
||||
|
||||
String normalizeBaseUrl(String value) {
|
||||
|
||||
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
|
||||
set(BINARY_NAME "mylis")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.mylis")
|
||||
set(APPLICATION_ID "com.duidev.mylis")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
||||
@@ -395,7 +395,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mylis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mylis";
|
||||
@@ -409,7 +409,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mylis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mylis";
|
||||
@@ -423,7 +423,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mylis.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mylis";
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
PRODUCT_NAME = MyLIS
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.mylis
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.duidev.mylis
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2026 Rumah Sakit Umum Daerah Dr. Saiful Anwar. All rights reserved.
|
||||
Reference in New Issue
Block a user