feat: Implement Android keystore setup for secure signing in release builds and update documentation for local and CI/CD environments

This commit is contained in:
Achintha Isuru
2026-02-28 22:32:54 -05:00
parent 7c701ded5f
commit 1ab5ba2e6f
14 changed files with 190 additions and 60 deletions

2
.gitignore vendored
View File

@@ -119,7 +119,6 @@ vite.config.ts.timestamp-*
# Android # Android
.gradle/ .gradle/
**/android/app/libs/ **/android/app/libs/
**/android/key.properties
**/android/local.properties **/android/local.properties
# Build outputs # Build outputs
@@ -193,3 +192,4 @@ AGENTS.md
CLAUDE.md CLAUDE.md
GEMINI.md GEMINI.md
TASKS.md TASKS.md
\n# Android Signing (Secure)\n**.jks\n**key.properties

View File

@@ -26,7 +26,60 @@ The project is organized into modular packages to ensure separation of concerns
### 1. Prerequisites ### 1. Prerequisites
Ensure you have the Flutter SDK installed and configured. 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: 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 ```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. **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`: You can run the applications using Melos scripts or through the `Makefile`:
First, find your device ID: First, find your device ID:

View File

@@ -7,8 +7,7 @@ gradle-wrapper.jar
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/ .cxx/
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore files.
# See https://flutter.dev/to/reference-keystore # See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks

View File

@@ -1,4 +1,5 @@
import java.util.Base64 import java.util.Base64
import java.util.Properties
plugins { plugins {
id("com.android.application") 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 { android {
namespace = "com.krowwithus.client" namespace = "com.krowwithus.client"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -44,14 +52,32 @@ android {
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName 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 { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("release")
} }
} }
} }

View File

@@ -86,11 +86,11 @@
}, },
"oauth_client": [ "oauth_client": [
{ {
"client_id": "933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com", "client_id": "933560802882-qbl6keingmd14fepn6qp76agdmbr84fg.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "com.krowwithus.client", "package_name": "com.krowwithus.client",
"certificate_hash": "c3efbe1642239c599c16ad04c7fac340902fe280" "certificate_hash": "f5491c60ec20eb27bb3ec581352ba653053f3740"
} }
}, },
{ {
@@ -130,11 +130,11 @@
}, },
"oauth_client": [ "oauth_client": [
{ {
"client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com", "client_id": "933560802882-nh589kkndmur9hgibkgg5g8lhmo7mg3v.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "com.krowwithus.staff", "package_name": "com.krowwithus.staff",
"certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d" "certificate_hash": "a6ef7fe8ade313e69377b178544192d835b29153"
} }
}, },
{ {

View File

@@ -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

View File

@@ -7,8 +7,7 @@ gradle-wrapper.jar
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/ .cxx/
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore files.
# See https://flutter.dev/to/reference-keystore # See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks

View File

@@ -1,4 +1,5 @@
import java.util.Base64 import java.util.Base64
import java.util.Properties
plugins { plugins {
id("com.android.application") 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 { android {
namespace = "com.krowwithus.staff" namespace = "com.krowwithus.staff"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -44,14 +52,32 @@ android {
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName 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 { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works. // Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("release")
} }
} }
} }

View File

@@ -86,11 +86,11 @@
}, },
"oauth_client": [ "oauth_client": [
{ {
"client_id": "933560802882-fbqg2icq24bmci3f84evjrbth5huh87f.apps.googleusercontent.com", "client_id": "933560802882-qbl6keingmd14fepn6qp76agdmbr84fg.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "com.krowwithus.client", "package_name": "com.krowwithus.client",
"certificate_hash": "c3efbe1642239c599c16ad04c7fac340902fe280" "certificate_hash": "f5491c60ec20eb27bb3ec581352ba653053f3740"
} }
}, },
{ {
@@ -130,11 +130,11 @@
}, },
"oauth_client": [ "oauth_client": [
{ {
"client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com", "client_id": "933560802882-nh589kkndmur9hgibkgg5g8lhmo7mg3v.apps.googleusercontent.com",
"client_type": 1, "client_type": 1,
"android_info": { "android_info": {
"package_name": "com.krowwithus.staff", "package_name": "com.krowwithus.staff",
"certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d" "certificate_hash": "a6ef7fe8ade313e69377b178544192d835b29153"
} }
}, },
{ {
@@ -167,4 +167,4 @@
} }
], ],
"configuration_version": "1" "configuration_version": "1"
} }

