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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
3
apps/mobile/apps/client/android/.gitignore
vendored
3
apps/mobile/apps/client/android/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
9
apps/mobile/apps/client/android/key.properties
Normal file
9
apps/mobile/apps/client/android/key.properties
Normal 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
|
||||
3
apps/mobile/apps/staff/android/.gitignore
vendored
3
apps/mobile/apps/staff/android/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
9
apps/mobile/apps/staff/android/key.properties
Normal file
9
apps/mobile/apps/staff/android/key.properties
Normal 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
|
||||
@@ -339,7 +339,6 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _service.auth.signOut();
|
||||
dc.ClientSessionStore.instance.clear();
|
||||
_service.clearCache();
|
||||
} catch (e) {
|
||||
throw Exception('Error signing out: ${e.toString()}');
|
||||
|
||||
@@ -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: <Widget>[
|
||||
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<TaxFormsCubit>(
|
||||
create: (BuildContext context) {
|
||||
@@ -84,7 +55,7 @@ class TaxFormsPage extends StatelessWidget {
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: Column(
|
||||
spacing: UiConstants.space6,
|
||||
spacing: UiConstants.space4,
|
||||
children: <Widget>[
|
||||
_buildProgressOverview(state.forms),
|
||||
...state.forms.map(
|
||||
|
||||
@@ -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: <Widget>[
|
||||
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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user