From 213270ede163bfc75b42bbfa5a6505c5c18473a8 Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Wed, 22 Jul 2026 16:33:42 +0700 Subject: [PATCH] update --- mylis/android/app/build.gradle.kts | 29 +- mylis/android/app/google-services.json | 29 + .../{example => duidev}/mylis/MainActivity.kt | 2 +- .../reports/problems/problems-report.html | 663 ++++++++++++++++++ mylis/ios/Runner.xcodeproj/project.pbxproj | 12 +- mylis/lib/screens/login/login_screen.dart | 14 +- mylis/lib/services/api_client.dart | 121 +++- mylis/lib/services/session_store.dart | 46 +- mylis/linux/CMakeLists.txt | 2 +- mylis/macos/Runner.xcodeproj/project.pbxproj | 6 +- mylis/macos/Runner/Configs/AppInfo.xcconfig | 2 +- 11 files changed, 889 insertions(+), 37 deletions(-) create mode 100644 mylis/android/app/google-services.json rename mylis/android/app/src/main/kotlin/com/{example => duidev}/mylis/MainActivity.kt (78%) create mode 100644 mylis/android/build/reports/problems/problems-report.html diff --git a/mylis/android/app/build.gradle.kts b/mylis/android/app/build.gradle.kts index d8cbe967..261b6908 100644 --- a/mylis/android/app/build.gradle.kts +++ b/mylis/android/app/build.gradle.kts @@ -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") } } } diff --git a/mylis/android/app/google-services.json b/mylis/android/app/google-services.json new file mode 100644 index 00000000..2b25f375 --- /dev/null +++ b/mylis/android/app/google-services.json @@ -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" +} \ No newline at end of file diff --git a/mylis/android/app/src/main/kotlin/com/example/mylis/MainActivity.kt b/mylis/android/app/src/main/kotlin/com/duidev/mylis/MainActivity.kt similarity index 78% rename from mylis/android/app/src/main/kotlin/com/example/mylis/MainActivity.kt rename to mylis/android/app/src/main/kotlin/com/duidev/mylis/MainActivity.kt index 248b3fcb..067728bf 100644 --- a/mylis/android/app/src/main/kotlin/com/example/mylis/MainActivity.kt +++ b/mylis/android/app/src/main/kotlin/com/duidev/mylis/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.mylis +package com.duidev.mylis import io.flutter.embedding.android.FlutterActivity diff --git a/mylis/android/build/reports/problems/problems-report.html b/mylis/android/build/reports/problems/problems-report.html new file mode 100644 index 00000000..8cbf7496 --- /dev/null +++ b/mylis/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/mylis/ios/Runner.xcodeproj/project.pbxproj b/mylis/ios/Runner.xcodeproj/project.pbxproj index 4c9cb0f9..4b82eba1 100644 --- a/mylis/ios/Runner.xcodeproj/project.pbxproj +++ b/mylis/ios/Runner.xcodeproj/project.pbxproj @@ -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; diff --git a/mylis/lib/screens/login/login_screen.dart b/mylis/lib/screens/login/login_screen.dart index d509e30f..32294032 100644 --- a/mylis/lib/screens/login/login_screen.dart +++ b/mylis/lib/screens/login/login_screen.dart @@ -20,11 +20,13 @@ class _LoginScreenState extends State { @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 { 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 { 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; } diff --git a/mylis/lib/services/api_client.dart b/mylis/lib/services/api_client.dart index 0bb3b8ed..47d0d9b6 100644 --- a/mylis/lib/services/api_client.dart +++ b/mylis/lib/services/api_client.dart @@ -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? query]) { final normalizedBase = normalizeBaseUrl(baseUrl); @@ -25,12 +25,55 @@ class ApiClient { String path, Map 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> get( + String path, [ + Map? query, + ]) async { + return _sendWithRenew( + path, + (client, uri) => client + .get(uri, headers: _headers) + .timeout(const Duration(seconds: 15)), + query, + ); + } + + Future> _sendWithRenew( + String path, + Future Function(http.Client client, Uri uri) send, [ + Map? 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> _sendOnce( + Uri uri, + Future 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> get( - String path, [ - Map? 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 _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; } diff --git a/mylis/lib/services/session_store.dart b/mylis/lib/services/session_store.dart index 2e77a9a9..77aff43a 100644 --- a/mylis/lib/services/session_store.dart +++ b/mylis/lib/services/session_store.dart @@ -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 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 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 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 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) { diff --git a/mylis/linux/CMakeLists.txt b/mylis/linux/CMakeLists.txt index 7e77cd8a..addb4ab8 100644 --- a/mylis/linux/CMakeLists.txt +++ b/mylis/linux/CMakeLists.txt @@ -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. diff --git a/mylis/macos/Runner.xcodeproj/project.pbxproj b/mylis/macos/Runner.xcodeproj/project.pbxproj index 305fb03f..385e7434 100644 --- a/mylis/macos/Runner.xcodeproj/project.pbxproj +++ b/mylis/macos/Runner.xcodeproj/project.pbxproj @@ -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"; diff --git a/mylis/macos/Runner/Configs/AppInfo.xcconfig b/mylis/macos/Runner/Configs/AppInfo.xcconfig index 904a3698..640795ed 100644 --- a/mylis/macos/Runner/Configs/AppInfo.xcconfig +++ b/mylis/macos/Runner/Configs/AppInfo.xcconfig @@ -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.