View File

@@ -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

View File

@@ -339,7 +339,6 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
Future<void> signOut() async { Future<void> signOut() async {
try { try {
await _service.auth.signOut(); await _service.auth.signOut();
dc.ClientSessionStore.instance.clear();
_service.clearCache(); _service.clearCache();
} catch (e) { } catch (e) {
throw Exception('Error signing out: ${e.toString()}'); throw Exception('Error signing out: ${e.toString()}');

View File

@@ -3,8 +3,8 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../blocs/tax_forms/tax_forms_cubit.dart'; import '../blocs/tax_forms/tax_forms_cubit.dart';
import '../blocs/tax_forms/tax_forms_state.dart'; import '../blocs/tax_forms/tax_forms_state.dart';
@@ -14,39 +14,10 @@ class TaxFormsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: const UiAppBar(
backgroundColor: UiColors.primary, title: 'Tax Documents',
elevation: 0, subtitle: 'Complete required forms to start working',
leading: IconButton( showBackButton: true,
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: <Widget>[
Expanded(
child: Text(
'Complete required forms to start working',
style: UiTypography.body3r.copyWith(
color: UiColors.primaryForeground.withValues(alpha: 0.8),
),
),
),
],
),
),
),
), ),
body: BlocProvider<TaxFormsCubit>( body: BlocProvider<TaxFormsCubit>(
create: (BuildContext context) { create: (BuildContext context) {
@@ -84,7 +55,7 @@ class TaxFormsPage extends StatelessWidget {
vertical: UiConstants.space6, vertical: UiConstants.space6,
), ),
child: Column( child: Column(
spacing: UiConstants.space6, spacing: UiConstants.space4,
children: <Widget>[ children: <Widget>[
_buildProgressOverview(state.forms), _buildProgressOverview(state.forms),
...state.forms.map( ...state.forms.map(

View File

@@ -351,14 +351,13 @@ class _FileTypesBanner extends StatelessWidget {
vertical: UiConstants.space3, vertical: UiConstants.space3,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.tagActive, color: UiColors.primary.withAlpha(20),
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.primary.withValues(alpha: 0.3)),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon(UiIcons.info, size: 20, color: UiColors.primary), const Icon(UiIcons.info, size: 20, color: UiColors.primary),
const SizedBox(width: UiConstants.space3), const SizedBox(width: UiConstants.space3),
Expanded( Expanded(
child: Text(message, style: UiTypography.body2r.textSecondary), child: Text(message, style: UiTypography.body2r.textSecondary),

View File

@@ -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 # Reusable script for building the Flutter app
client-app-android-apk-build-script: &client-app-android-apk-build-script client-app-android-apk-build-script: &client-app-android-apk-build-script
name: 👷🤖 Build Client App APK (Android) name: 👷🤖 Build Client App APK (Android)
@@ -170,6 +174,12 @@ workflows:
cocoapods: default cocoapods: default
groups: groups:
- client_app_dev_credentials - 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: vars:
ENV: dev ENV: dev
scripts: scripts:
@@ -185,6 +195,12 @@ workflows:
cocoapods: default cocoapods: default
groups: groups:
- client_app_staging_credentials - 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: vars:
ENV: staging ENV: staging
scripts: scripts:
@@ -197,6 +213,12 @@ workflows:
environment: environment:
groups: groups:
- client_app_prod_credentials - 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: vars:
ENV: prod ENV: prod
scripts: scripts:
@@ -254,6 +276,12 @@ workflows:
cocoapods: default cocoapods: default
groups: groups:
- staff_app_dev_credentials - 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: vars:
ENV: dev ENV: dev
scripts: scripts:
@@ -269,6 +297,12 @@ workflows:
cocoapods: default cocoapods: default
groups: groups:
- staff_app_staging_credentials - 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: vars:
ENV: staging ENV: staging
scripts: scripts:
@@ -284,6 +318,12 @@ workflows:
cocoapods: default cocoapods: default
groups: groups:
- staff_app_prod_credentials - 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: vars:
ENV: prod ENV: prod
scripts: scripts: