From 1ab5ba2e6f2ecb38e1ebe1af662d9ad7752009f3 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 28 Feb 2026 22:32:54 -0500 Subject: [PATCH] feat: Implement Android keystore setup for secure signing in release builds and update documentation for local and CI/CD environments --- .gitignore | 2 +- apps/mobile/README.md | 57 ++++++++++++++++++- apps/mobile/apps/client/android/.gitignore | 3 +- .../apps/client/android/app/build.gradle.kts | 30 +++++++++- .../client/android/app/google-services.json | 8 +-- .../mobile/apps/client/android/key.properties | 9 +++ apps/mobile/apps/staff/android/.gitignore | 3 +- .../apps/staff/android/app/build.gradle.kts | 30 +++++++++- .../staff/android/app/google-services.json | 10 ++-- apps/mobile/apps/staff/android/key.properties | 9 +++ .../auth_repository_impl.dart | 1 - .../presentation/pages/tax_forms_page.dart | 41 ++----------- .../pages/attire_capture_page.dart | 7 +-- codemagic.yaml | 40 +++++++++++++ 14 files changed, 190 insertions(+), 60 deletions(-) create mode 100644 apps/mobile/apps/client/android/key.properties create mode 100644 apps/mobile/apps/staff/android/key.properties diff --git a/.gitignore b/.gitignore index e91fb146..53393800 100644 --- a/.gitignore +++ b/.gitignore @@ -119,7 +119,6 @@ vite.config.ts.timestamp-* # Android .gradle/ **/android/app/libs/ -**/android/key.properties **/android/local.properties # Build outputs @@ -193,3 +192,4 @@ AGENTS.md CLAUDE.md GEMINI.md TASKS.md +\n# Android Signing (Secure)\n**.jks\n**key.properties diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 2b6c2076..6f7afc3b 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -26,7 +26,60 @@ The project is organized into modular packages to ensure separation of concerns ### 1. Prerequisites Ensure you have the Flutter SDK installed and configured. -### 2. Initial Setup +### 2. Android Keystore Setup (Required for Release Builds) + +To build release APKs/AABs for Android, you need the signing keystores. The keystore configuration (`key.properties`) is committed to the repository, but the actual keystore files are **not** for security reasons. + +#### For Local Development (First-time Setup) + +Contact your team lead to obtain the keystore files: +- `krow_with_us_client_dev.jks` - Client app signing keystore +- `krow_with_us_staff_dev.jks` - Staff app signing keystore + +Once you have the keystores, copy them to the respective app directories: + +```bash +# Copy keystores to their locations +cp krow_with_us_client_dev.jks apps/mobile/apps/client/android/app/ +cp krow_with_us_staff_dev.jks apps/mobile/apps/staff/android/app/ +``` + +The `key.properties` configuration files are already in the repository: +- `apps/mobile/apps/client/android/key.properties` +- `apps/mobile/apps/staff/android/key.properties` + +No manual property file creation is needed — just place the `.jks` files in the correct locations. + +#### For CI/CD (CodeMagic) + +CodeMagic uses a native keystore management system. Follow these steps: + +**Step 1: Upload Keystores to CodeMagic** +1. Go to **CodeMagic Team Settings** → **Code signing identities** → **Android keystores** +2. Upload the keystore files with these **Reference names** (important!): + - `krow_client_dev` (for dev builds) + - `krow_client_staging` (for staging builds) + - `krow_client_prod` (for production builds) + - `krow_staff_dev` (for dev builds) + - `krow_staff_staging` (for staging builds) + - `krow_staff_prod` (for production builds) +3. When uploading, enter the keystore password, key alias, and key password for each keystore + +**Step 2: Automatic Environment Variables** +CodeMagic automatically injects the following environment variables based on the keystore reference: +- `CM_KEYSTORE_PATH_CLIENT` / `CM_KEYSTORE_PATH_STAFF` - Path to the keystore file +- `CM_KEYSTORE_PASSWORD_CLIENT` / `CM_KEYSTORE_PASSWORD_STAFF` - Keystore password +- `CM_KEY_ALIAS_CLIENT` / `CM_KEY_ALIAS_STAFF` - Key alias +- `CM_KEY_PASSWORD_CLIENT` / `CM_KEY_PASSWORD_STAFF` - Key password + +**Step 3: Build Configuration** +The `build.gradle.kts` files are already configured to: +- Use CodeMagic environment variables when running in CI (`CI=true`) +- Fall back to `key.properties` for local development + +Reference: [CodeMagic Android Signing Documentation](https://docs.codemagic.io/yaml-code-signing/signing-android/) + +### 3. Initial Setup Run the following command from the **project root** to install Melos, bootstrap all packages, generate localization files, and generate the Firebase Data Connect SDK: ```bash @@ -42,7 +95,7 @@ This command will: **Note:** The Firebase Data Connect SDK files (`dataconnect_generated/`) are auto-generated and not committed to the repository. They will be regenerated automatically when you run `make mobile-install` or any mobile development commands. -### 3. Running the Apps +### 4. Running the Apps You can run the applications using Melos scripts or through the `Makefile`: First, find your device ID: diff --git a/apps/mobile/apps/client/android/.gitignore b/apps/mobile/apps/client/android/.gitignore index be3943c9..5064d8ff 100644 --- a/apps/mobile/apps/client/android/.gitignore +++ b/apps/mobile/apps/client/android/.gitignore @@ -7,8 +7,7 @@ gradle-wrapper.jar GeneratedPluginRegistrant.java .cxx/ -# Remember to never publicly share your keystore. +# Remember to never publicly share your keystore files. # See https://flutter.dev/to/reference-keystore -key.properties **/*.keystore **/*.jks diff --git a/apps/mobile/apps/client/android/app/build.gradle.kts b/apps/mobile/apps/client/android/app/build.gradle.kts index 593af2c7..f169e26c 100644 --- a/apps/mobile/apps/client/android/app/build.gradle.kts +++ b/apps/mobile/apps/client/android/app/build.gradle.kts @@ -1,4 +1,5 @@ import java.util.Base64 +import java.util.Properties plugins { id("com.android.application") @@ -20,6 +21,13 @@ dartDefinesString.split(",").forEach { } } +val keystoreProperties = Properties().apply { + val propertiesFile = rootProject.file("key.properties") + if (propertiesFile.exists()) { + load(propertiesFile.inputStream()) + } +} + android { namespace = "com.krowwithus.client" compileSdk = flutter.compileSdkVersion @@ -44,14 +52,32 @@ android { versionCode = flutter.versionCode versionName = flutter.versionName - manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: "" + manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: "" + } + + signingConfigs { + create("release") { + if (System.getenv()["CI"] == "true") { + // CodeMagic CI environment + storeFile = file(System.getenv()["CM_KEYSTORE_PATH_CLIENT"] ?: "") + storePassword = System.getenv()["CM_KEYSTORE_PASSWORD_CLIENT"] + keyAlias = System.getenv()["CM_KEY_ALIAS_CLIENT"] + keyPassword = System.getenv()["CM_KEY_PASSWORD_CLIENT"] + } else { + // Local development environment + keyAlias = keystoreProperties["keyAlias"] as String? + keyPassword = keystoreProperties["keyPassword"] as String? + storeFile = keystoreProperties["storeFile"]?.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/apps/mobile/apps/client/android/app/google-services.json b/apps/mobile/apps/client/android/app/google-services.json index fcd3c0e0..e7c91c27 100644 --- a/apps/mobile/apps/client/android/app/google-services.json +++ b/apps/mobile/apps/client/android/app/google-services.json @@ -86,11 +86,11 @@ }, "oauth_client": [ { - "client_id": "933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com", + "client_id": "933560802882-qbl6keingmd14fepn6qp76agdmbr84fg.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.krowwithus.client", - "certificate_hash": "c3efbe1642239c599c16ad04c7fac340902fe280" + "certificate_hash": "f5491c60ec20eb27bb3ec581352ba653053f3740" } }, { @@ -130,11 +130,11 @@ }, "oauth_client": [ { - "client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com", + "client_id": "933560802882-nh589kkndmur9hgibkgg5g8lhmo7mg3v.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.krowwithus.staff", - "certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d" + "certificate_hash": "a6ef7fe8ade313e69377b178544192d835b29153" } }, { diff --git a/apps/mobile/apps/client/android/key.properties b/apps/mobile/apps/client/android/key.properties new file mode 100644 index 00000000..b07f333c --- /dev/null +++ b/apps/mobile/apps/client/android/key.properties @@ -0,0 +1,9 @@ +storePassword=krowwithus +keyPassword=krowwithus +keyAlias=krow_client_dev +storeFile=app/krow_with_us_client_dev.jks + +### +### Client +### SHA1: F5:49:1C:60:EC:20:EB:27:BB:3E:C5:81:35:2B:A6:53:05:3F:37:40 +### SHA256: 27:88:E4:EB:6C:BF:8E:25:66:37:76:B3:5D:DA:92:8A:CB:1A:6F:24:F3:38:9B:EA:DE:F0:25:62:FD:7A:7E:77 diff --git a/apps/mobile/apps/staff/android/.gitignore b/apps/mobile/apps/staff/android/.gitignore index be3943c9..5064d8ff 100644 --- a/apps/mobile/apps/staff/android/.gitignore +++ b/apps/mobile/apps/staff/android/.gitignore @@ -7,8 +7,7 @@ gradle-wrapper.jar GeneratedPluginRegistrant.java .cxx/ -# Remember to never publicly share your keystore. +# Remember to never publicly share your keystore files. # See https://flutter.dev/to/reference-keystore -key.properties **/*.keystore **/*.jks diff --git a/apps/mobile/apps/staff/android/app/build.gradle.kts b/apps/mobile/apps/staff/android/app/build.gradle.kts index 24b1df50..135ca04e 100644 --- a/apps/mobile/apps/staff/android/app/build.gradle.kts +++ b/apps/mobile/apps/staff/android/app/build.gradle.kts @@ -1,4 +1,5 @@ import java.util.Base64 +import java.util.Properties plugins { id("com.android.application") @@ -20,6 +21,13 @@ dartDefinesString.split(",").forEach { } } +val keystoreProperties = Properties().apply { + val propertiesFile = rootProject.file("key.properties") + if (propertiesFile.exists()) { + load(propertiesFile.inputStream()) + } +} + android { namespace = "com.krowwithus.staff" compileSdk = flutter.compileSdkVersion @@ -44,14 +52,32 @@ android { versionCode = flutter.versionCode versionName = flutter.versionName - manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: "" + manifestPlaceholders["GOOGLE_MAPS_API_KEY"] = dartEnvironmentVariables["GOOGLE_MAPS_API_KEY"] ?: "" + } + + signingConfigs { + create("release") { + if (System.getenv()["CI"] == "true") { + // CodeMagic CI environment + storeFile = file(System.getenv()["CM_KEYSTORE_PATH_STAFF"] ?: "") + storePassword = System.getenv()["CM_KEYSTORE_PASSWORD_STAFF"] + keyAlias = System.getenv()["CM_KEY_ALIAS_STAFF"] + keyPassword = System.getenv()["CM_KEY_PASSWORD_STAFF"] + } else { + // Local development environment + keyAlias = keystoreProperties["keyAlias"] as String? + keyPassword = keystoreProperties["keyPassword"] as String? + storeFile = keystoreProperties["storeFile"]?.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/apps/mobile/apps/staff/android/app/google-services.json b/apps/mobile/apps/staff/android/app/google-services.json index fcd3c0e0..8d5acf3a 100644 --- a/apps/mobile/apps/staff/android/app/google-services.json +++ b/apps/mobile/apps/staff/android/app/google-services.json @@ -86,11 +86,11 @@ }, "oauth_client": [ { - "client_id": "933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com", + "client_id": "933560802882-qbl6keingmd14fepn6qp76agdmbr84fg.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.krowwithus.client", - "certificate_hash": "c3efbe1642239c599c16ad04c7fac340902fe280" + "certificate_hash": "f5491c60ec20eb27bb3ec581352ba653053f3740" } }, { @@ -130,11 +130,11 @@ }, "oauth_client": [ { - "client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com", + "client_id": "933560802882-nh589kkndmur9hgibkgg5g8lhmo7mg3v.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.krowwithus.staff", - "certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d" + "certificate_hash": "a6ef7fe8ade313e69377b178544192d835b29153" } }, { @@ -167,4 +167,4 @@ } ], "configuration_version": "1" -} \ No newline at end of file +} diff --git a/apps/mobile/apps/staff/android/key.properties b/apps/mobile/apps/staff/android/key.properties new file mode 100644 index 00000000..94fa9453 --- /dev/null +++ b/apps/mobile/apps/staff/android/key.properties @@ -0,0 +1,9 @@ +storePassword=krowwithus +keyPassword=krowwithus +keyAlias=krow_staff_dev +storeFile=app/krow_with_us_staff_dev.jks + +### +### Staff +### SHA1: A6:EF:7F:E8:AD:E3:13:E6:93:77:B1:78:54:41:92:D8:35:B2:91:53 +### SHA256: 26:B5:BD:1A:DE:18:92:1F:A3:7B:59:99:5E:4E:D0:BB:DF:93:D6:F6:01:16:04:55:0F:AA:57:55:C1:6B:7D:95 \ No newline at end of file diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index d21ac6ce..511e6f15 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -339,7 +339,6 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { Future signOut() async { try { await _service.auth.signOut(); - dc.ClientSessionStore.instance.clear(); _service.clearCache(); } catch (e) { throw Exception('Error signing out: ${e.toString()}'); diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart index 4ce3382b..2dd39496 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart @@ -3,8 +3,8 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; + import '../blocs/tax_forms/tax_forms_cubit.dart'; import '../blocs/tax_forms/tax_forms_state.dart'; @@ -14,39 +14,10 @@ class TaxFormsPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - backgroundColor: UiColors.primary, - elevation: 0, - leading: IconButton( - icon: const Icon(UiIcons.arrowLeft, color: UiColors.bgPopup), - onPressed: () => Modular.to.popSafe(), - ), - title: Text( - 'Tax Documents', - style: UiTypography.headline3m.textSecondary, - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(24), - child: Padding( - padding: const EdgeInsets.only( - left: UiConstants.space5, - right: UiConstants.space5, - bottom: UiConstants.space5, - ), - child: Row( - children: [ - Expanded( - child: Text( - 'Complete required forms to start working', - style: UiTypography.body3r.copyWith( - color: UiColors.primaryForeground.withValues(alpha: 0.8), - ), - ), - ), - ], - ), - ), - ), + appBar: const UiAppBar( + title: 'Tax Documents', + subtitle: 'Complete required forms to start working', + showBackButton: true, ), body: BlocProvider( create: (BuildContext context) { @@ -84,7 +55,7 @@ class TaxFormsPage extends StatelessWidget { vertical: UiConstants.space6, ), child: Column( - spacing: UiConstants.space6, + spacing: UiConstants.space4, children: [ _buildProgressOverview(state.forms), ...state.forms.map( diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart index 65e61a9f..1f0b60f1 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart @@ -351,14 +351,13 @@ class _FileTypesBanner extends StatelessWidget { vertical: UiConstants.space3, ), decoration: BoxDecoration( - color: UiColors.tagActive, + color: UiColors.primary.withAlpha(20), borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(UiIcons.info, size: 20, color: UiColors.primary), + const Icon(UiIcons.info, size: 20, color: UiColors.primary), const SizedBox(width: UiConstants.space3), Expanded( child: Text(message, style: UiTypography.body2r.textSecondary), diff --git a/codemagic.yaml b/codemagic.yaml index 1c6c846a..d853fbba 100644 --- a/codemagic.yaml +++ b/codemagic.yaml @@ -1,3 +1,7 @@ +# Note: key.properties files are now committed to the repository +# CodeMagic keystores are uploaded via Team Settings > Code signing identities > Android keystores +# The keystores are referenced in each workflow's environment section with custom variable names + # Reusable script for building the Flutter app client-app-android-apk-build-script: &client-app-android-apk-build-script name: 👷🤖 Build Client App APK (Android) @@ -170,6 +174,12 @@ workflows: cocoapods: default groups: - client_app_dev_credentials + android_signing: + - keystore: krow_client_dev + keystore_environment_variable: CM_KEYSTORE_PATH_CLIENT + keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_CLIENT + key_alias_environment_variable: CM_KEY_ALIAS_CLIENT + key_password_environment_variable: CM_KEY_PASSWORD_CLIENT vars: ENV: dev scripts: @@ -185,6 +195,12 @@ workflows: cocoapods: default groups: - client_app_staging_credentials + android_signing: + - keystore: krow_client_staging + keystore_environment_variable: CM_KEYSTORE_PATH_CLIENT + keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_CLIENT + key_alias_environment_variable: CM_KEY_ALIAS_CLIENT + key_password_environment_variable: CM_KEY_PASSWORD_CLIENT vars: ENV: staging scripts: @@ -197,6 +213,12 @@ workflows: environment: groups: - client_app_prod_credentials + android_signing: + - keystore: krow_client_prod + keystore_environment_variable: CM_KEYSTORE_PATH_CLIENT + keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_CLIENT + key_alias_environment_variable: CM_KEY_ALIAS_CLIENT + key_password_environment_variable: CM_KEY_PASSWORD_CLIENT vars: ENV: prod scripts: @@ -254,6 +276,12 @@ workflows: cocoapods: default groups: - staff_app_dev_credentials + android_signing: + - keystore: krow_staff_dev + keystore_environment_variable: CM_KEYSTORE_PATH_STAFF + keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_STAFF + key_alias_environment_variable: CM_KEY_ALIAS_STAFF + key_password_environment_variable: CM_KEY_PASSWORD_STAFF vars: ENV: dev scripts: @@ -269,6 +297,12 @@ workflows: cocoapods: default groups: - staff_app_staging_credentials + android_signing: + - keystore: krow_staff_staging + keystore_environment_variable: CM_KEYSTORE_PATH_STAFF + keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_STAFF + key_alias_environment_variable: CM_KEY_ALIAS_STAFF + key_password_environment_variable: CM_KEY_PASSWORD_STAFF vars: ENV: staging scripts: @@ -284,6 +318,12 @@ workflows: cocoapods: default groups: - staff_app_prod_credentials + android_signing: + - keystore: krow_staff_prod + keystore_environment_variable: CM_KEYSTORE_PATH_STAFF + keystore_password_environment_variable: CM_KEYSTORE_PASSWORD_STAFF + key_alias_environment_variable: CM_KEY_ALIAS_STAFF + key_password_environment_variable: CM_KEY_PASSWORD_STAFF vars: ENV: prod scripts: