diff --git a/.github/workflows/build-and-deploy-site.yml b/.github/workflows/build-and-deploy-site.yml
new file mode 100644
index 0000000000..9bba343297
--- /dev/null
+++ b/.github/workflows/build-and-deploy-site.yml
@@ -0,0 +1,65 @@
+# This workflow is designed to automate the process of building and deploying a Kotlin/JS web application to GitHub Pages.
+# It ensures that whenever changes are merged into the dev branch or when manually triggered, the web application is built,
+# packaged, and deployed to the GitHub Pages environment, making it accessible online.
+
+# Key Features:
+# - Automated web application build using Kotlin/JS
+# - Deployment to GitHub Pages
+# - Supports configurable web project module name
+# - Manages deployment concurrency and environment settings
+# - Provides secure deployment with proper permissions
+
+# Prerequisites:
+# - Kotlin Multiplatform/JS project configured with Gradle
+# - Web module set up for browser distribution
+# - Java 17 or compatible version
+# - GitHub Pages enabled in repository settings
+
+# Workflow Configuration:
+# - Requires input of `web_package_name` to specify the web project module
+# - Uses Windows runner for build process
+# - Leverages GitHub Actions for build, pages configuration, and deployment
+
+# Workflow Triggers:
+# - Can be manually called from other workflows
+# - Supports workflow_call for reusability across projects
+
+# Deployment Process:
+# 1. Checkout repository code
+# 2. Set up Java development environment
+# 3. Build Kotlin/JS web application
+# 4. Configure GitHub Pages
+# 5. Upload built artifacts
+# 6. Deploy to GitHub Pages
+
+# https://github.com/openMF/mifos-x-actionhub/blob/main/.github/workflows/build-and-deploy-site.yaml
+
+# ##############################################################################
+# DON'T EDIT THIS FILE UNLESS NECESSARY #
+# ##############################################################################
+
+name: Build And Deploy Web App
+
+# Trigger conditions for the workflow
+on:
+ workflow_dispatch:
+
+# Concurrency settings to manage multiple workflow runs
+# This ensures orderly deployment to production environment
+concurrency:
+ group: "web-pages"
+ cancel-in-progress: false
+
+permissions:
+ contents: read # Read repository contents
+ pages: write # Write to GitHub Pages
+ id-token: write # Write authentication tokens
+ pull-requests: write # Write to pull requests
+
+jobs:
+ build_and_deploy_web:
+ name: Build And Deploy Web App
+ uses: openMF/mifos-x-actionhub/.github/workflows/build-and-deploy-site.yaml@v1.0.8
+ secrets: inherit
+ with:
+ web_package_name: 'cmp-web' # <-- Change with your web package name
diff --git a/.github/workflows/cache-cleanup.yaml b/.github/workflows/cache-cleanup.yaml
new file mode 100644
index 0000000000..31178eeba7
--- /dev/null
+++ b/.github/workflows/cache-cleanup.yaml
@@ -0,0 +1,15 @@
+name: Cleanup Cache
+
+on:
+ pull_request:
+ types: [ closed ]
+ workflow_dispatch:
+
+jobs:
+ cleanup:
+ uses: openMF/mifos-x-actionhub/.github/workflows/cache-cleanup.yaml@v1.0.8
+ with:
+ cleanup_pr: ${{ github.event_name == 'pull_request' && github.event.repository.private == true }}
+ cleanup_all: ${{ github.event_name == 'workflow_dispatch' }}
+ secrets:
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/monthly-version-tag.yml b/.github/workflows/monthly-version-tag.yml
new file mode 100644
index 0000000000..6383b0a424
--- /dev/null
+++ b/.github/workflows/monthly-version-tag.yml
@@ -0,0 +1,66 @@
+# Automated Monthly Release Versioning Workflow
+# ============================================
+
+# Purpose:
+# - Automatically create consistent monthly version tags
+# - Implement a calendar-based versioning strategy
+# - Facilitate easy tracking of monthly releases
+
+# Versioning Strategy:
+# - Tag format: YYYY.MM.0 (e.g., 2024.01.0 for January 2024)
+# - First digit: Full year
+# - Second digit: Month (01-12)
+# - Third digit: Patch version (starts at 0, allows for potential updates)
+
+# Key Features:
+# - Runs automatically on the first day of each month at 3:30 AM UTC
+# - Can be manually triggered via workflow_dispatch
+# - Uses GitHub Actions to generate tags programmatically
+# - Provides a predictable and systematic versioning approach
+
+# Prerequisites:
+# - Repository configured with GitHub Actions
+# - Permissions to create tags
+# - Access to actions/checkout and tag creation actions
+
+# Workflow Triggers:
+# - Scheduled monthly run
+# - Manual workflow dispatch
+# - Callable from other workflows
+
+# Actions Used:
+# 1. actions/checkout@v4 - Checks out repository code
+# 2. josStorer/get-current-time - Retrieves current timestamp
+# 3. rickstaa/action-create-tag - Creates Git tags
+
+# Example Generated Tags:
+# - 2024.01.0 (January 2024 initial release)
+# - 2024.02.0 (February 2024 initial release)
+# - 2024.02.1 (Potential patch for February 2024)
+
+# https://github.com/openMF/mifos-x-actionhub/blob/main/.github/workflows/monthly-version-tag.yaml
+
+# ##############################################################################
+# DON'T EDIT THIS FILE UNLESS NECESSARY #
+# ##############################################################################
+
+name: Tag Monthly Release
+
+on:
+ # Allow manual triggering of the workflow
+ workflow_dispatch:
+ # Schedule the workflow to run monthly
+ schedule:
+ # Runs at 03:30 UTC on the first day of every month
+ # Cron syntax: minute hour day-of-month month day-of-week
+ - cron: '30 3 1 * *'
+
+concurrency:
+ group: "monthly-release"
+ cancel-in-progress: false
+
+jobs:
+ monthly_release:
+ name: Tag Monthly Release
+ uses: openMF/mifos-x-actionhub/.github/workflows/monthly-version-tag.yaml@v1.0.8
+ secrets: inherit
diff --git a/.github/workflows/multi-platform-build-and-publish.yml b/.github/workflows/multi-platform-build-and-publish.yml
index 9d256dd3e4..fb96f14f1c 100644
--- a/.github/workflows/multi-platform-build-and-publish.yml
+++ b/.github/workflows/multi-platform-build-and-publish.yml
@@ -71,7 +71,7 @@ on:
target_branch:
type: string
- default: 'development'
+ default: 'dev'
description: 'Target branch for release'
distribute_ios_firebase:
@@ -89,16 +89,6 @@ on:
default: false
description: Distribute iOS App to Appstore
- distribute_macos_testflight:
- type: boolean
- default: false
- description: Distribute macOS App via TestFlight (App Store Connect)
-
- distribute_macos_appstore:
- type: boolean
- default: false
- description: Distribute macOS App to Appstore
-
permissions:
contents: write
id-token: write
@@ -111,32 +101,20 @@ concurrency:
jobs:
multi_platform_build_and_publish:
name: Multi-Platform Build and Publish
- uses: openMF/mifos-x-actionhub/.github/workflows/multi-platform-build-and-publish.yaml@v1.0.7
+ uses: openMF/mifos-x-actionhub/.github/workflows/multi-platform-build-and-publish.yaml@v1.0.8
with:
- java-version: 21
release_type: ${{ inputs.release_type }}
target_branch: ${{ inputs.target_branch }}
android_package_name: 'cmp-android'
ios_package_name: 'cmp-ios'
desktop_package_name: 'cmp-desktop'
web_package_name: 'cmp-web'
- tester_groups: 'mifos-mobile-apps'
- app_identifier: 'org.mifos.mobile'
- git_url: 'git@github.com:openMF/ios-provisioning-profile.git'
- git_branch: 'mifos-mobile'
- match_type: 'adhoc'
- provisioning_profile_name: 'match AdHoc org.mifos.mobile'
- firebase_app_id: '1:728434912738:ios:ee2e0815a6915b351a1dbb'
- metadata_path: './fastlane/metadata/ios'
- use_cocoapods: true # <-- Set to true if using CocoaPods integration for KMP
- shared_module: ':cmp-shared' # <-- Gradle path to your shared KMP module (e.g., :shared)
- cmp_desktop_dir: 'cmp-desktop'
- keychain_name: signing.keychain-db # optional
+ metadata_path: './fastlane/metadata'
+ use_cocoapods: true
+ shared_module: ':cmp-shared'
distribute_ios_firebase: ${{ inputs.distribute_ios_firebase }}
distribute_ios_testflight: ${{ inputs.distribute_ios_testflight }}
distribute_ios_appstore: ${{ inputs.distribute_ios_appstore }}
- distribute_macos_testflight: ${{ inputs.distribute_macos_testflight }}
- distribute_macos_appstore: ${{ inputs.distribute_macos_appstore }}
secrets:
original_keystore_file: ${{ secrets.ORIGINAL_KEYSTORE_FILE }}
original_keystore_file_password: ${{ secrets.ORIGINAL_KEYSTORE_FILE_PASSWORD }}
@@ -151,12 +129,6 @@ jobs:
notarization_apple_id: ${{ secrets.NOTARIZATION_APPLE_ID }}
notarization_password: ${{ secrets.NOTARIZATION_PASSWORD }}
notarization_team_id: ${{ secrets.NOTARIZATION_TEAM_ID }}
- keychain_password: ${{ secrets.KEYCHAIN_PASSWORD }}
- certificates_password: ${{ secrets.CERTIFICATES_PASSWORD }}
- mac_app_distribution_certificate_b64: ${{ secrets.MAC_APP_DISTRIBUTION_CERTIFICATE_B64 }}
- mac_installer_distribution_certificate_b64: ${{ secrets.MAC_INSTALLER_DISTRIBUTION_CERTIFICATE_B64 }}
- mac_embedded_provision_b64: ${{ secrets.MAC_EMBEDDED_PROVISION_B64 }}
- mac_runtime_provision_b64: ${{ secrets.MAC_RUNTIME_PROVISION_B64 }}
appstore_key_id: ${{ secrets.APPSTORE_KEY_ID }}
appstore_issuer_id: ${{ secrets.APPSTORE_ISSUER_ID }}
appstore_auth_key: ${{ secrets.APPSTORE_AUTH_KEY }}
@@ -178,4 +150,4 @@ jobs:
google_services: ${{ secrets.GOOGLESERVICES }}
firebase_creds: ${{ secrets.FIREBASECREDS }}
playstore_creds: ${{ secrets.PLAYSTORECREDS }}
- token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
index 0452cca62e..89e26a7d26 100644
--- a/.github/workflows/pr-check.yml
+++ b/.github/workflows/pr-check.yml
@@ -13,7 +13,7 @@
### Workflow Jobs
# 1. **Setup**: Prepares the build environment
# - Checks out repository code
-# - Sets up Java (configurable; defaults to 17)
+# - Sets up Java 17
# - Configures Gradle
# - Manages dependency caching
#
@@ -36,7 +36,7 @@
# - Generates platform-specific executables and packages
#
### Prerequisites
-# - Java (configurable; default 17)
+# - Java 17
# - Gradle
# - Configured build scripts for:
# - Android module
@@ -49,16 +49,10 @@
### Configuration Parameters
# The workflow requires two input parameters:
#
-# | Parameter | Description | Type | Required |
-# |------------------------|------------------------------------|--------|-----------|
-# | `android_package_name` | Name of the Android project module | String | Yes |
-# | `desktop_package_name` | Name of the Desktop project module | String | Yes |
-# |`web_package_name` | Name of the Web (Kotlin/JS) project/module | String | No|
-# |`ios_package_name` | Name of the iOS project/module | String | No |
-# |`build_ios` | Build iOS targets as part of PR checks | Boolean | No |
-# |`use_cocoapods` | Use CocoaPods for iOS integration | Boolean | No |
-# |`shared_module | Path of the shared KMP module | String | (required when build_ios=true) |
-# |`java-version | Java version to use (configurable; defaults to 17)| No |
+# | Parameter | Description | Type | Required |
+# |------------------------|------------------------------------|--------|----------|
+# | `android_package_name` | Name of the Android project module | String | Yes |
+# | `desktop_package_name` | Name of the Desktop project module | String | Yes |
#
# https://github.com/openMF/mifos-x-actionhub/blob/main/.github/workflows/pr-check.yaml
@@ -68,18 +62,18 @@
# ##############################################################################
-name: PR Checks for KMP
+name: PR Checks
# Trigger conditions for the workflow
on:
push:
- branches: [ development ] # Runs on pushes to dev branch
+ branches: [ dev ] # Runs on pushes to dev branch
pull_request:
- branches: [ development ] # Runs on pushes to dev branch
+ branches: [ dev ] # Runs on pushes to dev branch
# Concurrency settings to prevent multiple simultaneous workflow runs
concurrency:
- group: pr-kmp-${{ github.ref }}
+ group: pr-${{ github.ref }}
cancel-in-progress: true # Cancels previous runs if a new one is triggered
permissions:
@@ -87,9 +81,8 @@ permissions:
jobs:
pr_checks:
- name: PR Checks KMP
- uses: openMF/mifos-x-actionhub/.github/workflows/pr-check.yaml@v1.0.7
- secrets: inherit
+ name: PR Checks
+ uses: openMF/mifos-x-actionhub/.github/workflows/pr-check.yaml@v1.0.8
with:
android_package_name: 'cmp-android' # <-- Change Your Android Package Name
desktop_package_name: 'cmp-desktop' # <-- Change Your Desktop Package Name
@@ -98,4 +91,3 @@ jobs:
build_ios: true # <-- Change to 'false' if you don't want to build iOS
use_cocoapods: true
shared_module: ':cmp-shared'
- java-version: '21'
diff --git a/.github/workflows/promote-to-production.yml b/.github/workflows/promote-to-production.yml
index 8099c4ffa6..ee0067d6e3 100644
--- a/.github/workflows/promote-to-production.yml
+++ b/.github/workflows/promote-to-production.yml
@@ -43,7 +43,7 @@
# end
# ```
-# https://github.com/openMF/mifos-mobile-github-actions/blob/main/.github/workflows/promote-to-production.yaml
+# https://github.com/openMF/mifos-x-actionhub/blob/main/.github/workflows/promote-to-production.yaml
# ##############################################################################
# DON'T EDIT THIS FILE UNLESS NECESSARY #
@@ -70,6 +70,6 @@ jobs:
# Job to promote app from beta to production in Play Store
play_promote_production:
name: Promote Beta to Production Play Store
- uses: openMF/mifos-x-actionhub/.github/workflows/promote-to-production.yaml@v1.0.7
+ uses: openMF/mifos-x-actionhub/.github/workflows/promote-to-production.yaml@v1.0.8
secrets:
playstore_creds: ${{ secrets.PLAYSTORECREDS }}
diff --git a/.github/workflows/sync-dirs.yaml b/.github/workflows/sync-dirs.yaml
index 6a947f649a..0247382081 100644
--- a/.github/workflows/sync-dirs.yaml
+++ b/.github/workflows/sync-dirs.yaml
@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- ref: development
+ ref: dev
- name: Setup Git config
run: |
@@ -32,12 +32,14 @@ jobs:
- name: Add upstream remote and fetch
run: |
+ set -euo pipefail
UPSTREAM="${{ inputs.upstream || 'https://github.com/openMF/kmp-project-template.git' }}"
git remote add upstream "$UPSTREAM" || true
git fetch upstream || exit 1
- name: Check upstream/dev exists
run: |
+ set -euo pipefail
if ! git rev-parse --verify upstream/dev >/dev/null 2>&1; then
echo "Error: upstream/dev branch does not exist"
exit 1
@@ -45,62 +47,67 @@ jobs:
- name: Create and checkout temporary branch
run: |
+ set -euo pipefail
TEMP_BRANCH="temp-sync-branch-${{ github.run_number }}"
git checkout -b "$TEMP_BRANCH" upstream/dev || exit 1
echo "TEMP_BRANCH=$TEMP_BRANCH" >> $GITHUB_ENV
- name: Sync directories and files
+ shell: bash
run: |
+ set -euo pipefail
+
# Declare directories and files to sync
DIRS=(
- "cmp-android"
"cmp-desktop"
- "cmp-ios"
"cmp-web"
"cmp-shared"
"core-base"
"build-logic"
"fastlane"
+ "fastlane-config"
"scripts"
"config"
".github"
".run"
)
-
+
FILES=(
"Gemfile"
"Gemfile.lock"
"ci-prepush.bat"
"ci-prepush.sh"
)
-
+
# Define exclusions
declare -A EXCLUSIONS=(
- ["cmp-android"]="src/main/res dependencies src/main/ic_launcher-playstore.png google-services.json"
["cmp-web"]="src/jsMain/resources src/wasmJsMain/resources"
- ["cmp-desktop"]="icons"
- ["cmp-ios"]="iosApp/Assets.xcassets"
+ ["cmp-desktop"]="icons build.gradle.kts"
+ ["fastlane-config"]="project_config.rb extract_config.rb"
+ [".github"]="workflows/sync-dirs.yaml"
["root"]="secrets.env"
)
-
+
# Function to check if path should be excluded
should_exclude() {
local dir=$1
local path=$2
-
- # Check for root exclusions
- if [[ "$dir" == "." && -n "${EXCLUSIONS["root"]}" ]]; then
- local root_excluded_paths=(${EXCLUSIONS["root"]})
+
+ # Check for root exclusions (when dir is "." or "root")
+ if [[ "$dir" == "." || "$dir" == "root" ]] && [[ -v "EXCLUSIONS[root]" ]]; then
+ local root_excluded_paths
+ IFS=' ' read -ra root_excluded_paths <<< "${EXCLUSIONS[root]}"
for excluded in "${root_excluded_paths[@]}"; do
if [[ "$path" == *"$excluded"* ]]; then
return 0
fi
done
fi
-
+
# Check directory-specific exclusions
- if [[ -n "${EXCLUSIONS[$dir]}" ]]; then
- local excluded_paths=(${EXCLUSIONS[$dir]})
+ if [[ -v "EXCLUSIONS[$dir]" ]]; then
+ local excluded_paths
+ IFS=' ' read -ra excluded_paths <<< "${EXCLUSIONS[$dir]}"
for excluded in "${excluded_paths[@]}"; do
if [[ "$path" == *"$excluded"* ]]; then
return 0
@@ -109,29 +116,31 @@ jobs:
fi
return 1
}
-
+
# Function to preserve excluded paths
preserve_excluded() {
local dir=$1
- if [[ -n "${EXCLUSIONS[$dir]}" ]]; then
- local excluded_paths=(${EXCLUSIONS[$dir]})
+ if [[ -v "EXCLUSIONS[$dir]" ]]; then
+ local excluded_paths
+ IFS=' ' read -ra excluded_paths <<< "${EXCLUSIONS[$dir]}"
for excluded in "${excluded_paths[@]}"; do
local full_path="$dir/$excluded"
if [[ -e "$full_path" ]]; then
echo "Preserving excluded path: $full_path"
local temp_path="temp_excluded/$full_path"
mkdir -p "$(dirname "$temp_path")"
- cp -r "$full_path" "$(dirname "$temp_path")"
+ cp -r "$full_path" "$(dirname "$temp_path")/"
fi
done
fi
}
-
+
# Function to restore excluded paths
restore_excluded() {
local dir=$1
- if [[ -n "${EXCLUSIONS[$dir]}" ]]; then
- local excluded_paths=(${EXCLUSIONS[$dir]})
+ if [[ -v "EXCLUSIONS[$dir]" ]]; then
+ local excluded_paths
+ IFS=' ' read -ra excluded_paths <<< "${EXCLUSIONS[$dir]}"
for excluded in "${excluded_paths[@]}"; do
local full_path="$dir/$excluded"
local temp_path="temp_excluded/$full_path"
@@ -139,16 +148,17 @@ jobs:
echo "Restoring excluded path: $full_path"
mkdir -p "$(dirname "$full_path")"
rm -rf "$full_path"
- cp -r "$temp_path" "$(dirname "$full_path")"
+ cp -r "$temp_path" "$(dirname "$full_path")/"
fi
done
fi
}
-
+
# Function to preserve root-level excluded files
preserve_root_files() {
- if [[ -n "${EXCLUSIONS["root"]}" ]]; then
- local excluded_paths=(${EXCLUSIONS["root"]})
+ if [[ -v "EXCLUSIONS[root]" ]]; then
+ local excluded_paths
+ IFS=' ' read -ra excluded_paths <<< "${EXCLUSIONS[root]}"
for excluded in "${excluded_paths[@]}"; do
if [[ -e "$excluded" ]]; then
echo "Preserving root-level excluded file: $excluded"
@@ -158,11 +168,12 @@ jobs:
done
fi
}
-
+
# Function to restore root-level excluded files
restore_root_files() {
- if [[ -n "${EXCLUSIONS["root"]}" ]]; then
- local excluded_paths=(${EXCLUSIONS["root"]})
+ if [[ -v "EXCLUSIONS[root]" ]]; then
+ local excluded_paths
+ IFS=' ' read -ra excluded_paths <<< "${EXCLUSIONS[root]}"
for excluded in "${excluded_paths[@]}"; do
if [[ -e "temp_excluded/root/$excluded" ]]; then
echo "Restoring root-level excluded file: $excluded"
@@ -171,63 +182,81 @@ jobs:
done
fi
}
-
+
# Create temp directory for exclusions
mkdir -p temp_excluded
-
+
# Preserve root-level exclusions before sync
preserve_root_files
-
- # Switch to development branch
- git checkout development
-
+
+ # Switch to dev branch
+ git checkout dev
+
# Sync directories
for dir in "${DIRS[@]}"; do
- if [ ! -d "$dir" ]; then
+ if [[ ! -d "$dir" ]]; then
echo "Creating $dir..."
mkdir -p "$dir"
fi
-
+
# Preserve excluded paths before sync
if [[ -d "$dir" ]]; then
preserve_excluded "$dir"
fi
-
+
echo "Syncing $dir..."
- git checkout "${{ env.TEMP_BRANCH }}" -- "$dir" || exit 1
-
+ if ! git checkout "${{ env.TEMP_BRANCH }}" -- "$dir" 2>/dev/null; then
+ echo "Warning: Could not sync directory $dir (may not exist in upstream)"
+ fi
+
# Restore excluded paths after sync
restore_excluded "$dir"
done
-
+
# Sync files
for file in "${FILES[@]}"; do
- dir=$(dirname "$file")
- if ! should_exclude "$dir" "$file"; then
- echo "Syncing $file..."
- git checkout "${{ env.TEMP_BRANCH }}" -- "$file" || true
- else
+ file_dir=$(dirname "$file")
+ file_name=$(basename "$file")
+
+ # Check root exclusions for root-level files
+ if [[ "$file_dir" == "." ]]; then
+ if should_exclude "root" "$file_name"; then
+ echo "Skipping excluded file: $file"
+ continue
+ fi
+ elif should_exclude "$file_dir" "$file"; then
echo "Skipping excluded file: $file"
+ continue
+ fi
+
+ echo "Syncing $file..."
+ if ! git checkout "${{ env.TEMP_BRANCH }}" -- "$file" 2>/dev/null; then
+ echo "Warning: Could not sync file $file (may not exist in upstream)"
fi
done
-
+
# Restore root-level excluded files
restore_root_files
-
+
# Cleanup temp directory
rm -rf temp_excluded
+ echo "Sync completed successfully!"
+
- name: Clean up temporary branch
if: always()
- run: git branch -D "${{ env.TEMP_BRANCH }}" || true
+ run: git branch -D "${{ env.TEMP_BRANCH }}" 2>/dev/null || true
- name: Check for changes
id: check_changes
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
+ echo "Changes detected:"
+ git status --short
else
echo "has_changes=false" >> $GITHUB_OUTPUT
+ echo "No changes detected"
fi
- name: Create Pull Request
@@ -239,35 +268,36 @@ jobs:
title: "chore: Sync directories and files from upstream"
body: |
Automated sync of directories and files from upstream repository.
-
+
Changes included in this sync:
-
- Directories:
- - cmp-android (excluding src/main/res, dependencies, ic_launcher-playstore.png, google-services.json)
- - cmp-desktop (excluding icons)
- - cmp-ios (excluding iosApp/Assets.xcassets)
+
+ **Directories:**
+ - cmp-desktop (excluding icons, build.gradle.kts)
- cmp-web (excluding src/jsMain/resources, src/wasmJsMain/resources)
- cmp-shared
+ - core-base
- build-logic
- fastlane
+ - fastlane-config (excluding project_config.rb, extract_config.rb)
- scripts
- config
- - .github
+ - .github (excluding workflows/sync-dirs.yaml)
- .run
-
- Files:
+
+ **Files:**
- Gemfile
- Gemfile.lock
- ci-prepush.bat
- ci-prepush.sh
-
- Root-level exclusions:
+
+ **Root-level exclusions:**
- secrets.env
-
+
+ ---
Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
branch: sync-dirs-${{ github.run_number }}
delete-branch: true
labels: |
sync
automated pr
- base: development
\ No newline at end of file
+ base: dev
diff --git a/.github/workflows/tag-weekly-release.yml b/.github/workflows/tag-weekly-release.yml
index cd90ab15fd..9607410dbc 100644
--- a/.github/workflows/tag-weekly-release.yml
+++ b/.github/workflows/tag-weekly-release.yml
@@ -1,29 +1,104 @@
+# Weekly Release Tagging and Beta Deployment Workflow
+# ===================================================
+
+# Purpose:
+# - Automate weekly version tagging for consistent software versioning
+# - Trigger automated beta releases across multiple platforms
+# - Maintain a predictable release cycle
+
+# Workflow Overview:
+# - Runs automatically every Sunday at 4:00 AM UTC
+# - Supports manual triggering via workflow_dispatch
+# - Utilizes Gradle Reckon plugin for intelligent versioning
+# - Triggers multi-platform build and publish workflow
+
+# Key Features:
+# - Automatic semantic versioning
+# - Cross-platform release automation
+# - Configurable target branch for releases
+# - Full repository history checkout for accurate versioning
+
+# Versioning Strategy:
+# - Uses Reckon Gradle plugin for semantic versioning
+# - Generates production-ready (final) version tags
+# - Provides consistent and predictable version incrementation
+
+# Release Process:
+# 1. Checkout repository with full commit history
+# 2. Setup Java 17 development environment
+# 3. Create and push new version tag
+# 4. Trigger multi-platform build and publish workflow
+
+# Prerequisites:
+# - Gradle project configured with Reckon plugin
+# - Java 17 development environment
+# - Configured multi-platform build workflow
+# - GitHub Actions permissions for workflow dispatch
+
+# Workflow Inputs:
+# - target_branch: Branch to use for releases (default: 'dev')
+# Allows flexible release targeting across different branches
+
+# Security Considerations:
+# - Uses GitHub's native GITHUB_TOKEN for authentication
+# - Controlled workflow dispatch with specific inputs
+# - Limited to authorized repository members
+
+# Potential Use Cases:
+# - Regular software release cycles
+# - Automated beta testing distributions
+# - Consistent multi-platform deployment
+
+# Workflow Triggers:
+# - Scheduled weekly run (Sunday 4:00 AM UTC)
+# - Manual workflow dispatch
+# - Callable from other workflows
+
+# ##############################################################################
+# DON'T EDIT THIS FILE UNLESS NECESSARY #
+# ##############################################################################
+
name: Tag Weekly Release
on:
+ # Allow manual triggering of the workflow
workflow_dispatch:
+ # Schedule the workflow to run weekly
schedule:
+ # Runs at 04:00 UTC every Sunday
+ # Cron syntax: minute hour day-of-month month day-of-week
- cron: '0 4 * * 0'
+
+concurrency:
+ group: "weekly-release"
+ cancel-in-progress: false
+
jobs:
tag:
name: Tag Weekly Release
runs-on: ubuntu-latest
steps:
+ # Checkout the repository with full history for proper versioning
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - name: Set up JDK 21
+ # Setup Java environment for Gradle operations
+ - name: Set up JDK 17
uses: actions/setup-java@v4.2.2
with:
distribution: 'temurin'
- java-version: '21'
+ java-version: '17'
+ # Create and push a new version tag using Reckon
+ # This uses the 'final' stage for production-ready releases
- name: Tag Weekly Release
env:
- GITHUB_TOKEN: ${{ secrets.TAG_PUSH_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :reckonTagPush -Preckon.stage=final
+ # Trigger the build and publish workflow for beta release
+ # This starts the process of building and deploying the app to various platforms
- name: Trigger Workflow
uses: actions/github-script@v7
with:
@@ -31,9 +106,10 @@ jobs:
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
- workflow_id: 'android-release.yml',
- ref: 'development',
+ workflow_id: 'multi-platform-build-and-publish.yml',
+ ref: 'dev',
inputs: {
"release_type": "beta",
},
- })
\ No newline at end of file
+ })
+
diff --git a/.run/cmp-android.run.xml b/.run/cmp-android.run.xml
new file mode 100644
index 0000000000..3750e7d078
--- /dev/null
+++ b/.run/cmp-android.run.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/cmp-desktop.run.xml b/.run/cmp-desktop.run.xml
index 88186e065a..32bd0b3af3 100644
--- a/.run/cmp-desktop.run.xml
+++ b/.run/cmp-desktop.run.xml
@@ -1,11 +1,6 @@
-
diff --git a/.run/cmp-web-js.run.xml b/.run/cmp-web-js.run.xml
index 89a8b9653b..ae0413e3c7 100644
--- a/.run/cmp-web-js.run.xml
+++ b/.run/cmp-web-js.run.xml
@@ -10,7 +10,7 @@
diff --git a/Gemfile b/Gemfile
index 54cbd00f09..c06ae4a719 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,15 @@
source "https://rubygems.org"
+ruby '3.3.6'
+
+# Add compatibility gems for Ruby 3.3+
+gem "abbrev"
+gem "base64"
+gem "mutex_m"
+gem "bigdecimal"
+
gem "fastlane"
-gem "cocoapods"
+gem "cocoapods", "~> 1.16"
+
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
diff --git a/Gemfile.lock b/Gemfile.lock
index 2d23059c63..a15c9492ac 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,11 +1,9 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.7)
- base64
- nkf
- rexml
- activesupport (7.2.2.1)
+ CFPropertyList (3.0.8)
+ abbrev (0.1.2)
+ activesupport (7.2.3)
base64
benchmark (>= 0.3)
bigdecimal
@@ -17,34 +15,36 @@ GEM
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
+ addressable (2.8.8)
+ public_suffix (>= 2.0.2, < 8.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
- aws-eventstream (1.3.0)
- aws-partitions (1.1048.0)
- aws-sdk-core (3.218.1)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1208.0)
+ aws-sdk-core (3.241.4)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
+ bigdecimal
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.98.0)
- aws-sdk-core (~> 3, >= 3.216.0)
+ logger
+ aws-sdk-kms (1.121.0)
+ aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.180.0)
- aws-sdk-core (~> 3, >= 3.216.0)
+ aws-sdk-s3 (1.212.0)
+ aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
- aws-sigv4 (1.11.0)
+ aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
- benchmark (0.3.0)
- bigdecimal (3.2.2)
+ benchmark (0.5.0)
+ bigdecimal (4.0.1)
claide (1.1.0)
cocoapods (1.16.2)
addressable (~> 2.8)
@@ -87,18 +87,18 @@ GEM
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
- concurrent-ruby (1.3.5)
- connection_pool (2.5.0)
+ concurrent-ruby (1.3.6)
+ connection_pool (3.0.2)
+ csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
- drb (2.2.0)
- ruby2_keywords
+ drb (2.2.3)
emoji_regex (3.2.3)
escape (0.0.4)
- ethon (0.16.0)
+ ethon (0.15.0)
ffi (>= 1.15.0)
excon (0.112.0)
faraday (1.10.4)
@@ -113,14 +113,14 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
- faraday-cookie_jar (0.0.7)
+ faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
- http-cookie (~> 1.0.0)
+ http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
- faraday-em_synchrony (1.0.0)
+ faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
- faraday-multipart (1.1.0)
+ faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
@@ -130,15 +130,19 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
- fastlane (2.226.0)
+ fastlane (2.231.1)
CFPropertyList (>= 2.3, < 4.0.0)
+ abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
- bundler (>= 1.12.0, < 3.0.0)
+ base64 (~> 0.2.0)
+ benchmark (>= 0.1.0)
+ bundler (>= 1.17.3, < 5.0.0)
colored (~> 1.2)
commander (~> 4.6)
+ csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@@ -156,10 +160,14 @@ GEM
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
+ logger (>= 1.6, < 2.0)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
+ mutex_m (~> 0.3.0)
naturally (~> 2.2)
+ nkf (~> 0.2.0)
optparse (>= 0.1.1, < 1.0.0)
+ ostruct (>= 0.1.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
@@ -170,15 +178,16 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
- xcpretty (~> 0.4.0)
+ xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
- fastlane-plugin-firebase_app_distribution (0.10.0)
+ fastlane-plugin-firebase_app_distribution (0.10.1)
google-apis-firebaseappdistribution_v1 (~> 0.3.0)
google-apis-firebaseappdistribution_v1alpha (~> 0.2.0)
fastlane-plugin-increment_build_number (0.0.4)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
- ffi (1.17.1-arm64-darwin)
+ ffi (1.17.3)
+ ffi (1.17.3-arm64-darwin)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
@@ -202,12 +211,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
- google-cloud-core (1.7.1)
+ google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
- google-cloud-errors (1.4.0)
+ google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -225,46 +234,51 @@ GEM
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
- httpclient (2.8.3)
- i18n (1.14.7)
+ httpclient (2.9.0)
+ mutex_m
+ i18n (1.14.8)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
- json (2.9.1)
- jwt (2.10.1)
+ json (2.18.0)
+ jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- minitest (5.20.0)
+ minitest (6.0.1)
+ prism (~> 1.5)
molinillo (0.8.0)
- multi_json (1.15.0)
+ multi_json (1.19.1)
multipart-post (2.4.1)
+ mutex_m (0.3.0)
nanaimo (0.4.0)
nap (1.1.0)
- naturally (2.2.1)
+ naturally (2.3.0)
netrc (0.11.0)
nkf (0.2.0)
- optparse (0.6.0)
+ optparse (0.8.1)
os (1.1.4)
+ ostruct (0.6.3)
plist (3.7.2)
+ prism (1.8.0)
public_suffix (4.0.7)
- rake (13.2.1)
+ rake (13.3.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.4.0)
+ rexml (3.4.4)
rouge (3.28.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
- securerandom (0.3.1)
+ securerandom (0.4.1)
security (0.1.5)
- signet (0.19.0)
+ signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
- jwt (>= 1.5, < 3.0)
+ jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
@@ -278,8 +292,8 @@ GEM
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
- typhoeus (1.4.1)
- ethon (>= 0.9.0)
+ typhoeus (1.5.0)
+ ethon (>= 0.9.0, < 0.16.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
@@ -292,20 +306,27 @@ GEM
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
- xcpretty (0.4.0)
+ xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
- arm64-darwin-24
+ arm64-darwin-25
ruby
DEPENDENCIES
- cocoapods
+ abbrev
+ base64
+ bigdecimal
+ cocoapods (~> 1.16)
fastlane
fastlane-plugin-firebase_app_distribution
fastlane-plugin-increment_build_number
+ mutex_m
+
+RUBY VERSION
+ ruby 3.3.6p108
BUNDLED WITH
- 2.5.18
+ 2.7.2
diff --git a/build-logic/README.md b/build-logic/README.md
index f0ea8ede99..3852831e73 100644
--- a/build-logic/README.md
+++ b/build-logic/README.md
@@ -29,10 +29,10 @@ setup.
Current list of convention plugins:
-- [`mifos.android.application`](convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt),
- [`mifos.android.library`](convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt),
- [`mifos.android.test`](convention/src/main/kotlin/AndroidTestConventionPlugin.kt):
+- [`android.application`](convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt),
+ [`android.library`](convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt),
+ [`android.test`](convention/src/main/kotlin/AndroidTestConventionPlugin.kt):
Configures common Android and Kotlin options.
-- [`mifos.android.application.compose`](convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt),
- [`mifos.android.library.compose`](convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt):
+- [`android.application.compose`](convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt),
+ [`android.library.compose`](convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt):
Configures Jetpack Compose options
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index e0168aa06f..3a68b63bc4 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -1,20 +1,21 @@
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
`kotlin-dsl`
}
-group = "org.mifos.mobile.buildlogic"
+group = "org.convention.buildlogic"
-// Configure the build-logic plugins to target JDK 17
+// Configure the build-logic plugins to target JDK 19
// This matches the JDK used to build the project, and is not related to what is running on device.
java {
- sourceCompatibility = JavaVersion.VERSION_21
- targetCompatibility = JavaVersion.VERSION_21
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
-tasks.withType().configureEach {
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_21.toString()
+
+kotlin {
+ compilerOptions {
+ jvmTarget = JvmTarget.JVM_17
}
}
@@ -24,12 +25,11 @@ dependencies {
compileOnly(libs.compose.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
- compileOnly(libs.room.gradlePlugin)
compileOnly(libs.detekt.gradlePlugin)
compileOnly(libs.ktlint.gradlePlugin)
- compileOnly(libs.spotless.gradlePlugin)
+ compileOnly(libs.spotless.gradle)
implementation(libs.truth)
-
+ compileOnly(libs.androidx.room.gradle.plugin)
compileOnly(libs.firebase.crashlytics.gradlePlugin)
compileOnly(libs.firebase.performance.gradlePlugin)
}
@@ -39,21 +39,30 @@ tasks {
enableStricterValidation = true
failOnWarning = true
}
+
+ // Configure JUnit 5 for testing keystore management functionality
+ test {
+ useJUnitPlatform()
+ testLogging {
+ events("passed", "skipped", "failed")
+ }
+ }
}
gradlePlugin {
plugins {
- register("androidApplication") {
- id = "mifos.android.application"
- implementationClass = "AndroidApplicationConventionPlugin"
- }
+ // Android Plugins
register("androidApplicationCompose") {
- id = "mifos.android.application.compose"
+ id = "org.convention.android.application.compose"
implementationClass = "AndroidApplicationComposeConventionPlugin"
}
+ register("androidApplication") {
+ id = "org.convention.android.application"
+ implementationClass = "AndroidApplicationConventionPlugin"
+ }
register("androidFlavors") {
- id = "mifos.android.application.flavors"
+ id = "org.convention.android.application.flavors"
implementationClass = "AndroidApplicationFlavorsConventionPlugin"
}
@@ -67,59 +76,54 @@ gradlePlugin {
implementationClass = "AndroidLintConventionPlugin"
}
- // This can removed after migration
- register("androidLibrary") {
- id = "mifos.android.library"
- implementationClass = "AndroidLibraryConventionPlugin"
+ // KMP & CMP Plugins
+ register("cmpFeature") {
+ id = "org.convention.cmp.feature"
+ implementationClass = "CMPFeatureConventionPlugin"
}
- register("androidLibraryCompose") {
- id = "mifos.android.library.compose"
- implementationClass = "AndroidLibraryComposeConventionPlugin"
+ register("kmpKoin") {
+ id = "org.convention.kmp.koin"
+ implementationClass = "KMPKoinConventionPlugin"
}
-
- register("androidFeature") {
- id = "mifos.android.feature"
- implementationClass = "AndroidFeatureConventionPlugin"
+ register("kmpLibrary") {
+ id = "org.convention.kmp.library"
+ implementationClass = "KMPLibraryConventionPlugin"
}
- // Room Plugin
- register("kmpRoom") {
- id = "mifos.kmp.room"
- implementationClass = "KMPRoomConventionPlugin"
+ register("kmpCoreBaseLibrary") {
+ id = "org.convention.kmp.core.base.library"
+ implementationClass = "KMPCoreBaseLibraryConventionPlugin"
}
- // Utility Plugins
+ // Static Analysis & Formatting Plugins
register("detekt") {
- id = "mifos.detekt.plugin"
- implementationClass = "MifosDetektConventionPlugin"
+ id = "org.convention.detekt.plugin"
+ implementationClass = "DetektConventionPlugin"
description = "Configures detekt for the project"
}
register("spotless") {
- id = "mifos.spotless.plugin"
- implementationClass = "MifosSpotlessConventionPlugin"
+ id = "org.convention.spotless.plugin"
+ implementationClass = "SpotlessConventionPlugin"
description = "Configures spotless for the project"
}
+ register("ktlint") {
+ id = "org.convention.ktlint.plugin"
+ implementationClass = "KtlintConventionPlugin"
+ description = "Configures kotlinter for the project"
+ }
register("gitHooks") {
- id = "mifos.git.hooks"
- implementationClass = "MifosGitHooksConventionPlugin"
+ id = "org.convention.git.hooks"
+ implementationClass = "GitHooksConventionPlugin"
description = "Installs git hooks for the project"
}
- // KMP & CMP Plugins
- register("cmpFeature") {
- id = "org.convention.cmp.feature"
- implementationClass = "CMPFeatureConventionPlugin"
- }
-
- register("kmpKoin") {
- id = "org.convention.kmp.koin"
- implementationClass = "KMPKoinConventionPlugin"
+ // Room Plugin
+ register("KMPRoom"){
+ id = "mifos.kmp.room"
+ implementationClass = "KMPRoomConventionPlugin"
+ description = "Configures Room for the project"
}
- register("kmpLibrary") {
- id = "org.convention.kmp.library"
- implementationClass = "KMPLibraryConventionPlugin"
- }
}
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
index 2ec83e3836..7b0ab73ef5 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
@@ -1,10 +1,13 @@
import com.android.build.api.dsl.ApplicationExtension
+import org.convention.configureAndroidCompose
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
-import org.mifos.mobile.configureAndroidCompose
+/**
+ * Plugin that applies the Android application and Compose plugins and configures them.
+ */
class AndroidApplicationComposeConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
index 8f19540fad..6aa8c94741 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
@@ -1,37 +1,34 @@
+
import com.android.build.api.dsl.ApplicationExtension
-import com.android.build.api.variant.ApplicationAndroidComponentsExtension
-import com.android.build.gradle.BaseExtension
+import org.convention.configureGradleManagedDevices
+import org.convention.configureKotlinAndroid
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
-import org.gradle.kotlin.dsl.getByType
-import org.mifos.mobile.configureBadgingTasks
-import org.mifos.mobile.configureKotlinAndroid
-import org.mifos.mobile.configurePrintApksTask
+/**
+ * Plugin that applies the Android application plugin and configures it.
+ */
class AndroidApplicationConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
-
apply("com.dropbox.dependency-guard")
- apply("mifos.detekt.plugin")
- apply("mifos.spotless.plugin")
- apply("mifos.git.hooks")
+ apply("org.convention.detekt.plugin")
+ apply("org.convention.spotless.plugin")
+ apply("org.convention.git.hooks")
apply("org.convention.android.application.lint")
apply("org.convention.android.application.firebase")
-
}
extensions.configure {
configureKotlinAndroid(this)
- defaultConfig.targetSdk = 34
- }
- extensions.configure {
- configurePrintApksTask(this)
- configureBadgingTasks(extensions.getByType(), this)
+ defaultConfig.targetSdk = 36
+ @Suppress("UnstableApiUsage")
+ testOptions.animationsDisabled = true
+ configureGradleManagedDevices(this)
}
}
}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
index 8189044849..f9acb889c1 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
@@ -1,13 +1,27 @@
-
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
import com.android.build.api.dsl.ApplicationExtension
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
+import org.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
-import org.mifos.mobile.libs
class AndroidApplicationFirebaseConventionPlugin : Plugin {
override fun apply(target: Project) {
@@ -34,4 +48,4 @@ class AndroidApplicationFirebaseConventionPlugin : Plugin {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
index 3a7394b663..83759a99a4 100644
--- a/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFlavorsConventionPlugin.kt
@@ -2,8 +2,11 @@ import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
-import org.mifos.mobile.configureFlavors
+import org.convention.configureFlavors
+/**
+ * Plugin that applies the Android application flavors plugin and configures it.
+ */
class AndroidApplicationFlavorsConventionPlugin : Plugin {
override fun apply(target: Project) {
with(target) {
diff --git a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
index 09fdb72ccd..884d6f076b 100644
--- a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
@@ -31,4 +46,4 @@ private fun Lint.configure() {
sarifReport = true
checkDependencies = true
disable += "GradleDependency"
-}
\ No newline at end of file
+}
diff --git a/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt
index ce47af4bf4..c2531dce4f 100644
--- a/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/CMPFeatureConventionPlugin.kt
@@ -1,41 +1,59 @@
+import org.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
-import org.mifos.mobile.libs
+/**
+ * Plugin that applies the CMP feature plugin and configures it.
+ * This plugin applies the following plugins:
+ * - org.mifos.kmp.library - Kotlin Multiplatform Library
+ * - org.mifos.kmp.koin - Koin for Kotlin Multiplatform
+ * - org.jetbrains.kotlin.plugin.compose - Kotlin Compose
+ * - org.jetbrains.compose - Compose Multiplatform
+ * - org.mifos.detekt.plugin - Detekt Plugin
+ * - org.mifos.spotless.plugin - Spotless Plugin
+ *
+ */
class CMPFeatureConventionPlugin : Plugin {
-
override fun apply(target: Project) {
with(target) {
- with(pluginManager) {
+ pluginManager.apply {
apply("org.convention.kmp.library")
apply("org.convention.kmp.koin")
apply("org.jetbrains.kotlin.plugin.compose")
apply("org.jetbrains.compose")
+ apply("org.convention.detekt.plugin")
+ apply("org.convention.spotless.plugin")
}
dependencies {
add("commonMainImplementation", project(":core:ui"))
+ add("commonMainImplementation", project(":core-base:ui"))
add("commonMainImplementation", project(":core:designsystem"))
-// add("commonMainImplementation", project(":core:testing"))
+ add("commonMainImplementation", project(":core-base:designsystem"))
add("commonMainImplementation", project(":core:data"))
+ add("commonMainImplementation", project(":core-base:designsystem"))
+ add("commonMainImplementation", project(":core:analytics"))
add("commonMainImplementation", libs.findLibrary("koin.compose").get())
add("commonMainImplementation", libs.findLibrary("koin.compose.viewmodel").get())
add("commonMainImplementation", libs.findLibrary("jb.composeRuntime").get())
- add("commonMainImplementation", libs.findLibrary("jb.lifecycle.compose").get())
add("commonMainImplementation", libs.findLibrary("jb.composeViewmodel").get())
add("commonMainImplementation", libs.findLibrary("jb.lifecycleViewmodel").get())
- add("commonMainImplementation", libs.findLibrary("jb.lifecycleViewmodelSavedState").get())
+ add("commonMainImplementation", libs.findLibrary("jb.lifecycle.compose").get())
+
+ add(
+ "commonMainImplementation",
+ libs.findLibrary("jb.lifecycleViewmodelSavedState").get(),
+ )
add("commonMainImplementation", libs.findLibrary("jb.savedstate").get())
add("commonMainImplementation", libs.findLibrary("jb.bundle").get())
add("commonMainImplementation", libs.findLibrary("jb.composeNavigation").get())
- add("commonMainImplementation", libs.findLibrary("kotlinx.collections.immutable").get())
-
- add("androidMainImplementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
- add("androidMainImplementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
- add("androidMainImplementation", libs.findLibrary("androidx.tracing.ktx").get())
+ add(
+ "commonMainImplementation",
+ libs.findLibrary("kotlinx.collections.immutable").get(),
+ )
add("androidMainImplementation", platform(libs.findLibrary("koin-bom").get()))
add("androidMainImplementation", libs.findLibrary("koin-android").get())
@@ -46,12 +64,7 @@ class CMPFeatureConventionPlugin : Plugin {
add("androidMainImplementation", libs.findLibrary("koin.androidx.compose").get())
add("androidMainImplementation", libs.findLibrary("koin.core.viewmodel").get())
- add("androidTestImplementation", libs.findLibrary("koin.test.junit4").get())
-
- add("androidInstrumentedTestImplementation", libs.findLibrary("androidx.navigation.testing").get())
- add("androidInstrumentedTestImplementation", libs.findLibrary("androidx.compose.ui.test").get())
- add("androidInstrumentedTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get())
}
}
}
-}
\ No newline at end of file
+}
diff --git a/build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt b/build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt
new file mode 100644
index 0000000000..363f9df50a
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/DetektConventionPlugin.kt
@@ -0,0 +1,26 @@
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.convention.configureDetekt
+import org.convention.detektGradle
+
+/**
+ * Plugin that applies the Detekt plugin and configures it.
+ */
+class DetektConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ applyPlugins()
+
+ detektGradle {
+ configureDetekt(this)
+ }
+ }
+ }
+
+ private fun Project.applyPlugins() {
+ pluginManager.apply {
+ apply("io.gitlab.arturbosch.detekt")
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/FieldSkippingClassVisitor.kt b/build-logic/convention/src/main/kotlin/FieldSkippingClassVisitor.kt
index b632c04fb9..8074f34227 100644
--- a/build-logic/convention/src/main/kotlin/FieldSkippingClassVisitor.kt
+++ b/build-logic/convention/src/main/kotlin/FieldSkippingClassVisitor.kt
@@ -31,8 +31,8 @@ class FieldSkippingClassVisitor(
override fun createClassVisitor(
classContext: ClassContext,
- nextClassVisitor: ClassVisitor,
- ): ClassVisitor {
+ nextClassVisitor: org.objectweb.asm.ClassVisitor,
+ ): org.objectweb.asm.ClassVisitor {
return FieldSkippingClassVisitor(
apiVersion = instrumentationContext.apiVersion.get(),
nextClassVisitor = nextClassVisitor,
diff --git a/build-logic/convention/src/main/kotlin/GitHooksConventionPlugin.kt b/build-logic/convention/src/main/kotlin/GitHooksConventionPlugin.kt
new file mode 100644
index 0000000000..226e8d2d26
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/GitHooksConventionPlugin.kt
@@ -0,0 +1,57 @@
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Exec
+import org.gradle.kotlin.dsl.register
+import java.util.Locale
+
+/**
+ * Plugin that installs the pre-commit git hooks from the scripts directory.
+ */
+class GitHooksConventionPlugin : Plugin {
+ override fun apply(project: Project) {
+ // Define a function to check if the OS is Linux or MacOS
+ fun isLinuxOrMacOs(): Boolean {
+ val osName = System.getProperty("os.name").lowercase(Locale.getDefault())
+ return osName.contains("linux") || osName.contains("mac os") || osName.contains("macos")
+ }
+
+ // Define the copyGitHooks task
+ project.tasks.register("copyGitHooks") {
+ description = "Copies the git hooks from /scripts to the .git/hooks folder."
+ from("${project.rootDir}/scripts/") {
+ include("**/*.sh")
+ rename { it.removeSuffix(".sh") }
+ }
+ into("${project.rootDir}/.git/hooks")
+ }
+
+ // Define the installGitHooks task
+ project.tasks.register("installGitHooks") {
+ description = "Installs the pre-commit git hooks from the scripts directory."
+ group = "git hooks"
+ workingDir = project.rootDir
+
+ if (isLinuxOrMacOs()) {
+ commandLine("chmod", "-R", "+x", ".git/hooks/")
+ }else {
+ commandLine("cmd", "/c", "attrib", "-R", "+X", ".git/hooks/*.*")
+ }
+ dependsOn(project.tasks.named("copyGitHooks"))
+
+ doLast {
+ println("Git hooks installed successfully.")
+ }
+ }
+
+ // Configure task dependencies after evaluation
+ project.afterEvaluate {
+ project.tasks.matching {
+ it.name in listOf("preBuild", "build", "assembleDebug", "assembleRelease", "installDebug", "installRelease", "clean")
+ }.configureEach {
+ dependsOn(project.tasks.named("installGitHooks"))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/KMPCoreBaseLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPCoreBaseLibraryConventionPlugin.kt
new file mode 100644
index 0000000000..2ddf1528bf
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/KMPCoreBaseLibraryConventionPlugin.kt
@@ -0,0 +1,49 @@
+
+import com.android.build.gradle.LibraryExtension
+import org.convention.configureFlavors
+import org.convention.configureKotlinAndroid
+import org.convention.configureKotlinMultiplatform
+import org.convention.libs
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+
+/**
+ * Plugin that applies the Android library and Kotlin multiplatform plugins and configures them.
+ */
+class KMPCoreBaseLibraryConventionPlugin: Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("com.android.library")
+ apply("org.jetbrains.kotlin.multiplatform")
+ apply("org.convention.kmp.koin")
+ apply("org.convention.detekt.plugin")
+ apply("org.jetbrains.kotlin.plugin.serialization")
+ apply("org.jetbrains.kotlin.plugin.parcelize")
+ }
+
+ configureKotlinMultiplatform()
+
+ extensions.configure {
+ configureKotlinAndroid(this)
+ defaultConfig.targetSdk = 36
+ configureFlavors(this)
+ // The resource prefix is derived from the module name,
+ // so resources inside ":core:module1" must be prefixed with "core_module1_"
+ resourcePrefix = path
+ .split("""\W""".toRegex())
+ .drop(1).distinct()
+ .joinToString(separator = "_")
+ .lowercase() + "_"
+ }
+
+ dependencies {
+ add("commonMainImplementation", libs.findLibrary("kotlinx.serialization.json").get())
+ add("commonTestImplementation", libs.findLibrary("kotlin.test").get())
+ add("commonTestImplementation", libs.findLibrary("kotlinx.coroutines.test").get())
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/KMPKoinConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPKoinConventionPlugin.kt
index 27c413da7f..39149ccc41 100644
--- a/build-logic/convention/src/main/kotlin/KMPKoinConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KMPKoinConventionPlugin.kt
@@ -1,15 +1,16 @@
-import com.google.devtools.ksp.gradle.KspExtension
+
+import org.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
-import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
-import org.mifos.mobile.libs
+/**
+ * Plugin that applies the Koin plugin and configures it.
+ */
class KMPKoinConventionPlugin : Plugin {
-
override fun apply(target: Project) {
- with(target){
- with(pluginManager){
+ with(target) {
+ with(pluginManager) {
apply("com.google.devtools.ksp")
}
@@ -18,14 +19,9 @@ class KMPKoinConventionPlugin : Plugin {
add("commonMainImplementation", platform(bom))
add("commonMainImplementation", libs.findLibrary("koin.core").get())
add("commonMainImplementation", libs.findLibrary("koin.annotations").get())
- add("kspCommonMainMetadata", libs.findLibrary("koin.ksp.compiler").get())
- add("commonTestImplementation", libs.findLibrary("koin.test").get())
- }
- extensions.configure {
- arg("KOIN_CONFIG_CHECK","true")
+ add("commonTestImplementation", libs.findLibrary("koin.test").get())
}
}
}
-
-}
\ No newline at end of file
+}
diff --git a/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt
index 33aa493e62..8131dead71 100644
--- a/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt
@@ -1,35 +1,38 @@
+
import com.android.build.gradle.LibraryExtension
+import org.convention.configureFlavors
+import org.convention.configureKotlinAndroid
+import org.convention.configureKotlinMultiplatform
+import org.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
-import org.mifos.mobile.configureFlavors
-import org.mifos.mobile.configureKotlinAndroid
-import org.mifos.mobile.configureKotlinMultiplatform
-import org.mifos.mobile.libs
-
-class KMPLibraryConventionPlugin : Plugin {
+/**
+ * Plugin that applies the Android library and Kotlin multiplatform plugins and configures them.
+ */
+class KMPLibraryConventionPlugin: Plugin {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.multiplatform")
apply("org.convention.kmp.koin")
- apply("mifos.detekt.plugin")
- apply("mifos.spotless.plugin")
+ apply("org.convention.detekt.plugin")
+ apply("org.convention.spotless.plugin")
+ apply("org.jetbrains.kotlin.plugin.serialization")
+ apply("org.jetbrains.kotlin.plugin.parcelize")
}
configureKotlinMultiplatform()
extensions.configure {
configureKotlinAndroid(this)
- defaultConfig.targetSdk = 34
+ defaultConfig.targetSdk = 36
configureFlavors(this)
- /**
- * The resource prefix is derived from the module name,
- * so resources inside ":core:module1" must be prefixed with "core_module1_"
- */
+ // The resource prefix is derived from the module name,
+ // so resources inside ":core:module1" must be prefixed with "core_module1_"
resourcePrefix = path
.split("""\W""".toRegex())
.drop(1).distinct()
@@ -38,6 +41,7 @@ class KMPLibraryConventionPlugin : Plugin {
}
dependencies {
+ add("commonMainImplementation", libs.findLibrary("kotlinx.serialization.json").get())
add("commonTestImplementation", libs.findLibrary("kotlin.test").get())
add("commonTestImplementation", libs.findLibrary("kotlinx.coroutines.test").get())
}
diff --git a/build-logic/convention/src/main/kotlin/KMPRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPRoomConventionPlugin.kt
index 6edd9041b4..2a2de3ae99 100644
--- a/build-logic/convention/src/main/kotlin/KMPRoomConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/KMPRoomConventionPlugin.kt
@@ -1,10 +1,10 @@
import androidx.room.gradle.RoomExtension
import com.google.devtools.ksp.gradle.KspExtension
+import org.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
-import org.mifos.mobile.libs
class KMPRoomConventionPlugin : Plugin {
override fun apply(target: Project) {
@@ -24,9 +24,8 @@ class KMPRoomConventionPlugin : Plugin {
}
dependencies {
- "implementation"(libs.findLibrary("androidx.room.runtime").get())
- "implementation"(libs.findLibrary("androidx.room.ktx").get())
// Adding ksp dependencies for multiple platforms
+ "implementation"(libs.findLibrary("androidx.room.ktx").get())
listOf(
"kspDesktop",
"kspAndroid",
@@ -36,8 +35,10 @@ class KMPRoomConventionPlugin : Plugin {
// Add any other platform you may support
).forEach { platform ->
add(platform, libs.findLibrary("androidx.room.compiler").get())
+// Kotlin Extensions and Coroutines support for Room
+// add(platform, libs.findLibrary("androidx.room.ktx").get())
}
}
}
}
-}
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/KtlintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KtlintConventionPlugin.kt
new file mode 100644
index 0000000000..da7e3c06c0
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/KtlintConventionPlugin.kt
@@ -0,0 +1,19 @@
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * Plugin that applies the Ktlint plugin and configures it.
+ */
+class KtlintConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ applyPlugins()
+ }
+ }
+
+ private fun Project.applyPlugins() {
+ pluginManager.apply {
+ apply("org.jlleitschuh.gradle.ktlint")
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/SpotlessConventionPlugin.kt b/build-logic/convention/src/main/kotlin/SpotlessConventionPlugin.kt
new file mode 100644
index 0000000000..771e67a20c
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/SpotlessConventionPlugin.kt
@@ -0,0 +1,25 @@
+import org.convention.configureSpotless
+import org.convention.spotlessGradle
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+/**
+ * Plugin that applies the Spotless plugin and configures it.
+ */
+class SpotlessConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ applyPlugins()
+
+ spotlessGradle {
+ configureSpotless(this)
+ }
+ }
+ }
+
+ private fun Project.applyPlugins() {
+ pluginManager.apply {
+ apply("com.diffplug.spotless")
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/org/convention/AndroidCompose.kt
new file mode 100644
index 0000000000..ed68bb27b6
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/AndroidCompose.kt
@@ -0,0 +1,63 @@
+package org.convention
+
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.Project
+import org.gradle.api.provider.Provider
+import org.gradle.kotlin.dsl.assign
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
+
+/**
+ * Configure Compose-specific options
+ */
+internal fun Project.configureAndroidCompose(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+) {
+ commonExtension.apply {
+ buildFeatures {
+ compose = true
+ }
+
+ dependencies {
+ val bom = libs.findLibrary("androidx-compose-bom").get()
+ add("implementation", platform(bom))
+ add("androidTestImplementation", platform(bom))
+
+ add("implementation", libs.findLibrary("androidx-compose-ui-tooling-preview").get())
+ add("debugImplementation", libs.findLibrary("androidx-compose-ui-tooling").get())
+ }
+
+ testOptions {
+ unitTests {
+ // For Robolectric
+ isIncludeAndroidResources = true
+
+ isReturnDefaultValues = true
+
+ all {
+ it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware"
+ }
+ }
+ }
+
+ }
+
+ extensions.configure {
+ fun Provider.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } }
+ fun Provider<*>.relativeToRootProject(dir: String) = flatMap {
+ rootProject.layout.buildDirectory.dir(projectDir.toRelativeString(rootDir))
+ }.map { it.dir(dir) }
+
+ project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue()
+ .relativeToRootProject("compose-metrics")
+ .let(metricsDestination::set)
+
+ project.providers.gradleProperty("enableComposeCompilerReports").onlyIfTrue()
+ .relativeToRootProject("compose-reports")
+ .let(reportsDestination::set)
+
+ stabilityConfigurationFiles
+ .add(isolated.rootProject.projectDirectory.file("compose_compiler_config.conf"))
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/AndroidInstrumentedTests.kt b/build-logic/convention/src/main/kotlin/org/convention/AndroidInstrumentedTests.kt
new file mode 100644
index 0000000000..e6688d5803
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/AndroidInstrumentedTests.kt
@@ -0,0 +1,19 @@
+package org.convention
+
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import org.gradle.api.Project
+
+/**
+ * Disable unnecessary Android instrumented tests for the [project] if there is no `androidTest` folder.
+ * Otherwise, these projects would be compiled, packaged, installed and ran only to end-up with the following message:
+ *
+ * > Starting 0 tests on AVD
+ *
+ * Note: this could be improved by checking other potential sourceSets based on buildTypes and flavors.
+ */
+internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
+ project: Project,
+) = beforeVariants {
+ it.enableAndroidTest = it.enableAndroidTest
+ && project.projectDir.resolve("src/androidTest").exists()
+}
diff --git a/build-logic/convention/src/main/kotlin/org/convention/AppBuildType.kt b/build-logic/convention/src/main/kotlin/org/convention/AppBuildType.kt
new file mode 100644
index 0000000000..a663be5206
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/AppBuildType.kt
@@ -0,0 +1,9 @@
+package org.convention
+
+/**
+ * This is shared between :app and :benchmarks module to provide configurations type safety.
+ */
+enum class AppBuildType(val applicationIdSuffix: String? = null) {
+ DEBUG(".debug"),
+ RELEASE,
+}
diff --git a/build-logic/convention/src/main/kotlin/org/convention/AppFlavor.kt b/build-logic/convention/src/main/kotlin/org/convention/AppFlavor.kt
new file mode 100644
index 0000000000..095a42f78d
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/AppFlavor.kt
@@ -0,0 +1,48 @@
+package org.convention
+
+import com.android.build.api.dsl.ApplicationExtension
+import com.android.build.api.dsl.ApplicationProductFlavor
+import com.android.build.api.dsl.CommonExtension
+import com.android.build.api.dsl.ProductFlavor
+
+@Suppress("EnumEntryName")
+enum class FlavorDimension {
+ contentType
+}
+
+// The content for the app can either come from local static data which is useful for demo
+// purposes, or from a production backend server which supplies up-to-date, real content.
+// These two product flavors reflect this behaviour.
+@Suppress("EnumEntryName")
+enum class AppFlavor(val dimension: FlavorDimension, val applicationIdSuffix: String? = null) {
+ demo(FlavorDimension.contentType, applicationIdSuffix = ".demo"),
+ prod(FlavorDimension.contentType)
+}
+
+/**
+ * Configure product flavors for the app module
+ * @param commonExtension the common extension for the app module
+ * @param flavorConfigurationBlock the configuration block for each flavor
+ * @see AppFlavor
+ */
+fun configureFlavors(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+ flavorConfigurationBlock: ProductFlavor.(flavor: AppFlavor) -> Unit = {},
+) {
+ commonExtension.apply {
+ flavorDimensions += FlavorDimension.contentType.name
+ productFlavors {
+ AppFlavor.values().forEach {
+ create(it.name) {
+ dimension = it.dimension.name
+ flavorConfigurationBlock(this, it)
+ if (this@apply is ApplicationExtension && this is ApplicationProductFlavor) {
+ if (it.applicationIdSuffix != null) {
+ applicationIdSuffix = it.applicationIdSuffix
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/org/convention/Badging.kt b/build-logic/convention/src/main/kotlin/org/convention/Badging.kt
new file mode 100644
index 0000000000..bc5c5a0a9d
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/Badging.kt
@@ -0,0 +1,156 @@
+package org.convention
+
+import com.android.build.api.artifact.SingleArtifact
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.android.build.gradle.BaseExtension
+import com.android.SdkConstants
+import com.google.common.truth.Truth.assertWithMessage
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.configurationcache.extensions.capitalized
+import org.gradle.kotlin.dsl.register
+import org.gradle.language.base.plugins.LifecycleBasePlugin
+import org.gradle.process.ExecOperations
+import java.io.File
+import java.util.Locale
+import javax.inject.Inject
+
+/**
+ * Generates the badging information of the APK.
+ * This task is cacheable, meaning that if the inputs and outputs have not changed,
+ * the task will be considered up-to-date and will not run.
+ * This task is also incremental, meaning that if the inputs have not changed,
+ *
+ */
+@CacheableTask
+abstract class GenerateBadgingTask : DefaultTask() {
+
+ @get:OutputFile
+ abstract val badging: RegularFileProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val apk: RegularFileProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val aapt2Executable: RegularFileProperty
+
+ @get:Inject
+ abstract val execOperations: ExecOperations
+
+ @TaskAction
+ fun taskAction() {
+ execOperations.exec {
+ commandLine(
+ aapt2Executable.get().asFile.absolutePath,
+ "dump",
+ "badging",
+ apk.get().asFile.absolutePath,
+ )
+ standardOutput = badging.asFile.get().outputStream()
+ }
+ }
+}
+
+@CacheableTask
+abstract class CheckBadgingTask : DefaultTask() {
+
+ // In order for the task to be up-to-date when the inputs have not changed,
+ // the task must declare an output, even if it's not used. Tasks with no
+ // output are always run regardless of whether the inputs changed
+ @get:OutputDirectory
+ abstract val output: DirectoryProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val goldenBadging: RegularFileProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val generatedBadging: RegularFileProperty
+
+ @get:Input
+ abstract val updateBadgingTaskName: Property
+
+ override fun getGroup(): String = LifecycleBasePlugin.VERIFICATION_GROUP
+
+ @TaskAction
+ fun taskAction() {
+ assertWithMessage(
+ "Generated badging is different from golden badging! " +
+ "If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}",
+ )
+ .that(generatedBadging.get().asFile.readText())
+ .isEqualTo(goldenBadging.get().asFile.readText())
+ }
+}
+
+fun Project.configureBadgingTasks(
+ baseExtension: BaseExtension,
+ componentsExtension: ApplicationAndroidComponentsExtension,
+) {
+ // Registers a callback to be called, when a new variant is configured
+ componentsExtension.onVariants { variant ->
+ // Registers a new task to verify the app bundle.
+ val capitalizedVariantName = variant.name.let {
+ if (it.isEmpty()) it else it[0].titlecase(
+ Locale.getDefault(),
+ ) + it.substring(1)
+ }
+ val generateBadgingTaskName = "generate${capitalizedVariantName}Badging"
+ val generateBadging =
+ tasks.register(generateBadgingTaskName) {
+ apk.set(
+ variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE),
+ )
+ aapt2Executable.set(
+ File(
+ baseExtension.sdkDirectory,
+ "${SdkConstants.FD_BUILD_TOOLS}/" +
+ "${baseExtension.buildToolsVersion}/" +
+ SdkConstants.FN_AAPT2,
+ ),
+ )
+
+ badging.set(
+ project.layout.buildDirectory.file(
+ "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt",
+ ),
+ )
+ }
+
+ val updateBadgingTaskName = "update${capitalizedVariantName}Badging"
+ tasks.register(updateBadgingTaskName) {
+ from(generateBadging.get().badging)
+ into(project.layout.projectDirectory)
+ }
+
+ val checkBadgingTaskName = "check${capitalizedVariantName}Badging"
+ tasks.register(checkBadgingTaskName) {
+ goldenBadging.set(
+ project.layout.projectDirectory.file("${variant.name}-badging.txt"),
+ )
+ generatedBadging.set(
+ generateBadging.get().badging,
+ )
+ this.updateBadgingTaskName.set(updateBadgingTaskName)
+
+ output.set(
+ project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName"),
+ )
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/org/convention/Detekt.kt b/build-logic/convention/src/main/kotlin/org/convention/Detekt.kt
new file mode 100644
index 0000000000..0844153b46
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/Detekt.kt
@@ -0,0 +1,43 @@
+package org.convention
+
+import io.gitlab.arturbosch.detekt.Detekt
+import io.gitlab.arturbosch.detekt.extensions.DetektExtension
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.named
+
+/**
+ * Configures the Detekt plugin with the [extension] configuration.
+ * This includes setting the JVM target to 17 and enabling all reports.
+ * Additionally, it adds the `detekt-formatting` and `twitter-detekt-compose` plugins.
+ * @see DetektExtension
+ * @see Detekt
+ */
+internal fun Project.configureDetekt(extension: DetektExtension) = extension.apply {
+ tasks.named("detekt") {
+ mustRunAfter(":cmp-android:dependencyGuard")
+ jvmTarget = "17"
+ source(files(rootDir))
+ include("**/*.kt")
+ exclude("**/*.kts")
+ exclude("**/resources/**")
+ exclude("**/build/**")
+ exclude("**/generated/**")
+ exclude("**/build-logic/**")
+ exclude("**/spotless/**")
+ // TODO:: Remove this exclusion
+ exclude("core-base/designsystem/**")
+ exclude("feature/home/**")
+ reports {
+ xml.required.set(true)
+ html.required.set(true)
+ txt.required.set(true)
+ sarif.required.set(true)
+ md.required.set(true)
+ }
+ }
+ dependencies {
+ "detektPlugins"(libs.findLibrary("detekt-formatting").get())
+ "detektPlugins"(libs.findLibrary("twitter-detekt-compose").get())
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/GradleManagedDevices.kt b/build-logic/convention/src/main/kotlin/org/convention/GradleManagedDevices.kt
new file mode 100644
index 0000000000..202838ec6b
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/GradleManagedDevices.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.convention
+
+import com.android.build.api.dsl.CommonExtension
+import com.android.build.api.dsl.ManagedVirtualDevice
+import org.gradle.kotlin.dsl.get
+import org.gradle.kotlin.dsl.invoke
+
+/**
+ * Configure project for Gradle managed devices
+ */
+internal fun configureGradleManagedDevices(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+) {
+ val pixel4 = DeviceConfig("Pixel 4", 30, "aosp-atd")
+ val pixel6 = DeviceConfig("Pixel 6", 31, "aosp")
+ val pixelC = DeviceConfig("Pixel C", 30, "aosp-atd")
+
+ val localDevices = listOf(pixel4, pixel6, pixelC)
+ val ciDevices = listOf(pixel4, pixelC)
+
+ commonExtension.testOptions {
+ managedDevices {
+ allDevices {
+ localDevices.forEach { deviceConfig ->
+ maybeCreate(deviceConfig.taskName, ManagedVirtualDevice::class.java).apply {
+ device = deviceConfig.device
+ apiLevel = deviceConfig.apiLevel
+ systemImageSource = deviceConfig.systemImageSource
+ }
+ }
+ }
+ groups {
+ maybeCreate("ci").apply {
+ ciDevices.forEach { deviceConfig ->
+ targetDevices.add(allDevices[deviceConfig.taskName])
+ }
+ }
+ }
+ }
+ }
+}
+
+private data class DeviceConfig(
+ val device: String,
+ val apiLevel: Int,
+ val systemImageSource: String,
+) {
+ val taskName = buildString {
+ append(device.lowercase().replace(" ", ""))
+ append("api")
+ append(apiLevel.toString())
+ append(systemImageSource.replace("-", ""))
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/org/convention/HierarchyTemplate.kt b/build-logic/convention/src/main/kotlin/org/convention/HierarchyTemplate.kt
new file mode 100644
index 0000000000..db72fb9476
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/HierarchyTemplate.kt
@@ -0,0 +1,179 @@
+/**
+ * Kotlin Multiplatform project hierarchy template configuration.
+ *
+ * This file defines a structured hierarchy for organizing source sets in Kotlin Multiplatform
+ * projects. It establishes a logical grouping of platform targets that enables efficient code
+ * sharing across platforms with similar characteristics.
+ *
+ * The hierarchy template creates the following logical groupings:
+ * - `common`: Base shared code for all platforms
+ * - `nonAndroid`: Code shared between JVM, JS, and native platforms, excluding Android
+ * - `jsCommon`: Code shared between JavaScript and WebAssembly JavaScript targets
+ * - `nonJsCommon`: Code shared between JVM and native platforms, excluding JS platforms
+ * - `jvmCommon`: Code shared between Android and JVM targets
+ * - `nonJvmCommon`: Code shared between JS and native platforms, excluding JVM platforms
+ * - `native`: Code shared across all native platforms
+ * - `apple`: Code shared across Apple platforms (iOS, macOS)
+ * - `ios`: iOS-specific code
+ * - `macos`: macOS-specific code
+ * - `nonNative`: Code shared between JS and JVM platforms
+ *
+ * This template applies to both main and test source sets, establishing a consistent
+ * structure throughout the project.
+ *
+ * Note: This implementation uses experimental Kotlin Gradle plugin APIs and may be subject
+ * to change in future Kotlin releases.
+ */
+@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
+
+package org.convention
+
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyBuilder
+import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
+
+/**
+ * Defines the hierarchical structure for source set organization.
+ *
+ * This template establishes the relationships between different platform targets,
+ * creating logical groupings based on platform similarities to facilitate code sharing.
+ */
+private val hierarchyTemplate = KotlinHierarchyTemplate {
+ withSourceSetTree(
+ KotlinSourceSetTree.main,
+ KotlinSourceSetTree.test,
+ )
+
+ common {
+ withCompilations { true }
+
+ groupNonAndroid()
+ groupJsCommon()
+ groupNonJsCommon()
+ groupJvmCommon()
+ groupNonJvmCommon()
+ groupNative()
+ groupNonNative()
+ groupJvmJsCommon()
+ groupMobile()
+ }
+}
+
+/**
+ * Creates a group of non-Android platforms (JVM, JS, and native).
+ */
+private fun KotlinHierarchyBuilder.groupNonAndroid() {
+ group("nonAndroid") {
+ withJvm()
+ groupJsCommon()
+ groupNative()
+ }
+}
+
+/**
+ * Creates a group of JavaScript-related platforms (JS and WebAssembly JS).
+ */
+private fun KotlinHierarchyBuilder.groupJsCommon() {
+ group("jsCommon") {
+ withJs()
+ withWasmJs()
+ }
+}
+
+/**
+ * Creates a group of non-JavaScript platforms (JVM-based and native).
+ */
+private fun KotlinHierarchyBuilder.groupNonJsCommon() {
+ group("nonJsCommon") {
+ groupJvmCommon()
+ groupNative()
+ }
+}
+
+/**
+ * Creates a group of JVM-based platforms (Android and JVM).
+ */
+private fun KotlinHierarchyBuilder.groupJvmCommon() {
+ group("jvmCommon") {
+ withAndroidTarget()
+ withJvm()
+ }
+}
+
+/**
+ * Creates a group of non-JVM platforms (JavaScript and native).
+ */
+private fun KotlinHierarchyBuilder.groupNonJvmCommon() {
+ group("nonJvmCommon") {
+ groupJsCommon()
+ groupNative()
+ }
+}
+
+/**
+ * Creates a group of JVM, JS platforms (JavaScript and JVM).
+ */
+private fun KotlinHierarchyBuilder.groupJvmJsCommon() {
+ group("jvmJsCommon") {
+ groupJsCommon()
+ withJvm()
+ }
+}
+
+/**
+ * Creates a hierarchical group of native platforms with subgroups for Apple platforms.
+ */
+private fun KotlinHierarchyBuilder.groupNative() {
+ group("native") {
+ withNative()
+
+ group("apple") {
+ withApple()
+
+ group("ios") {
+ withIos()
+ }
+
+ group("macos") {
+ withMacos()
+ }
+ }
+ }
+}
+
+/**
+ * Creates a group of non-native platforms (JavaScript and JVM-based).
+ */
+private fun KotlinHierarchyBuilder.groupNonNative() {
+ group("nonNative") {
+ groupJsCommon()
+ groupJvmCommon()
+ }
+}
+
+private fun KotlinHierarchyBuilder.groupMobile() {
+ group("mobile") {
+ withAndroidTarget()
+ withApple()
+ }
+}
+
+/**
+ * Applies the predefined hierarchy template to a Kotlin Multiplatform project.
+ *
+ * This extension function should be called within the `kotlin` block of a Multiplatform
+ * project's build script to establish the source set hierarchy defined in this file.
+ *
+ * Example usage:
+ * ```
+ * kotlin {
+ * applyProjectHierarchyTemplate()
+ * // Configure targets...
+ * }
+ * ```
+ */
+fun KotlinMultiplatformExtension.applyProjectHierarchyTemplate() {
+ applyHierarchyTemplate(hierarchyTemplate)
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/Jacoco.kt b/build-logic/convention/src/main/kotlin/org/convention/Jacoco.kt
new file mode 100644
index 0000000000..6f4dbd383f
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/Jacoco.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.convention
+
+import com.android.build.api.artifact.ScopedArtifact
+import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.api.variant.ScopedArtifacts
+import com.android.build.api.variant.SourceDirectories
+import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.file.RegularFile
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.testing.Test
+import org.gradle.kotlin.dsl.assign
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.register
+import org.gradle.kotlin.dsl.withType
+import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
+import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
+import org.gradle.testing.jacoco.tasks.JacocoReport
+import java.util.Locale
+
+private val coverageExclusions = listOf(
+ // Android
+ "**/R.class",
+ "**/R\$*.class",
+ "**/BuildConfig.*",
+ "**/Manifest*.*",
+ "**/*_Hilt*.class",
+ "**/Hilt_*.class",
+)
+
+private fun String.capitalize() = replaceFirstChar {
+ if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
+}
+
+/**
+ * Creates a new task that generates a combined coverage report with data from local and
+ * instrumented tests.
+ *
+ * `create{variant}CombinedCoverageReport`
+ *
+ * Note that coverage data must exist before running the task. This allows us to run device
+ * tests on CI using a different Github Action or an external device farm.
+ */
+internal fun Project.configureJacoco(
+ androidComponentsExtension: AndroidComponentsExtension<*, *, *>,
+) {
+ configure {
+ toolVersion = libs.findVersion("jacoco").get().toString()
+ }
+
+ androidComponentsExtension.onVariants { variant ->
+ val myObjFactory = project.objects
+ val buildDir = layout.buildDirectory.get().asFile
+ val allJars: ListProperty = myObjFactory.listProperty(RegularFile::class.java)
+ val allDirectories: ListProperty =
+ myObjFactory.listProperty(Directory::class.java)
+ val reportTask =
+ tasks.register(
+ "create${variant.name.capitalize()}CombinedCoverageReport",
+ JacocoReport::class,
+ ) {
+
+ classDirectories.setFrom(
+ allJars,
+ allDirectories.map { dirs ->
+ dirs.map { dir ->
+ myObjFactory.fileTree().setDir(dir).exclude(coverageExclusions)
+ }
+ },
+ )
+ reports {
+ xml.required = true
+ html.required = true
+ }
+
+ fun SourceDirectories.Flat?.toFilePaths(): Provider> = this
+ ?.all
+ ?.map { directories -> directories.map { it.asFile.path } }
+ ?: provider { emptyList() }
+ sourceDirectories.setFrom(
+ files(
+ variant.sources.java.toFilePaths(),
+ variant.sources.kotlin.toFilePaths()
+ ),
+ )
+
+ executionData.setFrom(
+ project.fileTree("$buildDir/outputs/unit_test_code_coverage/${variant.name}UnitTest")
+ .matching { include("**/*.exec") },
+
+ project.fileTree("$buildDir/outputs/code_coverage/${variant.name}AndroidTest")
+ .matching { include("**/*.ec") },
+ )
+ }
+
+
+ variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT)
+ .use(reportTask)
+ .toGet(
+ ScopedArtifact.CLASSES,
+ { _ -> allJars },
+ { _ -> allDirectories },
+ )
+ }
+
+ tasks.withType().configureEach {
+ configure {
+ // Required for JaCoCo + Robolectric
+ // https://github.com/robolectric/robolectric/issues/2230
+ isIncludeNoLocationClasses = true
+
+ // Required for JDK 11 with the above
+ // https://github.com/gradle/gradle/issues/5184#issuecomment-391982009
+ excludes = listOf("jdk.internal.*")
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/org/convention/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/convention/KotlinAndroid.kt
new file mode 100644
index 0000000000..fe40731b70
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/KotlinAndroid.kt
@@ -0,0 +1,77 @@
+package org.convention
+
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.JavaVersion
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.kotlin.dsl.assign
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.provideDelegate
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+/**
+ * Configure base Kotlin with Android options
+ */
+internal fun Project.configureKotlinAndroid(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+) {
+ commonExtension.apply {
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 26
+ }
+
+ compileOptions {
+ // Up to Java 11 APIs are available through desugaring
+ // https://developer.android.com/studio/write/java11-minimal-support-table
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ }
+
+ configureKotlin()
+
+ dependencies {
+ add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get())
+ }
+}
+
+/**
+ * Configure base Kotlin options for JVM (non-Android)
+ */
+internal fun Project.configureKotlinJvm() {
+ extensions.configure {
+ // Up to Java 11 APIs are available through desugaring
+ // https://developer.android.com/studio/write/java11-minimal-support-table
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ configureKotlin()
+}
+
+/**
+ * Configure base Kotlin options
+ */
+private fun Project.configureKotlin() {
+ // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947
+ tasks.withType().configureEach {
+ compilerOptions {
+ // Set JVM target to 17
+ jvmTarget = JvmTarget.JVM_17
+ // Treat all Kotlin warnings as errors (disabled by default)
+ // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
+ val warningsAsErrors: String? by project
+ allWarningsAsErrors = warningsAsErrors.toBoolean()
+ freeCompilerArgs.add(
+ // Enable experimental coroutines APIs, including Flow
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/KotlinMultiplatform.kt b/build-logic/convention/src/main/kotlin/org/convention/KotlinMultiplatform.kt
new file mode 100644
index 0000000000..4ff2e48d45
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/KotlinMultiplatform.kt
@@ -0,0 +1,40 @@
+package org.convention
+
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+
+/**
+ * Configure the Kotlin Multiplatform plugin with the default hierarchy template and additional targets.
+ * This includes JVM, Android, iOS, JS and WASM targets.
+ * @see KotlinMultiplatformExtension
+ * @see configure
+ */
+@OptIn(ExperimentalWasmDsl::class, ExperimentalKotlinGradlePluginApi::class)
+internal fun Project.configureKotlinMultiplatform() {
+ extensions.configure {
+ applyProjectHierarchyTemplate()
+
+ jvm("desktop")
+ androidTarget()
+ iosSimulatorArm64()
+ iosX64()
+ iosArm64()
+ js(IR) {
+ this.nodejs()
+ binaries.executable()
+ }
+ wasmJs() {
+ browser()
+ nodejs()
+ }
+
+ compilerOptions {
+ freeCompilerArgs.add("-Xexpect-actual-classes")
+ freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
+ freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/PrintTestApks.kt b/build-logic/convention/src/main/kotlin/org/convention/PrintTestApks.kt
new file mode 100644
index 0000000000..d95410bb87
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/PrintTestApks.kt
@@ -0,0 +1,95 @@
+package org.convention
+
+import com.android.build.api.artifact.SingleArtifact
+import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.api.variant.BuiltArtifactsLoader
+import com.android.build.api.variant.HasAndroidTest
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+import java.io.File
+
+/**
+ * Configures the `printTestApks` task for the [project].
+ * This task will print the location of the androidTest APKs.
+ * This is useful when running tests on a device or emulator.
+ * The task will only be created if there are androidTest sources.
+ * @see PrintApkLocationTask
+ * @see HasAndroidTest
+ */
+internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtension<*, *, *>) {
+ extension.onVariants { variant ->
+ if (variant is HasAndroidTest) {
+ val loader = variant.artifacts.getBuiltArtifactsLoader()
+ val artifact = variant.androidTest?.artifacts?.get(SingleArtifact.APK)
+ val javaSources = variant.androidTest?.sources?.java?.all
+ val kotlinSources = variant.androidTest?.sources?.kotlin?.all
+
+ val testSources = if (javaSources != null && kotlinSources != null) {
+ javaSources.zip(kotlinSources) { javaDirs, kotlinDirs ->
+ javaDirs + kotlinDirs
+ }
+ } else javaSources ?: kotlinSources
+
+ if (artifact != null && testSources != null) {
+ tasks.register(
+ "${variant.name}PrintTestApk",
+ PrintApkLocationTask::class.java,
+ ) {
+ apkFolder.set(artifact)
+ builtArtifactsLoader.set(loader)
+ variantName.set(variant.name)
+ sources.set(testSources)
+ }
+ }
+ }
+ }
+}
+
+@DisableCachingByDefault(because = "Prints output")
+internal abstract class PrintApkLocationTask : DefaultTask() {
+
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputDirectory
+ abstract val apkFolder: DirectoryProperty
+
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputFiles
+ abstract val sources: ListProperty
+
+ @get:Internal
+ abstract val builtArtifactsLoader: Property
+
+ @get:Input
+ abstract val variantName: Property
+
+ @TaskAction
+ fun taskAction() {
+ val hasFiles = sources.orNull?.any { directory ->
+ directory.asFileTree.files.any {
+ it.isFile && "build${File.separator}generated" !in it.parentFile.path
+ }
+ } ?: throw RuntimeException("Cannot check androidTest sources")
+
+ // Don't print APK location if there are no androidTest source files
+ if (!hasFiles) return
+
+ val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
+ ?: throw RuntimeException("Cannot load APKs")
+ if (builtArtifacts.elements.size != 1)
+ throw RuntimeException("Expected one APK !")
+ val apk = File(builtArtifacts.elements.single().outputFile).toPath()
+ println(apk)
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/convention/ProjectExtensions.kt
new file mode 100644
index 0000000000..8b05d6ee0b
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/ProjectExtensions.kt
@@ -0,0 +1,39 @@
+package org.convention
+
+import com.diffplug.gradle.spotless.SpotlessExtension
+import io.gitlab.arturbosch.detekt.extensions.DetektExtension
+import org.gradle.api.Project
+import org.gradle.api.artifacts.VersionCatalog
+import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.getByType
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyBuilder
+
+/**
+ * Get the `libs` version catalog.
+ */
+val Project.libs
+ get(): VersionCatalog = extensions.getByType().named("libs")
+
+/**
+ * Get the dynamic version of the project.
+ */
+val Project.dynamicVersion
+ get() = project.version.toString().split('+')[0]
+
+/**
+ * Configures the `detekt` plugin with the [configure] lambda.
+ */
+inline fun Project.detektGradle(crossinline configure: DetektExtension.() -> Unit) =
+ extensions.configure {
+ configure()
+ }
+
+/**
+ * Configures the `spotless` plugin with the [configure] lambda.
+ */
+inline fun Project.spotlessGradle(crossinline configure: SpotlessExtension.() -> Unit) =
+ extensions.configure {
+ configure()
+ }
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/convention/Spotless.kt b/build-logic/convention/src/main/kotlin/org/convention/Spotless.kt
new file mode 100644
index 0000000000..8a29e1d7fa
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/convention/Spotless.kt
@@ -0,0 +1,36 @@
+package org.convention
+
+import com.diffplug.gradle.spotless.SpotlessExtension
+import org.gradle.api.Project
+
+const val ktlintVersion = "1.0.1"
+
+/**
+ * Configures the Spotless plugin with the [extension] configuration.
+ * This includes setting up the `ktlint` formatter and the license header.
+ * @see SpotlessExtension
+ */
+internal fun Project.configureSpotless(extension: SpotlessExtension) = extension.apply {
+ kotlin {
+ target("**/*.kt")
+ targetExclude("**/build/**/*.kt")
+ ktlint(ktlintVersion).editorConfigOverride(
+ mapOf("android" to "true"),
+ )
+ licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
+ }
+
+ format("kts") {
+ target("**/*.kts")
+ targetExclude("**/build/**/*.kts")
+ // Look for the first line that doesn't have a block comment (assumed to be the license)
+ licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
+ }
+
+ format("xml") {
+ target("**/*.xml")
+ targetExclude("**/build/**/*.xml")
+ // Look for the first XML tag that isn't a comment (
diff --git a/cmp-android/prodRelease-badging.txt b/cmp-android/prodRelease-badging.txt
index 0314444d1f..c3c9483802 100644
--- a/cmp-android/prodRelease-badging.txt
+++ b/cmp-android/prodRelease-badging.txt
@@ -1,123 +1,115 @@
-package: name='org.mifos.mobile' versionCode='1' versionName='2024.12.4-beta.0.4' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
-sdkVersion:'26'
-targetSdkVersion:'34'
+package: name='cmp.android.app' versionCode='1' versionName='2025.7.3-beta.0.0' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
+minSdkVersion:'26'
+targetSdkVersion:'36'
uses-permission: name='android.permission.INTERNET'
-uses-permission: name='android.permission.CAMERA'
-uses-permission: name='android.permission.READ_EXTERNAL_STORAGE' maxSdkVersion='32'
-uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE' maxSdkVersion='32'
-uses-permission: name='android.permission.VIBRATE'
-uses-permission: name='android.permission.FLASHLIGHT'
+uses-permission: name='android.permission.POST_NOTIFICATIONS'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE'
uses-permission: name='android.permission.ACCESS_ADSERVICES_ATTRIBUTION'
uses-permission: name='android.permission.ACCESS_ADSERVICES_AD_ID'
-uses-permission: name='org.mifos.mobile.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'
-application-label:'Mifos Mobile'
-application-label-af:'Mifos Mobile'
-application-label-am:'Mifos Mobile'
-application-label-ar:'Mifos Mobile'
-application-label-as:'Mifos Mobile'
-application-label-az:'Mifos Mobile'
-application-label-be:'Mifos Mobile'
-application-label-bg:'Mifos Mobile'
-application-label-bn:'Mifos Mobile'
-application-label-bs:'Mifos Mobile'
-application-label-ca:'Mifos Mobile'
-application-label-cs:'Mifos Mobile'
-application-label-da:'Mifos Mobile'
-application-label-de:'Mifos Mobile'
-application-label-el:'Mifos Mobile'
-application-label-en-AU:'Mifos Mobile'
-application-label-en-CA:'Mifos Mobile'
-application-label-en-GB:'Mifos Mobile'
-application-label-en-IN:'Mifos Mobile'
-application-label-en-XC:'Mifos Mobile'
-application-label-es:'Mifos Mobile'
-application-label-es-US:'Mifos Mobile'
-application-label-et:'Mifos Mobile'
-application-label-eu:'Mifos Mobile'
-application-label-fa:'Mifos Mobile'
-application-label-fi:'Mifos Mobile'
-application-label-fr:'Mifos Mobile'
-application-label-fr-CA:'Mifos Mobile'
-application-label-gl:'Mifos Mobile'
-application-label-gu:'Mifos Mobile'
-application-label-hi:'Mifos Mobile'
-application-label-hr:'Mifos Mobile'
-application-label-hu:'Mifos Mobile'
-application-label-hy:'Mifos Mobile'
-application-label-in:'Mifos Mobile'
-application-label-is:'Mifos Mobile'
-application-label-it:'Mifos Mobile'
-application-label-iw:'Mifos Mobile'
-application-label-ja:'Mifos Mobile'
-application-label-ka:'Mifos Mobile'
-application-label-kk:'Mifos Mobile'
-application-label-km:'Mifos Mobile'
-application-label-kn:'Mifos Mobile'
-application-label-ko:'Mifos Mobile'
-application-label-ky:'Mifos Mobile'
-application-label-lo:'Mifos Mobile'
-application-label-lt:'Mifos Mobile'
-application-label-lv:'Mifos Mobile'
-application-label-mk:'Mifos Mobile'
-application-label-ml:'Mifos Mobile'
-application-label-mn:'Mifos Mobile'
-application-label-mr:'Mifos Mobile'
-application-label-ms:'Mifos Mobile'
-application-label-my:'Mifos Mobile'
-application-label-nb:'Mifos Mobile'
-application-label-ne:'Mifos Mobile'
-application-label-nl:'Mifos Mobile'
-application-label-or:'Mifos Mobile'
-application-label-pa:'Mifos Mobile'
-application-label-pl:'Mifos Mobile'
-application-label-pt:'Mifos Mobile'
-application-label-pt-BR:'Mifos Mobile'
-application-label-pt-PT:'Mifos Mobile'
-application-label-ro:'Mifos Mobile'
-application-label-ru:'Mifos Mobile'
-application-label-si:'Mifos Mobile'
-application-label-sk:'Mifos Mobile'
-application-label-sl:'Mifos Mobile'
-application-label-sq:'Mifos Mobile'
-application-label-sr:'Mifos Mobile'
-application-label-sr-Latn:'Mifos Mobile'
-application-label-sv:'Mifos Mobile'
-application-label-sw:'Mifos Mobile'
-application-label-ta:'Mifos Mobile'
-application-label-te:'Mifos Mobile'
-application-label-th:'Mifos Mobile'
-application-label-tl:'Mifos Mobile'
-application-label-tr:'Mifos Mobile'
-application-label-uk:'Mifos Mobile'
-application-label-ur:'Mifos Mobile'
-application-label-uz:'Mifos Mobile'
-application-label-vi:'Mifos Mobile'
-application-label-zh-CN:'Mifos Mobile'
-application-label-zh-HK:'Mifos Mobile'
-application-label-zh-TW:'Mifos Mobile'
-application-label-zu:'Mifos Mobile'
+uses-permission: name='cmp.android.app.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'
+application-label:'AndroidApp'
+application-label-af:'AndroidApp'
+application-label-am:'AndroidApp'
+application-label-ar:'AndroidApp'
+application-label-as:'AndroidApp'
+application-label-az:'AndroidApp'
+application-label-be:'AndroidApp'
+application-label-bg:'AndroidApp'
+application-label-bn:'AndroidApp'
+application-label-bs:'AndroidApp'
+application-label-ca:'AndroidApp'
+application-label-cs:'AndroidApp'
+application-label-da:'AndroidApp'
+application-label-de:'AndroidApp'
+application-label-el:'AndroidApp'
+application-label-en-AU:'AndroidApp'
+application-label-en-CA:'AndroidApp'
+application-label-en-GB:'AndroidApp'
+application-label-en-IN:'AndroidApp'
+application-label-en-XC:'AndroidApp'
+application-label-es:'AndroidApp'
+application-label-es-US:'AndroidApp'
+application-label-et:'AndroidApp'
+application-label-eu:'AndroidApp'
+application-label-fa:'AndroidApp'
+application-label-fi:'AndroidApp'
+application-label-fr:'AndroidApp'
+application-label-fr-CA:'AndroidApp'
+application-label-gl:'AndroidApp'
+application-label-gu:'AndroidApp'
+application-label-hi:'AndroidApp'
+application-label-hr:'AndroidApp'
+application-label-hu:'AndroidApp'
+application-label-hy:'AndroidApp'
+application-label-in:'AndroidApp'
+application-label-is:'AndroidApp'
+application-label-it:'AndroidApp'
+application-label-iw:'AndroidApp'
+application-label-ja:'AndroidApp'
+application-label-ka:'AndroidApp'
+application-label-kk:'AndroidApp'
+application-label-km:'AndroidApp'
+application-label-kn:'AndroidApp'
+application-label-ko:'AndroidApp'
+application-label-ky:'AndroidApp'
+application-label-lo:'AndroidApp'
+application-label-lt:'AndroidApp'
+application-label-lv:'AndroidApp'
+application-label-mk:'AndroidApp'
+application-label-ml:'AndroidApp'
+application-label-mn:'AndroidApp'
+application-label-mr:'AndroidApp'
+application-label-ms:'AndroidApp'
+application-label-my:'AndroidApp'
+application-label-nb:'AndroidApp'
+application-label-ne:'AndroidApp'
+application-label-nl:'AndroidApp'
+application-label-or:'AndroidApp'
+application-label-pa:'AndroidApp'
+application-label-pl:'AndroidApp'
+application-label-pt:'AndroidApp'
+application-label-pt-BR:'AndroidApp'
+application-label-pt-PT:'AndroidApp'
+application-label-ro:'AndroidApp'
+application-label-ru:'AndroidApp'
+application-label-si:'AndroidApp'
+application-label-sk:'AndroidApp'
+application-label-sl:'AndroidApp'
+application-label-sq:'AndroidApp'
+application-label-sr:'AndroidApp'
+application-label-sr-Latn:'AndroidApp'
+application-label-sv:'AndroidApp'
+application-label-sw:'AndroidApp'
+application-label-ta:'AndroidApp'
+application-label-te:'AndroidApp'
+application-label-th:'AndroidApp'
+application-label-tl:'AndroidApp'
+application-label-tr:'AndroidApp'
+application-label-uk:'AndroidApp'
+application-label-ur:'AndroidApp'
+application-label-uz:'AndroidApp'
+application-label-vi:'AndroidApp'
+application-label-zh-CN:'AndroidApp'
+application-label-zh-HK:'AndroidApp'
+application-label-zh-TW:'AndroidApp'
+application-label-zu:'AndroidApp'
application-icon-160:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-240:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-320:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-480:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml'
application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml'
-application: label='Mifos Mobile' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
-launchable-activity: name='org.mifospay.MainActivity' label='' icon=''
-property: name='android.adservices.AD_SERVICES_CONFIG' resource='res/xml/ga_ad_services_config.xml'
+application: label='AndroidApp' icon='res/mipmap-anydpi-v26/ic_launcher.xml'
+launchable-activity: name='cmp.android.app.MainActivity' label='' icon=''
uses-library-not-required:'androidx.window.extensions'
uses-library-not-required:'androidx.window.sidecar'
uses-library-not-required:'android.ext.adservices'
feature-group: label=''
- uses-feature: name='android.hardware.camera'
- uses-feature: name='android.hardware.camera.autofocus'
+ uses-feature-not-required: name='android.hardware.camera'
uses-feature: name='android.hardware.faketouch'
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
- uses-feature: name='android.hardware.screen.portrait'
- uses-implied-feature: name='android.hardware.screen.portrait' reason='one or more activities have specified a portrait orientation'
main
other-activities
other-receivers
diff --git a/cmp-android/proguard-rules.pro b/cmp-android/proguard-rules.pro
index fe8b865e07..47934ff6be 100644
--- a/cmp-android/proguard-rules.pro
+++ b/cmp-android/proguard-rules.pro
@@ -1,135 +1,5 @@
--ignorewarnings
-
-# Rules for: uCrop - Image Cropping Library for Android
--dontwarn com.yalantis.ucrop**
--dontwarn java.lang.management.ManagementFactory
--keep class com.yalantis.ucrop** { *; }
--keep interface com.yalantis.ucrop** { *; }
-
-# Proguard Kotlin Example https://github.com/Guardsquare/proguard/blob/master/examples/application-kotlin/proguard.pro
-
--keepattributes *Annotation*
-
--keep class kotlin.Metadata { *; }
-
-# Kotlin
-
--keep class kotlin.reflect.jvm.internal.** { *; }
--keep class kotlin.text.RegexOption { *; }
-
--keep class kotlin.** { *; }
--keep class org.jetbrains.skia.** { *; }
--keep class org.jetbrains.skiko.** { *; }
-
--assumenosideeffects public class androidx.compose.runtime.ComposerKt {
- void sourceInformation(androidx.compose.runtime.Composer,java.lang.String);
- void sourceInformationMarkerStart(androidx.compose.runtime.Composer,int,java.lang.String);
- void sourceInformationMarkerEnd(androidx.compose.runtime.Composer);
- boolean isTraceInProgress();
- void traceEventEnd();
-}
-
-# Kotlinx Coroutines Rules
-# https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro
-
--keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
--keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
--keepclassmembers class kotlinx.coroutines.** {
- volatile ;
-}
--keepclassmembers class kotlin.coroutines.SafeContinuation {
- volatile ;
-}
--dontwarn java.lang.instrument.ClassFileTransformer
--dontwarn sun.misc.SignalHandler
--dontwarn java.lang.instrument.Instrumentation
--dontwarn sun.misc.Signal
--dontwarn java.lang.ClassValue
--dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-
-# https://github.com/Kotlin/kotlinx.coroutines/issues/2046
--dontwarn android.annotation.SuppressLint
-
-# https://github.com/JetBrains/compose-jb/issues/2393
--dontnote kotlin.coroutines.jvm.internal.**
--dontnote kotlin.internal.**
--dontnote kotlin.jvm.internal.**
--dontnote kotlin.reflect.**
--dontnote kotlinx.coroutines.debug.internal.**
--dontnote kotlinx.coroutines.internal.**
--keep class kotlin.coroutines.Continuation
--keep class kotlinx.coroutines.CancellableContinuation
--keep class kotlinx.coroutines.channels.Channel
--keep class kotlinx.coroutines.CoroutineDispatcher
--keep class kotlinx.coroutines.CoroutineScope
-# this is a weird one, but breaks build on some combinations of OS and JDK (reproduced on Windows 10 + Corretto 16)
--dontwarn org.graalvm.compiler.core.aarch64.AArch64NodeMatchRules_MatchStatementSet*
-
-### kotlinx.serialization rules
-
-# Keep `Companion` object fields of serializable classes.
-# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
--if @kotlinx.serialization.Serializable class **
--keepclassmembers class <1> {
- static <1>$Companion Companion;
-}
-
-# Keep `serializer()` on companion objects (both default and named) of serializable classes.
--if @kotlinx.serialization.Serializable class ** {
- static **$* *;
-}
--keepclassmembers class <2>$<3> {
- kotlinx.serialization.KSerializer serializer(...);
-}
-
-# Keep `INSTANCE.serializer()` of serializable objects.
--if @kotlinx.serialization.Serializable class ** {
- public static ** INSTANCE;
-}
--keepclassmembers class <1> {
- public static <1> INSTANCE;
- kotlinx.serialization.KSerializer serializer(...);
-}
-
-# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
--keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-
-# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes
-# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900
--dontnote kotlinx.serialization.**
-
-# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
-# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning.
-# However, since in this case they will not be used, we can disable these warnings
--dontwarn kotlinx.serialization.internal.ClassValueReferences
-
-# JSR 305 annotations are for embedding nullability information.
--dontwarn javax.annotation.**
-
-# A resource is loaded with a relative path so the package of this class must be preserved.
--keeppackagenames okhttp3.internal.publicsuffix.*
--adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz
-
-# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
--dontwarn org.codehaus.mojo.animal_sniffer.*
-
# OkHttp platform used only on JVM and when Conscrypt and other security providers are available.
-dontwarn okhttp3.internal.platform.**
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
-
--keep class io.ktor.** { *; }
--keep class kotlinx.serialization.** { *; }
--keep class io.ktor.client.network.sockets.** { *; }
--keep class io.ktor.client.plugins.* { *; }
--keep class io.ktor.util.* { *; }
--keep class io.ktor.utils.io.* { *; }
--keep class java.lang.management.* { *; }
--dontwarn io.ktor.client.network.sockets.SocketTimeoutException
--dontwarn java.lang.management.RuntimeMXBean
-
--keep class org.mifospay.core.network.services.* { *;}
--keep class de.jensklingenberg.ktorfit.converter.** { *; }
--keep class de.jensklingenberg.ktorfit.** { *; }
--keeppackagenames de.jensklingenberg.ktorfit.*
\ No newline at end of file
diff --git a/cmp-android/src/main/AndroidManifest.xml b/cmp-android/src/main/AndroidManifest.xml
index d9da703fa3..0cef8ebdbb 100644
--- a/cmp-android/src/main/AndroidManifest.xml
+++ b/cmp-android/src/main/AndroidManifest.xml
@@ -1,12 +1,12 @@
@@ -16,13 +16,7 @@
android:required="false" />
-
-
-
-
+
-
-
-
-
-
-
-
+
+
-
+
-
+
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/cmp-android/src/main/kotlin/cmp/android/app/AndroidApp.kt b/cmp-android/src/main/kotlin/cmp/android/app/AndroidApp.kt
index c9d1b0167d..17e1f180ba 100644
--- a/cmp-android/src/main/kotlin/cmp/android/app/AndroidApp.kt
+++ b/cmp-android/src/main/kotlin/cmp/android/app/AndroidApp.kt
@@ -1,19 +1,32 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package cmp.android.app
import android.app.Application
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.os.LocaleListCompat
import cmp.shared.utils.initKoin
+import coil3.ImageLoader
+import coil3.PlatformContext
+import coil3.SingletonImageLoader
+import coil3.disk.DiskCache
+import coil3.disk.directory
+import coil3.request.CachePolicy
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
-import org.koin.core.logger.Level
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import org.mifos.core.data.repository.UserDataRepository
+import template.core.base.ui.getDefaultImageLoader
/**
* Android application class.
@@ -23,12 +36,59 @@ import org.koin.core.logger.Level
* @constructor Create empty Android app
* @see Application
*/
-class AndroidApp : Application() {
+class AndroidApp : Application(), SingletonImageLoader.Factory, KoinComponent {
+
+ private val userDataRepository: UserDataRepository by inject()
+
override fun onCreate() {
super.onCreate()
initKoin {
- androidContext(this@AndroidApp) // Provides the Android app context
- androidLogger(Level.DEBUG) // Enables Koin's logging for debugging
+ androidContext(this@AndroidApp)
+ androidLogger()
}
+
+ // Restore the user's saved language preference to AppCompatDelegate.
+ // This ensures the app always launches with the user's chosen language,
+ // regardless of system settings or device language.
+ restoreSavedLanguage()
}
+
+ /**
+ * Restores the user's saved language preference from the repository to AppCompatDelegate.
+ *
+ * This runs BEFORE any Activities are created, ensuring the app launches with the
+ * correct language. The app's saved preference always takes precedence.
+ */
+ private fun restoreSavedLanguage() {
+ runBlocking {
+ val userData = userDataRepository.userData.first()
+ val savedLanguage = userData.appLanguage
+
+ // Convert the saved LanguageConfig to LocaleListCompat
+ val desiredLocales = if (savedLanguage.localeName != null) {
+ LocaleListCompat.forLanguageTags(savedLanguage.localeName)
+ } else {
+ // System default
+ LocaleListCompat.getEmptyLocaleList()
+ }
+
+ // Only update if the current locale differs from saved preference
+ val currentLocales = AppCompatDelegate.getApplicationLocales()
+ if (currentLocales != desiredLocales) {
+ AppCompatDelegate.setApplicationLocales(desiredLocales)
+ }
+ }
+ }
+
+ override fun newImageLoader(context: PlatformContext): ImageLoader =
+ getDefaultImageLoader(context)
+ .newBuilder()
+ .diskCachePolicy(CachePolicy.ENABLED)
+ .diskCache {
+ DiskCache.Builder()
+ .directory(context.cacheDir.resolve("image_cache"))
+ .maxSizePercent(0.25)
+ .build()
+ }
+ .build()
}
diff --git a/cmp-android/src/main/kotlin/cmp/android/app/AppThemeExtensions.kt b/cmp-android/src/main/kotlin/cmp/android/app/AppThemeExtensions.kt
new file mode 100644
index 0000000000..0068bf6df8
--- /dev/null
+++ b/cmp-android/src/main/kotlin/cmp/android/app/AppThemeExtensions.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package cmp.android.app
+
+import org.mifos.core.model.DarkThemeConfig
+
+fun DarkThemeConfig.isDarkMode(
+ isSystemDarkMode: Boolean,
+): Boolean =
+ when (this) {
+ DarkThemeConfig.FOLLOW_SYSTEM -> isSystemDarkMode
+ DarkThemeConfig.DARK -> true
+ DarkThemeConfig.LIGHT -> false
+ }
diff --git a/cmp-android/src/main/kotlin/cmp/android/app/BuildConfigUtils.kt b/cmp-android/src/main/kotlin/cmp/android/app/BuildConfigUtils.kt
new file mode 100644
index 0000000000..a96b9151c4
--- /dev/null
+++ b/cmp-android/src/main/kotlin/cmp/android/app/BuildConfigUtils.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package cmp.android.app
+
+import android.os.Build
+
+/**
+ * A boolean property that indicates whether the current build is a dev build.
+ */
+val isDevBuild: Boolean
+ get() = BuildConfig.BUILD_TYPE == "debug"
+
+/**
+ * A string that represents a displayable app version.
+ */
+val versionData: String
+ get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
+
+/**
+ * A string that represents device data.
+ */
+val deviceData: String get() = "$deviceBrandModel $osInfo $buildInfo"
+
+/**
+ * A string representing the build flavor or blank if it is the standard configuration.
+ */
+private val buildFlavorName: String
+ get() = when (BuildConfig.FLAVOR) {
+ "demo" -> ""
+ else -> "-${BuildConfig.FLAVOR}"
+ }
+
+/**
+ * A string representing the build type.
+ */
+private val buildTypeName: String
+ get() = when (BuildConfig.BUILD_TYPE) {
+ "debug" -> "dev"
+ "release" -> "prod"
+ else -> BuildConfig.BUILD_TYPE
+ }
+
+/**
+ * A string representing the device brand and model.
+ */
+private val deviceBrandModel: String get() = "\uD83D\uDCF1 ${Build.BRAND} ${Build.MODEL}"
+
+/**
+ * A string representing the operating system information.
+ */
+private val osInfo: String get() = "\uD83E\uDD16 ${Build.VERSION.RELEASE}@${Build.VERSION.SDK_INT}"
+
+/**
+ * A string representing the build information.
+ */
+private val buildInfo: String
+ get() = "\uD83D\uDCE6 $buildTypeName" +
+ buildFlavorName.takeUnless { it.isBlank() }?.let { " $it" }.orEmpty()
diff --git a/cmp-android/src/main/kotlin/cmp/android/app/ComponentActivityExtensions.kt b/cmp-android/src/main/kotlin/cmp/android/app/ComponentActivityExtensions.kt
index 17502f58bf..7d4af98bce 100644
--- a/cmp-android/src/main/kotlin/cmp/android/app/ComponentActivityExtensions.kt
+++ b/cmp-android/src/main/kotlin/cmp/android/app/ComponentActivityExtensions.kt
@@ -1,33 +1,36 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package cmp.android.app
+import android.content.res.Configuration
+import android.graphics.Color
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatDelegate
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
+import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import cmp.android.app.util.isSystemInDarkModeFlow
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
-import org.mifos.mobile.core.model.MifosThemeConfig
+import org.mifos.core.model.DarkThemeConfig
@ColorInt
-private val SCRIM_COLOR: Int = Color.Transparent.toArgb()
+private val SCRIM_COLOR: Int = Color.TRANSPARENT
/**
* Helper method to handle edge-to-edge logic for dark mode.
@@ -37,7 +40,7 @@ private val SCRIM_COLOR: Int = Color.Transparent.toArgb()
*/
@Suppress("MaxLineLength")
fun ComponentActivity.setupEdgeToEdge(
- appThemeFlow: Flow,
+ appThemeFlow: Flow,
) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
@@ -45,18 +48,8 @@ fun ComponentActivity.setupEdgeToEdge(
isSystemInDarkModeFlow(),
appThemeFlow,
) { isSystemDarkMode, appTheme ->
-
- val currentNightMode = AppCompatDelegate.getDefaultNightMode()
- if (currentNightMode != appTheme.osValue) {
- AppCompatDelegate.setDefaultNightMode(appTheme.osValue)
- }
-
- when (appTheme.osValue) {
- AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> isSystemDarkMode
- AppCompatDelegate.MODE_NIGHT_YES -> true
- AppCompatDelegate.MODE_NIGHT_NO -> false
- else -> isSystemDarkMode
- }
+ AppCompatDelegate.setDefaultNightMode(appTheme.osValue)
+ appTheme.isDarkMode(isSystemDarkMode = isSystemDarkMode)
}
.distinctUntilChanged()
.collect { isDarkMode ->
@@ -66,10 +59,28 @@ fun ComponentActivity.setupEdgeToEdge(
val style = SystemBarStyle.auto(
darkScrim = SCRIM_COLOR,
lightScrim = SCRIM_COLOR,
- detectDarkMode = { isDarkMode },
+ // Disabling Dark Mode for this app
+ detectDarkMode = { false },
)
enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style)
}
}
}
}
+
+/**
+ * Adds a configuration change listener to retrieve whether system is in
+ * dark theme or not. This will emit current status immediately and then
+ * will emit changes as needed.
+ */
+private fun ComponentActivity.isSystemInDarkModeFlow(): Flow =
+ callbackFlow {
+ channel.trySend(element = resources.configuration.isSystemInDarkMode)
+ val listener = Consumer {
+ channel.trySend(element = it.isSystemInDarkMode)
+ }
+ addOnConfigurationChangedListener(listener = listener)
+ awaitClose { removeOnConfigurationChangedListener(listener = listener) }
+ }
+ .distinctUntilChanged()
+ .conflate()
diff --git a/cmp-android/src/main/kotlin/cmp/android/app/ConfigurationExtension.kt b/cmp-android/src/main/kotlin/cmp/android/app/ConfigurationExtension.kt
new file mode 100644
index 0000000000..cf4c540f44
--- /dev/null
+++ b/cmp-android/src/main/kotlin/cmp/android/app/ConfigurationExtension.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package cmp.android.app
+
+import android.content.res.Configuration
+
+val Configuration.isSystemInDarkMode
+ get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
diff --git a/cmp-android/src/main/kotlin/cmp/android/app/MainActivity.kt b/cmp-android/src/main/kotlin/cmp/android/app/MainActivity.kt
index 1cd7e025a0..7f4793002e 100644
--- a/cmp-android/src/main/kotlin/cmp/android/app/MainActivity.kt
+++ b/cmp-android/src/main/kotlin/cmp/android/app/MainActivity.kt
@@ -1,93 +1,139 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package cmp.android.app
+import android.content.res.Resources
import android.os.Bundle
+import android.view.WindowManager
+import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.runtime.getValue
import androidx.core.os.LocaleListCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.core.view.WindowCompat
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cmp.shared.SharedApp
import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.dialogs.init
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
import org.koin.android.ext.android.inject
-import org.mifos.mobile.core.datastore.UserPreferencesRepository
-import org.mifos.mobile.core.ui.utils.ShareUtils
-import template.core.base.platform.LocalManagerProvider
+import org.mifos.core.data.repository.NetworkMonitor
+import org.mifos.core.data.repository.UserDataRepository
+import template.core.base.analytics.AnalyticsHelper
+import template.core.base.analytics.lifecycleTracker
+import template.core.base.platform.update.AppUpdateManager
+import template.core.base.platform.update.AppUpdateManagerImpl
+import template.core.base.ui.ShareUtils
import java.util.Locale
-import kotlin.getValue
/**
- * Main activity class.
- * This class is used to set the content view of the activity.
+ * Main activity class. This class is used to set the content view of the
+ * activity.
*
* @constructor Create empty Main activity
- * @see AppCompatActivity
+ * @see ComponentActivity
*/
+@Suppress("UnusedPrivateProperty")
class MainActivity : AppCompatActivity() {
- /**
- * Called when the activity is starting.
- * This is where most initialization should go: calling [setContentView(int)] to inflate the activity's UI,
- */
- private val userPreferencesRepository: UserPreferencesRepository by inject()
+ private lateinit var appUpdateManager: AppUpdateManager
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- runBlocking {
- val userThemeConfig = userPreferencesRepository.observeDarkThemeConfig.first()
- AppCompatDelegate.setDefaultNightMode(userThemeConfig.osValue)
- }
+ private val userPreferencesRepository: UserDataRepository by inject()
+
+ private val networkMonitor: NetworkMonitor by inject()
+ private val analyticsHelper: AnalyticsHelper by inject()
+ private val lifecycleTracker by lazy { analyticsHelper.lifecycleTracker() }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
var shouldShowSplashScreen = true
installSplashScreen().setKeepOnScreenCondition { shouldShowSplashScreen }
+ super.onCreate(savedInstanceState)
+ appUpdateManager = AppUpdateManagerImpl(this)
+
val darkThemeConfigFlow = userPreferencesRepository.observeDarkThemeConfig
- WindowCompat.setDecorFitsSystemWindows(window, false)
setupEdgeToEdge(darkThemeConfigFlow)
+
ShareUtils.setActivityProvider { return@setActivityProvider this }
FileKit.init(this)
- /**
- * Set the content view of the activity.
- * @see setContent
- */
+
+ analyticsHelper.setUserId(deviceData)
+
setContent {
- LocalManagerProvider(context = this) {
- SharedApp(
- handleThemeMode = {
- AppCompatDelegate.setDefaultNightMode(it)
- },
- handleAppLocale = {
- if (it.isNullOrBlank()) {
- AppCompatDelegate.setApplicationLocales(
- LocaleListCompat.getEmptyLocaleList(),
- )
+ val status by networkMonitor.isOnline.collectAsStateWithLifecycle(false)
+
+ if (status) {
+ appUpdateManager.checkForAppUpdate()
+ }
+
+ lifecycleTracker.markAppLaunchComplete()
+
+ SharedApp(
+ updateScreenCapture = ::updateScreenCapture,
+ handleRecreate = ::handleRecreate,
+ handleThemeMode = {
+ AppCompatDelegate.setDefaultNightMode(it)
+ },
+ handleAppLocale = { localeTag ->
+ val currentLocales = AppCompatDelegate.getApplicationLocales()
+ val newLocales = if (localeTag != null) {
+ LocaleListCompat.forLanguageTags(localeTag)
+ } else {
+ // System Default: clear app-specific locale
+ LocaleListCompat.getEmptyLocaleList()
+ }
+
+ // Only update if the locale has actually changed
+ if (currentLocales != newLocales) {
+ AppCompatDelegate.setApplicationLocales(newLocales)
+ // Update Locale.setDefault for non-UI formatting
+ if (localeTag != null) {
+ // Use forLanguageTag to properly parse locales like "en-GB", "pt-BR"
+ Locale.setDefault(Locale.forLanguageTag(localeTag))
} else {
- AppCompatDelegate.setApplicationLocales(
- LocaleListCompat.forLanguageTags(
- it,
- ),
- )
- Locale.setDefault(Locale(it))
+ // Reset to true system default locale from device configuration
+ // Use Resources.getSystem() to get device locale unaffected by app overrides
+ val systemLocale = Resources.getSystem().configuration.locales[0]
+ Locale.setDefault(systemLocale)
}
- },
- onSplashScreenRemoved = {
- shouldShowSplashScreen = false
- },
- )
- }
+ }
+ },
+ onSplashScreenRemoved = {
+ shouldShowSplashScreen = false
+ },
+ )
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ appUpdateManager.checkForResumeUpdateState()
+ lifecycleTracker.markAppBackground()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ lifecycleTracker.markAppLaunchStart()
+ }
+
+ private fun handleRecreate() {
+ recreate()
+ }
+
+ private fun updateScreenCapture(isScreenCaptureAllowed: Boolean) {
+ if (isScreenCaptureAllowed) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
+ } else {
+ window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
diff --git a/cmp-android/src/prod/AndroidManifest.xml b/cmp-android/src/prod/AndroidManifest.xml
new file mode 100644
index 0000000000..0d211bbf94
--- /dev/null
+++ b/cmp-android/src/prod/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cmp-desktop/build.gradle.kts b/cmp-desktop/build.gradle.kts
index 9ae1d463d9..1dd0cc5bf2 100644
--- a/cmp-desktop/build.gradle.kts
+++ b/cmp-desktop/build.gradle.kts
@@ -17,11 +17,9 @@ plugins {
}
kotlin {
- jvm {
- withJava()
- }
+ jvm()
- jvmToolchain(21)
+ jvmToolchain(17)
sourceSets {
jvmMain.dependencies {
@@ -33,32 +31,19 @@ kotlin {
implementation(libs.kotlin.reflect)
implementation(libs.koin.core)
-
- implementation(compose.components.resources)
}
}
}
-val appName: String = libs.versions.packageName.get()
-val packageNameSpace: String = libs.versions.packageNamespace.get()
-val appVersion: String = libs.versions.packageVersion.get()
+val appName: String = libs.versions.desktopPackageName.get()
+val packageNameSpace: String = libs.versions.desktopPackageNamespace.get()
+val appVersion: String = libs.versions.desktopPackageVersion.get()
compose.desktop {
application {
mainClass = "MainKt"
-
- val buildNumber: String = (project.findProperty("buildNumber") as String?) ?: "1"
- val isAppStoreRelease: Boolean =
- (project.findProperty("macOsAppStoreRelease") as String?)?.toBoolean() ?: false
-
nativeDistributions {
- targetFormats(
- TargetFormat.Pkg,
- TargetFormat.Dmg,
- TargetFormat.Msi,
- TargetFormat.Exe,
- TargetFormat.Deb
- )
+ targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe, TargetFormat.Deb)
packageName = appName
packageVersion = appVersion
description = "Desktop Application"
@@ -66,39 +51,16 @@ compose.desktop {
vendor = "Mifos Initiative"
licenseFile.set(project.file("../LICENSE"))
includeAllModules = true
- outputBaseDir.set(project.layout.buildDirectory.dir("release"))
macOS {
bundleID = packageNameSpace
dockName = appName
iconFile.set(project.file("icons/ic_launcher.icns"))
- minimumSystemVersion = "12.0"
- appStore = isAppStoreRelease
-
- infoPlist {
- packageBuildVersion = buildNumber
- extraKeysRawXml = """
- ITSAppUsesNonExemptEncryption
-
- """.trimIndent()
- }
-
- if (isAppStoreRelease) {
- signing {
- sign.set(true)
- identity.set("The Mifos Initiative")
- }
- provisioningProfile.set(project.file("embedded.provisionprofile"))
- runtimeProvisioningProfile.set(project.file("runtime.provisionprofile"))
- entitlementsFile.set(project.file("entitlements.plist"))
- runtimeEntitlementsFile.set(project.file("runtime-entitlements.plist"))
- } else {
- notarization {
- val providers = project.providers
- appleID.set(providers.environmentVariable("NOTARIZATION_APPLE_ID"))
- password.set(providers.environmentVariable("NOTARIZATION_PASSWORD"))
- teamID.set(providers.environmentVariable("NOTARIZATION_TEAM_ID"))
- }
+ notarization {
+ val providers = project.providers
+ appleID.set(providers.environmentVariable("NOTARIZATION_APPLE_ID"))
+ password.set(providers.environmentVariable("NOTARIZATION_PASSWORD"))
+ teamID.set(providers.environmentVariable("NOTARIZATION_TEAM_ID"))
}
}
@@ -116,36 +78,9 @@ compose.desktop {
}
}
buildTypes.release.proguard {
- isEnabled = false
-// configurationFiles.from(file("compose-desktop.pro"))
-// obfuscate.set(true)
-// optimize.set(true)
+ configurationFiles.from(file("compose-desktop.pro"))
+ obfuscate.set(true)
+ optimize.set(true)
}
}
}
-
-/**
- * Removes the `com.apple.quarantine` extended attribute from the built `.app`.
- *
- * Why:
- * Gatekeeper may mark files from the Internet with `com.apple.quarantine`.
- * If any such file ends up inside the `.app`, App Store validation can fail.
- */
-val unquarantineApp = tasks.register("unquarantineMacApp") {
- group = "macOS"
- description = "Remove com.apple.quarantine from the built .app before signing"
- onlyIf { org.gradle.internal.os.OperatingSystem.current().isMacOsX }
-
- dependsOn("createReleaseDistributable")
-
- val appName = "$appName.app" // set to your final .app name
- val appPath = layout.buildDirectory
- .dir("release/main-release/app/$appName")
- .map { it.asFile.absolutePath }
-
- commandLine("xattr", "-dr", "com.apple.quarantine", appPath.get())
-}
-
-tasks.matching { it.name == "packageReleasePkg" }.configureEach {
- dependsOn(unquarantineApp)
-}
diff --git a/cmp-desktop/src/jvmMain/kotlin/main.kt b/cmp-desktop/src/jvmMain/kotlin/main.kt
index 8d9a6853c1..9a8ad50196 100644
--- a/cmp-desktop/src/jvmMain/kotlin/main.kt
+++ b/cmp-desktop/src/jvmMain/kotlin/main.kt
@@ -8,14 +8,17 @@
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import cmp.shared.SharedApp
-import cmp.shared.generated.resources.Res
-import cmp.shared.generated.resources.application_title
import cmp.shared.utils.initKoin
-import org.jetbrains.compose.resources.stringResource
+import java.util.Locale
/**
* Main function.
@@ -39,18 +42,47 @@ fun main() {
// Creates a window state to manage the window's state.
val windowState = rememberWindowState()
+ // State to trigger recomposition when locale changes
+ var localeVersion by remember { mutableStateOf(0) }
+
// Creates a window with a specified title and close request handler.
Window(
onCloseRequest = ::exitApplication,
state = windowState,
- title = stringResource(Res.string.application_title),
+ title = "DesktopApp",
) {
- // Sets the content of the window.
- SharedApp(
- handleThemeMode = {},
- handleAppLocale = {},
- onSplashScreenRemoved = {},
- )
+ // Use key() to force complete recomposition when locale changes
+ key(localeVersion) {
+ // Sets the content of the window.
+ SharedApp(
+ updateScreenCapture = {},
+ handleRecreate = {
+ // Increment version to trigger recomposition
+ localeVersion++
+ },
+ handleThemeMode = {},
+ handleAppLocale = { languageTag ->
+ if (languageTag != null) {
+ // Parse language tag and set as default locale
+ val locale = when {
+ languageTag.contains("-") -> {
+ val parts = languageTag.split("-")
+ Locale(parts[0], parts[1])
+ }
+ else -> Locale(languageTag)
+ }
+ Locale.setDefault(locale)
+ } else {
+ // System Default: reset to system locale
+ val systemLocale = Locale.getDefault(Locale.Category.DISPLAY)
+ Locale.setDefault(systemLocale)
+ }
+ // Trigger recomposition with new locale
+ localeVersion++
+ },
+ onSplashScreenRemoved = {}
+ )
+ }
}
}
}
diff --git a/cmp-ios/Configuration/Config.xcconfig b/cmp-ios/Configuration/Config.xcconfig
index ce3c249aef..a23634af6c 100644
--- a/cmp-ios/Configuration/Config.xcconfig
+++ b/cmp-ios/Configuration/Config.xcconfig
@@ -1,3 +1,3 @@
TEAM_ID=L432S2FZP5
-BUNDLE_ID=org.mifos.mobile
-APP_NAME=Mifos Mobile
+BUNDLE_ID=cmp.ios
+APP_NAME=LiteDo
\ No newline at end of file
diff --git a/cmp-ios/Podfile b/cmp-ios/Podfile
index 0f05686a76..fb7d97ce0b 100644
--- a/cmp-ios/Podfile
+++ b/cmp-ios/Podfile
@@ -1,21 +1,36 @@
-deployment_target = '16.0'
+source 'https://cdn.cocoapods.org'
+platform :ios, '16.0'
+use_frameworks!
target 'iosApp' do
- use_frameworks!
- platform :ios, deployment_target
- # Pods for iosApp
+ project 'iosApp.xcodeproj'
pod 'cmp_shared', :path => '../cmp-shared'
+
+ # Native Firebase iOS Pods required by firebase-kotlin-sdk
+ pod 'FirebaseCore'
+ pod 'FirebaseAnalytics'
+ pod 'FirebaseCrashlytics'
end
post_install do |installer|
- installer.generated_projects.each do |project|
- project.targets.each do |target|
- target.build_configurations.each do |config|
- config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = deployment_target
- end
- end
- project.build_configurations.each do |config|
- config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = deployment_target
- end
+ installer.pods_project.targets.each do |target|
+ target.build_configurations.each do |config|
+ # Disable code signing for Pod targets
+ config.build_settings['CODE_SIGN_IDENTITY'] = ''
+ config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
+ config.build_settings['CODE_SIGNING_REQUIRED'] = 'NO'
+ config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ''
+
+ # Remove provisioning profile settings from Pod targets
+ config.build_settings.delete('PROVISIONING_PROFILE_SPECIFIER')
+ config.build_settings.delete('PROVISIONING_PROFILE')
+ config.build_settings.delete('DEVELOPMENT_TEAM')
+
+ # Fix deployment target warnings
+ deployment_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
+ if deployment_target && deployment_target.to_f < 12.0
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
+ end
end
-end
\ No newline at end of file
+ end
+end
diff --git a/cmp-ios/iosApp.xcodeproj/project.pbxproj b/cmp-ios/iosApp.xcodeproj/project.pbxproj
index 1cbac6655b..5e27706fc5 100644
--- a/cmp-ios/iosApp.xcodeproj/project.pbxproj
+++ b/cmp-ios/iosApp.xcodeproj/project.pbxproj
@@ -9,22 +9,22 @@
/* Begin PBXBuildFile section */
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
- 1C3FE2006C77932769810076 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DF4DB1CF5E68C5614135A56 /* Pods_iosApp.framework */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
+ 73867D86E599B875F7561EBD /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8B63CDEE6A83A279F370FDF /* Pods_iosApp.framework */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
- 1EB8E354CA5D35F960D11D5D /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; };
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
- 6DF4DB1CF5E68C5614135A56 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 7555FF7B242A565900829871 /* Mifos Mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mifos Mobile.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7555FF7B242A565900829871 /* LiteDo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiteDo.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 8DADAFB5E75F5E24CA4F0EB4 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
+ B5AB1CF32380D00920DC66AD /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; };
+ F8B63CDEE6A83A279F370FDF /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ FCFBF765CF1AE0CE66538ADF /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -32,7 +32,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 1C3FE2006C77932769810076 /* Pods_iosApp.framework in Frameworks */,
+ 73867D86E599B875F7561EBD /* Pods_iosApp.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -47,19 +47,10 @@
path = "Preview Content";
sourceTree = "";
};
- 1826950F674A72E12E219090 /* Pods */ = {
- isa = PBXGroup;
- children = (
- 8DADAFB5E75F5E24CA4F0EB4 /* Pods-iosApp.debug.xcconfig */,
- 1EB8E354CA5D35F960D11D5D /* Pods-iosApp.release.xcconfig */,
- );
- path = Pods;
- sourceTree = "";
- };
42799AB246E5F90AF97AA0EF /* Frameworks */ = {
isa = PBXGroup;
children = (
- 6DF4DB1CF5E68C5614135A56 /* Pods_iosApp.framework */,
+ F8B63CDEE6A83A279F370FDF /* Pods_iosApp.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -71,14 +62,14 @@
7555FF7D242A565900829871 /* iosApp */,
7555FF7C242A565900829871 /* Products */,
42799AB246E5F90AF97AA0EF /* Frameworks */,
- 1826950F674A72E12E219090 /* Pods */,
+ AF5B1102F079B671D1EB1074 /* Pods */,
);
sourceTree = "";
};
7555FF7C242A565900829871 /* Products */ = {
isa = PBXGroup;
children = (
- 7555FF7B242A565900829871 /* Mifos Mobile.app */,
+ 7555FF7B242A565900829871 /* LiteDo.app */,
);
name = Products;
sourceTree = "";
@@ -103,6 +94,15 @@
path = Configuration;
sourceTree = "";
};
+ AF5B1102F079B671D1EB1074 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ B5AB1CF32380D00920DC66AD /* Pods-iosApp.debug.xcconfig */,
+ FCFBF765CF1AE0CE66538ADF /* Pods-iosApp.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -110,11 +110,12 @@
isa = PBXNativeTarget;
buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */;
buildPhases = (
- D005710AF42AFBD3373CBB90 /* [CP] Check Pods Manifest.lock */,
+ C0BDCDFA5D651BAABC1D1A96 /* [CP] Check Pods Manifest.lock */,
7555FF77242A565900829871 /* Sources */,
B92378962B6B1156000C7307 /* Frameworks */,
7555FF79242A565900829871 /* Resources */,
- 3843B476B28208558ACE8C15 /* [CP] Copy Pods Resources */,
+ F3CE6A35EE97CC26402CBE37 /* [CP] Embed Pods Frameworks */,
+ 087C63914A01F15B2F3377F1 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -122,7 +123,7 @@
);
name = iosApp;
productName = iosApp;
- productReference = 7555FF7B242A565900829871 /* Mifos Mobile.app */;
+ productReference = 7555FF7B242A565900829871 /* LiteDo.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -172,7 +173,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
- 3843B476B28208558ACE8C15 /* [CP] Copy Pods Resources */ = {
+ 087C63914A01F15B2F3377F1 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -189,7 +190,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n";
showEnvVarsInLog = 0;
};
- D005710AF42AFBD3373CBB90 /* [CP] Check Pods Manifest.lock */ = {
+ C0BDCDFA5D651BAABC1D1A96 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -211,6 +212,23 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
+ F3CE6A35EE97CC26402CBE37 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -350,7 +368,7 @@
};
7555FFA6242A565B00829871 /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 8DADAFB5E75F5E24CA4F0EB4 /* Pods-iosApp.debug.xcconfig */;
+ baseConfigurationReference = B5AB1CF32380D00920DC66AD /* Pods-iosApp.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
@@ -363,15 +381,17 @@
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
);
INFOPLIST_FILE = iosApp/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = "Mifos Mobile";
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
+ INFOPLIST_KEY_CFBundleDisplayName = LiteDo;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
+ CURRENT_PROJECT_VERSION = 47;
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
+ "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.mifos.kmp.template;
PRODUCT_NAME = "${APP_NAME}";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@@ -381,28 +401,31 @@
};
7555FFA7242A565B00829871 /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 1EB8E354CA5D35F960D11D5D /* Pods-iosApp.release.xcconfig */;
+ baseConfigurationReference = FCFBF765CF1AE0CE66538ADF /* Pods-iosApp.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = "${TEAM_ID}";
+ DEVELOPMENT_TEAM = L432S2FZP5;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
- "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
+ "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
);
INFOPLIST_FILE = iosApp/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = "Mifos Mobile";
- INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
+ INFOPLIST_KEY_CFBundleDisplayName = LiteDo;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
+ CURRENT_PROJECT_VERSION = 47;
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
+ "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = org.mifos.kmp.template;
PRODUCT_NAME = "${APP_NAME}";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
diff --git a/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..0c67376eba
--- /dev/null
+++ b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/hekmatullahamin.xcuserdatad/UserInterfaceState.xcuserstate b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/hekmatullahamin.xcuserdatad/UserInterfaceState.xcuserstate
index 86c5772eb6..d70f71e4c9 100644
Binary files a/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/hekmatullahamin.xcuserdatad/UserInterfaceState.xcuserstate and b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/hekmatullahamin.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/skniyajali.xcuserdatad/UserInterfaceState.xcuserstate b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/skniyajali.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 0000000000..14469023a6
Binary files /dev/null and b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/skniyajali.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/skniyajali.xcuserdatad/WorkspaceSettings.xcsettings b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/skniyajali.xcuserdatad/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000000..723a561c1e
--- /dev/null
+++ b/cmp-ios/iosApp.xcodeproj/project.xcworkspace/xcuserdata/skniyajali.xcuserdatad/WorkspaceSettings.xcsettings
@@ -0,0 +1,16 @@
+
+
+
+
+ BuildLocationStyle
+ UseAppPreferences
+ CompilationCachingSetting
+ Default
+ CustomBuildLocationType
+ RelativeToDerivedData
+ DerivedDataLocationStyle
+ Default
+ ShowSharedSchemesAutomaticallyEnabled
+
+
+
diff --git a/cmp-ios/iosApp.xcodeproj/xcuserdata/hekmatullahamin.xcuserdatad/xcschemes/xcschememanagement.plist b/cmp-ios/iosApp.xcodeproj/xcuserdata/hekmatullahamin.xcuserdatad/xcschemes/xcschememanagement.plist
index aede5d13b6..56b5955f8e 100644
--- a/cmp-ios/iosApp.xcodeproj/xcuserdata/hekmatullahamin.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/cmp-ios/iosApp.xcodeproj/xcuserdata/hekmatullahamin.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -4,7 +4,7 @@
SchemeUserState
- cmp-ios.xcscheme_^#shared#^_
+ iosApp.xcscheme_^#shared#^_
orderHint
0
diff --git a/cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/iosApp.xcscheme b/cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/iosApp.xcscheme
new file mode 100644
index 0000000000..21d8ca5591
--- /dev/null
+++ b/cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/iosApp.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist b/cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000000..fa59f97d5e
--- /dev/null
+++ b/cmp-ios/iosApp.xcodeproj/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ SchemeUserState
+
+ iosApp.xcscheme
+
+ orderHint
+ 0
+
+
+
+
diff --git a/cmp-ios/iosApp.xcworkspace/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist b/cmp-ios/iosApp.xcworkspace/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000000..ee3458dd76
--- /dev/null
+++ b/cmp-ios/iosApp.xcworkspace/xcuserdata/skniyajali.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/cmp-ios/iosApp/ContentView.swift b/cmp-ios/iosApp/ContentView.swift
index e130b8a2e4..63b9da7e03 100644
--- a/cmp-ios/iosApp/ContentView.swift
+++ b/cmp-ios/iosApp/ContentView.swift
@@ -13,7 +13,8 @@ struct ComposeView: UIViewControllerRepresentable {
struct ContentView: View {
var body: some View {
ComposeView()
- .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
+ .ignoresSafeArea(edges: .all)
+ .ignoresSafeArea(.keyboard) // .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
}
}
diff --git a/cmp-ios/iosApp/Info.plist b/cmp-ios/iosApp/Info.plist
index 597a03ea9f..87d3ab6368 100644
--- a/cmp-ios/iosApp/Info.plist
+++ b/cmp-ios/iosApp/Info.plist
@@ -17,19 +17,21 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.0.0
+ $(MARKETING_VERSION)
CFBundleVersion
- 4
+ $(CURRENT_PROJECT_VERSION)
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
- NSCameraUsageDescription
- We use the camera to scan QR codes for payments and to add beneficiaries. No images or video are stored.
+ NSBluetoothAlwaysUsageDescription
+ This app does not use Bluetooth. This message is required for compliance only.
+ NSContactsUsageDescription
+ This app does not access your contacts. This message is required for compliance only.
+ NSLocationWhenInUseUsageDescription
+ This app does not access your location. This message is required for compliance only.
NSPhotoLibraryAddUsageDescription
- Allow access to choose a photo or document you decide to upload (e.g., profile photo or ID).
- NSPhotoLibraryUsageDescription
- Allow access to choose a photo or document you decide to upload (e.g., profile photo or ID).
+ Allow access to add photos to your library so you can save artworks directly to your device and view them offline.
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
diff --git a/cmp-shared/build.gradle.kts b/cmp-shared/build.gradle.kts
index 39dec004c3..c78a117d28 100644
--- a/cmp-shared/build.gradle.kts
+++ b/cmp-shared/build.gradle.kts
@@ -1,34 +1,41 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
+ alias(libs.plugins.kmp.library.convention)
alias(libs.plugins.cmp.feature.convention)
- alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlinCocoapods)
}
kotlin {
- iosX64()
- iosArm64()
- iosSimulatorArm64()
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach { iosTarget ->
+ iosTarget.binaries.framework {
+ baseName = "ComposeApp"
+ isStatic = true
+ optimized = true
+ }
+ }
sourceSets {
commonMain.dependencies {
// Navigation Modules
implementation(projects.cmpNavigation)
implementation(compose.components.resources)
- api(projects.core.data)
- api(projects.core.network)
- //put your multiplatform dependencies here
- implementation(compose.material)
- implementation(compose.material3)
+ implementation(projects.coreBase.platform)
+ implementation(projects.coreBase.ui)
+
+ implementation(libs.coil.kt.compose)
}
desktopMain.dependencies {
@@ -40,7 +47,7 @@ kotlin {
cocoapods {
summary = "KMP Shared Module"
- homepage = "https://github.com/openMF/mifos-mobile"
+ homepage = "https://github.com/openMF/kmp-project-template"
version = "1.0"
ios.deploymentTarget = "16.0"
podfile = project.file("../cmp-ios/Podfile")
diff --git a/cmp-shared/cmp_shared.podspec b/cmp-shared/cmp_shared.podspec
index 7094d449a6..a811666942 100644
--- a/cmp-shared/cmp_shared.podspec
+++ b/cmp-shared/cmp_shared.podspec
@@ -1,7 +1,7 @@
Pod::Spec.new do |spec|
spec.name = 'cmp_shared'
spec.version = '1.0'
- spec.homepage = 'https://github.com/openMF/mifos-mobile'
+ spec.homepage = 'https://github.com/openMF/kmp-project-template'
spec.source = { :http=> ''}
spec.authors = ''
spec.license = ''
diff --git a/cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt b/cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt
index aefea2f802..f28717e107 100644
--- a/cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt
+++ b/cmp-shared/src/commonMain/kotlin/cmp/shared/SharedApp.kt
@@ -1,29 +1,42 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package cmp.shared
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cmp.navigation.ComposeApp
+import coil3.compose.LocalPlatformContext
+import template.core.base.platform.LocalManagerProvider
+import template.core.base.platform.context.LocalContext
+import template.core.base.ui.LocalImageLoaderProvider
+import template.core.base.ui.getDefaultImageLoader
@Composable
fun SharedApp(
+ updateScreenCapture: (isScreenCaptureAllowed: Boolean) -> Unit,
+ handleRecreate: () -> Unit,
handleThemeMode: (osValue: Int) -> Unit,
handleAppLocale: (locale: String?) -> Unit,
- onSplashScreenRemoved: () -> Unit,
modifier: Modifier = Modifier,
+ onSplashScreenRemoved: () -> Unit,
) {
- ComposeApp(
- handleThemeMode = handleThemeMode,
- handleAppLocale = handleAppLocale,
- onSplashScreenRemoved = onSplashScreenRemoved,
- modifier = modifier,
- )
+ LocalManagerProvider(LocalContext.current) {
+ LocalImageLoaderProvider(getDefaultImageLoader(LocalPlatformContext.current)) {
+ ComposeApp(
+ updateScreenCapture = updateScreenCapture,
+ handleRecreate = handleRecreate,
+ handleThemeMode = handleThemeMode,
+ handleAppLocale = handleAppLocale,
+ onSplashScreenRemoved = onSplashScreenRemoved,
+ modifier = modifier,
+ )
+ }
+ }
}
diff --git a/cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt b/cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt
index a92847b988..55f64f0b1e 100644
--- a/cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt
+++ b/cmp-shared/src/commonMain/kotlin/cmp/shared/utils/KoinExt.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package cmp.shared.utils
diff --git a/cmp-shared/src/nativeMain/kotlin/org/mifos/shared/ViewController.kt b/cmp-shared/src/nativeMain/kotlin/org/mifos/shared/ViewController.kt
index e8be4c6b70..04d8815120 100644
--- a/cmp-shared/src/nativeMain/kotlin/org/mifos/shared/ViewController.kt
+++ b/cmp-shared/src/nativeMain/kotlin/org/mifos/shared/ViewController.kt
@@ -1,17 +1,24 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package org.mifos.shared
import androidx.compose.ui.window.ComposeUIViewController
import cmp.shared.SharedApp
import cmp.shared.utils.initKoin
+import platform.Foundation.NSUserDefaults
+import platform.QuartzCore.CALayer
+import platform.UIKit.UIApplication
+import platform.UIKit.UITextField
+import platform.UIKit.UIUserInterfaceStyle
+
+private var secureTextField: UITextField? = null
fun viewController() = ComposeUIViewController(
configure = {
@@ -19,8 +26,45 @@ fun viewController() = ComposeUIViewController(
},
) {
SharedApp(
- handleThemeMode = {},
- handleAppLocale = {},
+ updateScreenCapture = { isScreenCaptureAllowed ->
+ UIApplication.sharedApplication.keyWindow?.let { window ->
+ if (!isScreenCaptureAllowed) {
+ // Create secure text field to prevent screen capture/recording
+ if (secureTextField == null) {
+ val textField = UITextField()
+ textField.setSecureTextEntry(true)
+ textField.setUserInteractionEnabled(false)
+ window.addSubview(textField)
+ (textField.layer.sublayers?.firstOrNull() as? CALayer)?.let { secureLayer ->
+ window.layer.superlayer?.addSublayer(secureLayer)
+ }
+ secureTextField = textField
+ }
+ } else {
+ secureTextField?.removeFromSuperview()
+ secureTextField = null
+ }
+ }
+ },
+ handleRecreate = {},
+ handleThemeMode = { osValue ->
+ val style = when (osValue) {
+ 1 -> UIUserInterfaceStyle.UIUserInterfaceStyleLight
+ 2 -> UIUserInterfaceStyle.UIUserInterfaceStyleDark
+ else -> UIUserInterfaceStyle.UIUserInterfaceStyleUnspecified
+ }
+ UIApplication.sharedApplication.keyWindow?.overrideUserInterfaceStyle = style
+ },
+ handleAppLocale = { languageTag ->
+ if (languageTag != null) {
+ // Set specific language
+ NSUserDefaults.standardUserDefaults.setObject(listOf(languageTag), forKey = "AppleLanguages")
+ } else {
+ // System Default: remove app-specific language setting
+ NSUserDefaults.standardUserDefaults.removeObjectForKey("AppleLanguages")
+ }
+ NSUserDefaults.standardUserDefaults.synchronize()
+ },
onSplashScreenRemoved = {},
)
}
diff --git a/cmp-web/build.gradle.kts b/cmp-web/build.gradle.kts
index 5dec055ff0..f07ed52b8f 100644
--- a/cmp-web/build.gradle.kts
+++ b/cmp-web/build.gradle.kts
@@ -8,7 +8,7 @@ plugins {
kotlin {
js(IR) {
- moduleName = "cmp-web"
+ outputModuleName = "cmp-web"
browser {
commonWebpackConfig {
outputFileName = "cmp-web.js"
@@ -19,7 +19,7 @@ kotlin {
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
- moduleName = "cmp-wasm"
+ outputModuleName = "cmp-wasm"
browser {
commonWebpackConfig {
outputFileName = "cmp-wasm.js"
@@ -51,6 +51,7 @@ kotlin {
implementation(libs.multiplatform.settings.coroutines)
implementation(libs.koin.core)
+ implementation(libs.ktor.client.js)
}
}
@@ -59,10 +60,6 @@ kotlin {
}
}
-tasks.register("jsBrowserRun") {
- dependsOn("jsBrowserDevelopmentRun")
-}
-
compose.resources {
publicResClass = true
generateResClass = always
diff --git a/cmp-web/src/jsMain/kotlin/Application.kt b/cmp-web/src/jsMain/kotlin/Application.kt
index 68bdefc704..8bea726bd5 100644
--- a/cmp-web/src/jsMain/kotlin/Application.kt
+++ b/cmp-web/src/jsMain/kotlin/Application.kt
@@ -1,8 +1,15 @@
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import cmp.shared.SharedApp
import cmp.shared.utils.initKoin
import kotlinx.browser.document
+import kotlinx.browser.localStorage
+import kotlinx.browser.window
import org.jetbrains.skiko.wasm.onWasmReady
/*
@@ -18,13 +25,46 @@ fun main() {
initKoin() // Set up Koin for dependency injection.
+ // Apply stored language preference on startup
+ val storedLanguage = localStorage.getItem("app_language")
+ if (storedLanguage != null) {
+ document.documentElement?.setAttribute("lang", storedLanguage)
+ }
+
onWasmReady {
ComposeViewport(document.body!!) {
- SharedApp(
- handleThemeMode = {},
- handleAppLocale = {},
- onSplashScreenRemoved = {},
- )
+ // State to trigger recomposition when locale changes
+ var localeVersion by remember { mutableStateOf(0) }
+
+ // Use key() to force complete recomposition when locale changes
+ key(localeVersion) {
+ SharedApp(
+ updateScreenCapture = {},
+ handleRecreate = {
+ // Reload the page to apply locale changes
+ window.location.reload()
+ },
+ handleThemeMode = {},
+ handleAppLocale = { languageTag ->
+ if (languageTag != null) {
+ // Store language preference in localStorage
+ localStorage.setItem("app_language", languageTag)
+ // Set HTML lang attribute for accessibility
+ document.documentElement?.setAttribute("lang", languageTag)
+ } else {
+ // System Default: remove stored language preference
+ localStorage.removeItem("app_language")
+ // Reset to browser's default language
+ val browserLang = window.navigator.language
+ document.documentElement?.setAttribute("lang", browserLang)
+ }
+ // Reload page to apply language changes (required for web)
+ // Note: This will reload the page, and locale selection depends on browser settings
+ // window.location.reload()
+ },
+ onSplashScreenRemoved = {}
+ )
+ }
}
}
}
diff --git a/cmp-web/src/wasmJsMain/kotlin/Main.kt b/cmp-web/src/wasmJsMain/kotlin/Main.kt
index e91410a38d..16868baf5c 100644
--- a/cmp-web/src/wasmJsMain/kotlin/Main.kt
+++ b/cmp-web/src/wasmJsMain/kotlin/Main.kt
@@ -1,8 +1,19 @@
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
+import androidx.compose.ui.window.ComposeViewport
+import androidx.compose.ui.window.ComposeViewportConfiguration
import cmp.shared.SharedApp
import cmp.shared.utils.initKoin
+import kotlinx.browser.document
+import kotlinx.browser.localStorage
+import kotlinx.browser.window
import org.jetbrains.compose.resources.configureWebResources
+import org.jetbrains.skiko.wasm.onWasmReady
/**
* Main function.
@@ -24,6 +35,12 @@ fun main() {
*/
initKoin()
+ // Apply stored language preference on startup
+ val storedLanguage = localStorage.getItem("app_language")
+ if (storedLanguage != null) {
+ document.documentElement?.setAttribute("lang", storedLanguage)
+ }
+
/*
* Configures the web resources for the application.
* Specifically, it sets a path mapping for resources (e.g., CSS, JS).
@@ -36,18 +53,44 @@ fun main() {
* Creates a Canvas-based window for rendering the Compose UI.
* This window uses the canvas element with the ID "ComposeTarget" and has the title "WebApp".
*/
- CanvasBasedWindow(
- title = "Mifos Mobile", // Window title
- canvasElementId = "ComposeTarget", // The canvas element where the Compose UI will be rendered
- ) {
- /*
- * Invokes the root composable of the application.
- * This function is responsible for setting up the entire UI structure of the app.
- */
- SharedApp(
- handleThemeMode = {},
- handleAppLocale = {},
- onSplashScreenRemoved = {},
- )
+ onWasmReady {
+ ComposeViewport(document.body!!) {
+ // State to trigger recomposition when locale changes
+ var localeVersion by remember { mutableStateOf(0) }
+
+ // Use key() to force complete recomposition when locale changes
+ key(localeVersion) {
+ /*
+ * Invokes the root composable of the application.
+ * This function is responsible for setting up the entire UI structure of the app.
+ */
+ SharedApp(
+ updateScreenCapture = {},
+ handleRecreate = {
+ // Reload the page to apply locale changes
+ window.location.reload()
+ },
+ handleThemeMode = {},
+ handleAppLocale = { languageTag ->
+ if (languageTag != null) {
+ // Store language preference in localStorage
+ localStorage.setItem("app_language", languageTag)
+ // Set HTML lang attribute for accessibility
+ document.documentElement?.setAttribute("lang", languageTag)
+ } else {
+ // System Default: remove stored language preference
+ localStorage.removeItem("app_language")
+ // Reset to browser's default language
+ val browserLang = window.navigator.language
+ document.documentElement?.setAttribute("lang", browserLang)
+ }
+ // Reload page to apply language changes (required for web)
+ // Note: This will reload the page, and locale selection depends on browser settings
+ // window.location.reload()
+ },
+ onSplashScreenRemoved = {},
+ )
+ }
+ }
}
}
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
index 8bcd830b72..811d8cb047 100644
--- a/config/detekt/detekt.yml
+++ b/config/detekt/detekt.yml
@@ -137,7 +137,7 @@ complexity:
threshold: 15
ComplexCondition:
active: true
- threshold: 5
+ threshold: 4
ComplexInterface:
active: false
threshold: 10
@@ -146,7 +146,7 @@ complexity:
ignoreOverloaded: false
CyclomaticComplexMethod:
active: true
- threshold: 25
+ threshold: 15
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
@@ -168,7 +168,7 @@ complexity:
threshold: 600
LongMethod:
active: true
- threshold: 180 #60
+ threshold: 150 #60
LongParameterList:
active: true
# Updating Common values based on current scenario
@@ -186,7 +186,7 @@ complexity:
ignoreArgumentsMatchingNames: false
NestedBlockDepth:
active: true
- threshold: 6
+ threshold: 4
NestedScopeFunctions:
active: false
threshold: 1
@@ -232,7 +232,7 @@ complexity:
thresholdInFiles: 20
thresholdInClasses: 20
thresholdInInterfaces: 20
- thresholdInObjects: 22
+ thresholdInObjects: 20
thresholdInEnums: 20
ignoreDeprecated: false
ignorePrivate: false
@@ -432,6 +432,7 @@ naming:
"**/androidInstrumentedTest/**",
"**/jsTest/**",
"**/iosTest/**",
+ "**/generated/**",
]
functionPattern: "[a-z][a-zA-Z0-9]*"
excludeClassPattern: "$^"
@@ -689,7 +690,7 @@ style:
active: false
DestructuringDeclarationWithTooManyEntries:
active: true
- maxDestructuringEntries: 4
+ maxDestructuringEntries: 3
DoubleNegativeLambda:
active: false
negativeFunctions:
diff --git a/core-base/analytics/build.gradle.kts b/core-base/analytics/build.gradle.kts
index bf8cc7a4e5..e399ea9aac 100644
--- a/core-base/analytics/build.gradle.kts
+++ b/core-base/analytics/build.gradle.kts
@@ -1,14 +1,14 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
- alias(libs.plugins.kmp.library.convention)
+ alias(libs.plugins.kmp.core.base.library.convention)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}
@@ -25,7 +25,7 @@ kotlin {
implementation(compose.ui)
implementation(compose.foundation)
implementation(libs.kermit.logging)
-
+
// For timing and performance tracking
implementation(libs.kotlinx.datetime)
}
@@ -49,7 +49,7 @@ kotlin {
mobileMain.dependencies {
api(libs.gitlive.firebase.crashlytics)
}
-
+
// Test dependencies for all platforms
commonTest.dependencies {
implementation(libs.kotlin.test)
diff --git a/core-base/analytics/src/androidDemo/kotlin/template/core/base/analytics/di/AnalyticsModule.kt b/core-base/analytics/src/androidDemo/kotlin/template/core/base/analytics/di/AnalyticsModule.kt
index 7163240687..45bcfaff34 100644
--- a/core-base/analytics/src/androidDemo/kotlin/template/core/base/analytics/di/AnalyticsModule.kt
+++ b/core-base/analytics/src/androidDemo/kotlin/template/core/base/analytics/di/AnalyticsModule.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics.di
diff --git a/core-base/analytics/src/androidMain/AndroidManifest.xml b/core-base/analytics/src/androidMain/AndroidManifest.xml
index 2d705da335..afcf313e36 100644
--- a/core-base/analytics/src/androidMain/AndroidManifest.xml
+++ b/core-base/analytics/src/androidMain/AndroidManifest.xml
@@ -1,11 +1,11 @@
\ No newline at end of file
diff --git a/core-base/analytics/src/androidProd/kotlin/template.core.base.analytics/di/AnalyticsModule.kt b/core-base/analytics/src/androidProd/kotlin/template.core.base.analytics/di/AnalyticsModule.kt
index ad8883b833..1e09b2cb27 100644
--- a/core-base/analytics/src/androidProd/kotlin/template.core.base.analytics/di/AnalyticsModule.kt
+++ b/core-base/analytics/src/androidProd/kotlin/template.core.base.analytics/di/AnalyticsModule.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
@file:Suppress("InvalidPackageDeclaration")
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsEvent.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsEvent.kt
index ff57128e1e..12ff75c869 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsEvent.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsEvent.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2023 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
@@ -255,11 +255,26 @@ object Types {
*
* @since 1.0.0
*/
-data class Param(val key: String, val value: String) {
- init {
- require(key.isNotBlank()) { "Parameter key cannot be blank" }
- require(key.length <= 40) { "Parameter key cannot exceed 40 characters" }
- require(value.length <= 100) { "Parameter value cannot exceed 100 characters" }
+@ConsistentCopyVisibility
+data class Param private constructor(
+ val key: String,
+ val value: String,
+) {
+ companion object {
+ private const val MAX_VALUE_LENGTH = 100
+ private const val MAX_KEY_LENGTH = 40
+ private const val FALLBACK_KEY = "unknown_param"
+
+ operator fun invoke(key: String, value: String): Param {
+ val safeKey = key
+ .takeIf { it.isNotBlank() }
+ ?.take(MAX_KEY_LENGTH)
+ ?: FALLBACK_KEY
+
+ val safeValue = value.take(MAX_VALUE_LENGTH)
+
+ return Param(safeKey, safeValue)
+ }
}
}
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsExtensions.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsExtensions.kt
index a755deab7a..cd521e79f9 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsExtensions.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsExtensions.kt
@@ -1,15 +1,15 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
-import kotlinx.datetime.Clock
+import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.DurationUnit
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsHelper.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsHelper.kt
index 34ec640eda..e4561bc062 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsHelper.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/AnalyticsHelper.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2023 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/NoOpAnalyticsHelper.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/NoOpAnalyticsHelper.kt
index 8e85f955c9..4cd5a82250 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/NoOpAnalyticsHelper.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/NoOpAnalyticsHelper.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2023 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/PerformanceTracker.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/PerformanceTracker.kt
index e910de3a18..696a0643a3 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/PerformanceTracker.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/PerformanceTracker.kt
@@ -1,15 +1,15 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
-import kotlinx.datetime.Clock
+import kotlin.time.Clock
/** Performance tracking utilities for analytics */
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/StubAnalyticsHelper.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/StubAnalyticsHelper.kt
index e161f7f022..7b092612d4 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/StubAnalyticsHelper.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/StubAnalyticsHelper.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2023 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/TestingUtils.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/TestingUtils.kt
index bd05d2e118..38b2459384 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/TestingUtils.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/TestingUtils.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/UiHelpers.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/UiHelpers.kt
index dbe48f75f5..6a7d71dba4 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/UiHelpers.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/UiHelpers.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2023 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/ValidationUtils.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/ValidationUtils.kt
index 597b5ed749..bfb1440c02 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/ValidationUtils.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/ValidationUtils.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
diff --git a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/di/AnalyticsModule.kt b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/di/AnalyticsModule.kt
index 300fd2458e..ff49190443 100644
--- a/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/di/AnalyticsModule.kt
+++ b/core-base/analytics/src/commonMain/kotlin/template/core/base/analytics/di/AnalyticsModule.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics.di
diff --git a/core-base/analytics/src/desktopMain/kotlin/template/core/base/analytics/di/AnalyticsModule.desktop.kt b/core-base/analytics/src/desktopMain/kotlin/template/core/base/analytics/di/AnalyticsModule.desktop.kt
index eb69cd3f45..c82b116dca 100644
--- a/core-base/analytics/src/desktopMain/kotlin/template/core/base/analytics/di/AnalyticsModule.desktop.kt
+++ b/core-base/analytics/src/desktopMain/kotlin/template/core/base/analytics/di/AnalyticsModule.desktop.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics.di
diff --git a/core-base/analytics/src/jsMain/kotlin/template.core.base.analytics/di/AnalyticsModule.js.kt b/core-base/analytics/src/jsMain/kotlin/template.core.base.analytics/di/AnalyticsModule.js.kt
index 83d89fb5ad..1b3db40e5d 100644
--- a/core-base/analytics/src/jsMain/kotlin/template.core.base.analytics/di/AnalyticsModule.js.kt
+++ b/core-base/analytics/src/jsMain/kotlin/template.core.base.analytics/di/AnalyticsModule.js.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
@file:Suppress("InvalidPackageDeclaration")
diff --git a/core-base/analytics/src/nativeMain/kotlin/template.core.base.analytics.di/AnalyticsModule.native.kt b/core-base/analytics/src/nativeMain/kotlin/template.core.base.analytics.di/AnalyticsModule.native.kt
index 1d1bc710be..369336f23f 100644
--- a/core-base/analytics/src/nativeMain/kotlin/template.core.base.analytics.di/AnalyticsModule.native.kt
+++ b/core-base/analytics/src/nativeMain/kotlin/template.core.base.analytics.di/AnalyticsModule.native.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
@file:Suppress("InvalidPackageDeclaration")
diff --git a/core-base/analytics/src/nonJsCommonMain/kotlin/template/core/base/analytics/FirebaseAnalyticsHelper.kt b/core-base/analytics/src/nonJsCommonMain/kotlin/template/core/base/analytics/FirebaseAnalyticsHelper.kt
index 57b68ead17..bfa9814bac 100644
--- a/core-base/analytics/src/nonJsCommonMain/kotlin/template/core/base/analytics/FirebaseAnalyticsHelper.kt
+++ b/core-base/analytics/src/nonJsCommonMain/kotlin/template/core/base/analytics/FirebaseAnalyticsHelper.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics
diff --git a/core-base/analytics/src/wasmJsMain/kotlin/template/core/base/analytics/di/AnalyticsModule.wasmJs.kt b/core-base/analytics/src/wasmJsMain/kotlin/template/core/base/analytics/di/AnalyticsModule.wasmJs.kt
index eb69cd3f45..c82b116dca 100644
--- a/core-base/analytics/src/wasmJsMain/kotlin/template/core/base/analytics/di/AnalyticsModule.wasmJs.kt
+++ b/core-base/analytics/src/wasmJsMain/kotlin/template/core/base/analytics/di/AnalyticsModule.wasmJs.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.analytics.di
diff --git a/core-base/common/build.gradle.kts b/core-base/common/build.gradle.kts
index c64c7ea544..e21d2eb504 100644
--- a/core-base/common/build.gradle.kts
+++ b/core-base/common/build.gradle.kts
@@ -1,15 +1,14 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
- alias(libs.plugins.kmp.library.convention)
- alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.kmp.core.base.library.convention)
}
android {
@@ -44,4 +43,4 @@ kotlin {
api(libs.jb.kotlin.dom)
}
}
-}
\ No newline at end of file
+}
diff --git a/core-base/common/src/androidMain/AndroidManifest.xml b/core-base/common/src/androidMain/AndroidManifest.xml
index b0ca1ad2a4..8e3b49f55d 100644
--- a/core-base/common/src/androidMain/AndroidManifest.xml
+++ b/core-base/common/src/androidMain/AndroidManifest.xml
@@ -1,12 +1,12 @@
diff --git a/core-base/common/src/androidMain/kotlin/template/core/base/common/Parcelize.android.kt b/core-base/common/src/androidMain/kotlin/template/core/base/common/Parcelize.android.kt
index ed660ef654..e7d0f5e83f 100644
--- a/core-base/common/src/androidMain/kotlin/template/core/base/common/Parcelize.android.kt
+++ b/core-base/common/src/androidMain/kotlin/template/core/base/common/Parcelize.android.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common
diff --git a/core-base/common/src/androidMain/kotlin/template/core/base/common/di/CommonModule.android.kt b/core-base/common/src/androidMain/kotlin/template/core/base/common/di/CommonModule.android.kt
index 87ebc7c260..3b59da829f 100644
--- a/core-base/common/src/androidMain/kotlin/template/core/base/common/di/CommonModule.android.kt
+++ b/core-base/common/src/androidMain/kotlin/template/core/base/common/di/CommonModule.android.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common.di
diff --git a/core-base/common/src/androidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt b/core-base/common/src/androidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt
index 0cc83839cb..8268588f6c 100644
--- a/core-base/common/src/androidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt
+++ b/core-base/common/src/androidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
@file:Suppress("ktlint:standard:filename", "MatchingDeclarationName")
diff --git a/core-base/common/src/commonMain/kotlin/template/core/base/common/DataState.kt b/core-base/common/src/commonMain/kotlin/template/core/base/common/DataState.kt
index 4cca838c97..c100832463 100644
--- a/core-base/common/src/commonMain/kotlin/template/core/base/common/DataState.kt
+++ b/core-base/common/src/commonMain/kotlin/template/core/base/common/DataState.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common
diff --git a/core-base/common/src/commonMain/kotlin/template/core/base/common/DataStateExtensions.kt b/core-base/common/src/commonMain/kotlin/template/core/base/common/DataStateExtensions.kt
index bcd36c4ce7..b33afa7ab3 100644
--- a/core-base/common/src/commonMain/kotlin/template/core/base/common/DataStateExtensions.kt
+++ b/core-base/common/src/commonMain/kotlin/template/core/base/common/DataStateExtensions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common
diff --git a/core-base/common/src/commonMain/kotlin/template/core/base/common/ImageExtension.kt b/core-base/common/src/commonMain/kotlin/template/core/base/common/ImageExtension.kt
index 6710a40b77..d58bad17b8 100644
--- a/core-base/common/src/commonMain/kotlin/template/core/base/common/ImageExtension.kt
+++ b/core-base/common/src/commonMain/kotlin/template/core/base/common/ImageExtension.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common
diff --git a/core-base/common/src/commonMain/kotlin/template/core/base/common/Parcelize.kt b/core-base/common/src/commonMain/kotlin/template/core/base/common/Parcelize.kt
index 30bd0695ab..dcbb904905 100644
--- a/core-base/common/src/commonMain/kotlin/template/core/base/common/Parcelize.kt
+++ b/core-base/common/src/commonMain/kotlin/template/core/base/common/Parcelize.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common
diff --git a/core-base/common/src/commonMain/kotlin/template/core/base/common/di/CommonModule.kt b/core-base/common/src/commonMain/kotlin/template/core/base/common/di/CommonModule.kt
index d5cc510c32..d1b0ce6db0 100644
--- a/core-base/common/src/commonMain/kotlin/template/core/base/common/di/CommonModule.kt
+++ b/core-base/common/src/commonMain/kotlin/template/core/base/common/di/CommonModule.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common.di
diff --git a/core-base/common/src/commonMain/kotlin/template/core/base/common/manager/DispatcherManager.kt b/core-base/common/src/commonMain/kotlin/template/core/base/common/manager/DispatcherManager.kt
index 219c4d9bb6..5c686d4ad2 100644
--- a/core-base/common/src/commonMain/kotlin/template/core/base/common/manager/DispatcherManager.kt
+++ b/core-base/common/src/commonMain/kotlin/template/core/base/common/manager/DispatcherManager.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common.manager
diff --git a/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/Parcelize.nonAndroid.kt b/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/Parcelize.nonAndroid.kt
index 194a86f2d8..c4070fb95e 100644
--- a/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/Parcelize.nonAndroid.kt
+++ b/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/Parcelize.nonAndroid.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common
diff --git a/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/di/CommonModule.nonAndroid.kt b/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/di/CommonModule.nonAndroid.kt
index 87ebc7c260..3b59da829f 100644
--- a/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/di/CommonModule.nonAndroid.kt
+++ b/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/di/CommonModule.nonAndroid.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.common.di
diff --git a/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt b/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt
index 1eafd16d71..3dbee647c4 100644
--- a/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt
+++ b/core-base/common/src/nonAndroidMain/kotlin/template/core/base/common/manager/DispatchManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
@file:Suppress("ktlint:standard:filename", "MatchingDeclarationName")
diff --git a/core-base/database/build.gradle.kts b/core-base/database/build.gradle.kts
index ba0d0a811c..a4c45363d0 100644
--- a/core-base/database/build.gradle.kts
+++ b/core-base/database/build.gradle.kts
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
import org.jetbrains.compose.compose
@@ -19,7 +19,7 @@ import org.jetbrains.compose.compose
* See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
- alias(libs.plugins.kmp.library.convention)
+ alias(libs.plugins.kmp.core.base.library.convention)
}
android {
diff --git a/core-base/database/src/androidMain/kotlin/template/core/base/database/AppDatabaseFactory.kt b/core-base/database/src/androidMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
index 1eb343130a..302d3bff47 100644
--- a/core-base/database/src/androidMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
+++ b/core-base/database/src/androidMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.database
diff --git a/core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt b/core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt
index 3d15839f7d..1fd6c12ebd 100644
--- a/core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt
+++ b/core-base/database/src/commonMain/kotlin/template/core/base/database/Room.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.database
diff --git a/core-base/database/src/commonMain/kotlin/template/core/base/database/TypeConverter.kt b/core-base/database/src/commonMain/kotlin/template/core/base/database/TypeConverter.kt
index cf477cb037..a7dc4d4ecd 100644
--- a/core-base/database/src/commonMain/kotlin/template/core/base/database/TypeConverter.kt
+++ b/core-base/database/src/commonMain/kotlin/template/core/base/database/TypeConverter.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.database
diff --git a/core-base/database/src/desktopMain/kotlin/template/core/base/database/AppDatabaseFactory.kt b/core-base/database/src/desktopMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
index 1302921f91..0584d68ef5 100644
--- a/core-base/database/src/desktopMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
+++ b/core-base/database/src/desktopMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.database
diff --git a/core-base/database/src/nativeMain/kotlin/template/core/base/database/AppDatabaseFactory.kt b/core-base/database/src/nativeMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
index a9dfa799ca..7b5aac1d26 100644
--- a/core-base/database/src/nativeMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
+++ b/core-base/database/src/nativeMain/kotlin/template/core/base/database/AppDatabaseFactory.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.database
diff --git a/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/Room.nonJsCommon.kt b/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/Room.nonJsCommon.kt
index 1f9608e342..9bf7f9a1d5 100644
--- a/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/Room.nonJsCommon.kt
+++ b/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/Room.nonJsCommon.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.database
diff --git a/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/TypeConverter.nonJsCommon.kt b/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/TypeConverter.nonJsCommon.kt
index e9f04fac34..a414eeed0b 100644
--- a/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/TypeConverter.nonJsCommon.kt
+++ b/core-base/database/src/nonJsCommonMain/kotlin/template/core/base/database/TypeConverter.nonJsCommon.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.database
diff --git a/core-base/designsystem/build.gradle.kts b/core-base/designsystem/build.gradle.kts
index 1cf2c93623..e2e080201a 100644
--- a/core-base/designsystem/build.gradle.kts
+++ b/core-base/designsystem/build.gradle.kts
@@ -1,14 +1,14 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
- alias(libs.plugins.kmp.library.convention)
+ alias(libs.plugins.kmp.core.base.library.convention)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}
@@ -46,4 +46,4 @@ compose.resources {
publicResClass = true
generateResClass = always
packageOfResClass = "template.core.base.designsystem.generated.resources"
-}
\ No newline at end of file
+}
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptMaterialTheme.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptMaterialTheme.kt
index 46e6efeb6e..7b4f2aa420 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptMaterialTheme.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptMaterialTheme.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptTheme.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptTheme.kt
index fd7c885964..5098f1fe0e 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptTheme.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptTheme.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptThemeExtensions.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptThemeExtensions.kt
index aa207a4771..086203c524 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptThemeExtensions.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/KptThemeExtensions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/BounceAnimation.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/BounceAnimation.kt
index 492b5974b7..ec142c486f 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/BounceAnimation.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/BounceAnimation.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.component
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptAnimationSpecs.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptAnimationSpecs.kt
index 91e5cd8a34..542f012e53 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptAnimationSpecs.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptAnimationSpecs.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.component
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptShimmerLoadingBox.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptShimmerLoadingBox.kt
index 9a10d465ef..6036c161d2 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptShimmerLoadingBox.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptShimmerLoadingBox.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.component
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptSnackbarHost.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptSnackbarHost.kt
index 2f8377602c..514744e0ca 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptSnackbarHost.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptSnackbarHost.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.component
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptTopAppBar.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptTopAppBar.kt
index 8ce1edba97..695c9a1477 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptTopAppBar.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/KptTopAppBar.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
@file:OptIn(ExperimentalMaterial3Api::class)
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/SlideTransition.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/SlideTransition.kt
index 9f09a2e80b..15d3cf880b 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/SlideTransition.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/component/SlideTransition.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.component
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/ComponentStateHolder.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/ComponentStateHolder.kt
index b7d272460f..7815df866b 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/ComponentStateHolder.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/ComponentStateHolder.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.core
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptComponent.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptComponent.kt
index f7b0098c75..94066f4ee1 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptComponent.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptComponent.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.core
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptTopAppBarConfiguration.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptTopAppBarConfiguration.kt
index 099ec14422..901e218490 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptTopAppBarConfiguration.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/core/KptTopAppBarConfiguration.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.core
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveListDetailPaneScaffold.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveListDetailPaneScaffold.kt
index 04bfd6f198..265b89367f 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveListDetailPaneScaffold.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveListDetailPaneScaffold.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableListDetailScaffold.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableListDetailScaffold.kt
index 1a2db4e8c6..3d5df14276 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableListDetailScaffold.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableListDetailScaffold.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableSupportingPaneScaffold.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableSupportingPaneScaffold.kt
index 7610d6253f..1f3444862f 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableSupportingPaneScaffold.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigableSupportingPaneScaffold.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigationSuiteScaffold.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigationSuiteScaffold.kt
index b7833c31d3..14d8183c77 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigationSuiteScaffold.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/AdaptiveNavigationSuiteScaffold.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowColumn.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowColumn.kt
index f0471733a2..8222f3ab0c 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowColumn.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowColumn.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowRow.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowRow.kt
index 24292141ec..c07c5ef026 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowRow.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptFlowRow.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptGrid.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptGrid.kt
index a76035f6dd..498edd5080 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptGrid.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptGrid.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptMasonryGrid.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptMasonryGrid.kt
index e41c721334..6ad2338a50 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptMasonryGrid.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptMasonryGrid.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptResponsiveLayout.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptResponsiveLayout.kt
index 5c4d92cde2..e1d0af5b7c 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptResponsiveLayout.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptResponsiveLayout.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSidebarLayout.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSidebarLayout.kt
index 37b8bec0da..4d1a8c395e 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSidebarLayout.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSidebarLayout.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSplitPane.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSplitPane.kt
index 23b4477c82..fbc7a2b198 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSplitPane.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptSplitPane.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptStack.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptStack.kt
index dfab808d04..545a1b4d65 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptStack.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/layout/KptStack.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.layout
diff --git a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/theme/KptColorSchemeImpl.kt b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/theme/KptColorSchemeImpl.kt
index 27eafdaf08..95972a6245 100644
--- a/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/theme/KptColorSchemeImpl.kt
+++ b/core-base/designsystem/src/commonMain/kotlin/template/core/base/designsystem/theme/KptColorSchemeImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.designsystem.theme
diff --git a/core-base/network/build.gradle.kts b/core-base/network/build.gradle.kts
index 9c8ee7a265..84d6f2983d 100644
--- a/core-base/network/build.gradle.kts
+++ b/core-base/network/build.gradle.kts
@@ -1,19 +1,18 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
- alias(libs.plugins.kmp.library.convention)
- alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kmp.core.base.library.convention)
}
android {
- namespace = "org.mifos.corebase.network"
+ namespace = "template.core.base.network"
}
kotlin {
@@ -49,4 +48,4 @@ kotlin {
api(libs.ktor.client.js)
}
}
-}
\ No newline at end of file
+}
diff --git a/core-base/network/src/androidMain/kotlin/template/core/base/network/KtorHttpClient.android.kt b/core-base/network/src/androidMain/kotlin/template/core/base/network/KtorHttpClient.android.kt
new file mode 100644
index 0000000000..65edfc3301
--- /dev/null
+++ b/core-base/network/src/androidMain/kotlin/template/core/base/network/KtorHttpClient.android.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.engine.okhttp.OkHttp
+
+actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
+ config(this)
+}
diff --git a/core-base/network/src/commonMain/kotlin/template/core/base/network/KtorHttpClient.kt b/core-base/network/src/commonMain/kotlin/template/core/base/network/KtorHttpClient.kt
new file mode 100644
index 0000000000..e9d9584f48
--- /dev/null
+++ b/core-base/network/src/commonMain/kotlin/template/core/base/network/KtorHttpClient.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.plugins.HttpTimeout
+import io.ktor.client.plugins.auth.Auth
+import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
+import io.ktor.client.plugins.auth.providers.BearerTokens
+import io.ktor.client.plugins.auth.providers.DigestAuthCredentials
+import io.ktor.client.plugins.auth.providers.basic
+import io.ktor.client.plugins.auth.providers.bearer
+import io.ktor.client.plugins.auth.providers.digest
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.defaultRequest
+import io.ktor.client.plugins.logging.DEFAULT
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logger
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.http.HttpHeaders
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.serialization.json.Json
+import co.touchlab.kermit.Logger.Companion as KermitLogger
+
+expect fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient
+
+/**
+ * Provides a default [HttpClientConfig] setup for use with a Ktor-based HTTP client.
+ *
+ * This function simplifies client configuration by handling common concerns such as:
+ * - Authentication (Bearer, Basic, Digest)
+ * - Default headers
+ * - Timeouts
+ * - Logging
+ * - JSON serialization
+ *
+ * It can be passed directly into a Ktor client builder via the `config` lambda:
+ * ```kotlin
+ * val client = httpClient(setupDefaultHttpClient(baseUrl = "https://api.example.com"))
+ * ```
+ *
+ * @param baseUrl The base URL to be applied to all requests unless explicitly overridden.
+ * @param authRequiredUrl A list of hostnames that require authentication.
+ * @param defaultHeaders Headers that are applied to every request.
+ * @param requestTimeout Timeout in milliseconds for entire request lifecycle.
+ * @param socketTimeout Timeout in milliseconds for socket-level communication.
+ * @param httpLogger A logger used for HTTP logging (defaults to `Logger.DEFAULT`).
+ * @param httpLogLevel Level of HTTP logging (e.g. `LogLevel.ALL`).
+ * @param loggableHosts A list of hostnames for which HTTP logging is enabled.
+ * @param sensitiveHeaders List of headers to be hidden in logs (defaults to Authorization).
+ * @param jsonConfig Custom [Json] configuration used by `ContentNegotiation`.
+ * @param basicCredentialsProvider Provider for Basic authentication credentials.
+ * @param digestCredentialsProvider Provider for Digest authentication credentials.
+ * @param bearerTokensProvider Provider for Bearer token authentication.
+ * @param bearerRefreshProvider Optional refresh logic for Bearer tokens (only used if Bearer auth is configured).
+ *
+ * @return A configuration lambda to be passed into the Ktor [HttpClient].
+ */
+fun setupDefaultHttpClient(
+ baseUrl: String,
+ authRequiredUrl: List = emptyList(),
+ defaultHeaders: Map = emptyMap(),
+ requestTimeout: Long = 60_000L,
+ socketTimeout: Long = 60_000L,
+ httpLogger: Logger = Logger.DEFAULT,
+ httpLogLevel: LogLevel = LogLevel.ALL,
+ loggableHosts: List = emptyList(),
+ sensitiveHeaders: List = listOf(HttpHeaders.Authorization),
+ jsonConfig: Json = Json {
+ prettyPrint = true
+ isLenient = true
+ ignoreUnknownKeys = true
+ explicitNulls = false
+ },
+ basicCredentialsProvider: (() -> BasicAuthCredentials)? = null,
+ digestCredentialsProvider: (() -> DigestAuthCredentials)? = null,
+ bearerTokensProvider: (() -> BearerTokens)? = null,
+ bearerRefreshProvider: (() -> BearerTokens)? = null,
+): HttpClientConfig<*>.() -> Unit = {
+ when {
+ bearerTokensProvider != null -> {
+ install(Auth) {
+ bearer {
+ loadTokens { bearerTokensProvider() }
+ if (bearerRefreshProvider != null) {
+ refreshTokens {
+ bearerRefreshProvider()
+ }
+ }
+ sendWithoutRequest { request ->
+ request.url.host in authRequiredUrl
+ }
+ }
+ }
+ }
+
+ basicCredentialsProvider != null -> {
+ install(Auth) {
+ basic {
+ credentials {
+ basicCredentialsProvider()
+ }
+ sendWithoutRequest { request ->
+ request.url.host in authRequiredUrl
+ }
+ }
+ }
+ }
+
+ digestCredentialsProvider != null -> {
+ install(Auth) {
+ digest {
+ credentials {
+ digestCredentialsProvider()
+ }
+ }
+ }
+ }
+ }
+
+ defaultRequest {
+ url(baseUrl)
+ defaultHeaders.forEach { (key, value) ->
+ headers.append(key, value)
+ }
+ }
+
+ install(HttpTimeout) {
+ requestTimeoutMillis = requestTimeout
+ socketTimeoutMillis = socketTimeout
+ }
+
+ install(Logging) {
+ logger = httpLogger
+ level = httpLogLevel
+ filter { request ->
+ loggableHosts.any { host ->
+ request.url.host.contains(host)
+ }
+ }
+ sanitizeHeader { header ->
+ header in sensitiveHeaders
+ }
+ logger = object : Logger {
+ override fun log(message: String) {
+ KermitLogger.d(tag = "KtorClient", messageString = message)
+ }
+ }
+ }
+
+ install(ContentNegotiation) {
+ json(jsonConfig)
+ }
+}
diff --git a/core-base/network/src/commonMain/kotlin/template/core/base/network/NetworkError.kt b/core-base/network/src/commonMain/kotlin/template/core/base/network/NetworkError.kt
new file mode 100644
index 0000000000..c8c4f52f17
--- /dev/null
+++ b/core-base/network/src/commonMain/kotlin/template/core/base/network/NetworkError.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+/**
+ * Represents standardized error types for remote or network operations.
+ *
+ * This enum is typically used with the [NetworkResult.Error] variant to describe what kind of failure occurred.
+ */
+enum class NetworkError {
+
+ /**
+ * The request was malformed or missing required parameters (HTTP 400).
+ */
+ BAD_REQUEST,
+
+ /**
+ * The requested resource could not be found (HTTP 404).
+ */
+ NOT_FOUND,
+
+ /**
+ * Authentication failed due to invalid or missing credentials (HTTP 401).
+ */
+ UNAUTHORIZED,
+
+ /**
+ * The request timed out, usually due to a slow or unresponsive network (HTTP 408 or socket timeout).
+ */
+ REQUEST_TIMEOUT,
+
+ /**
+ * The client has sent too many requests in a given amount of time (HTTP 429).
+ */
+ TOO_MANY_REQUESTS,
+
+ /**
+ * A server-side error occurred (HTTP 5xx).
+ */
+ SERVER,
+
+ /**
+ * The response could not be deserialized, likely due to mismatched or invalid data formats.
+ */
+ SERIALIZATION,
+
+ /**
+ * An unknown or unexpected error occurred, used as a fallback when the specific cause is not identifiable.
+ */
+ UNKNOWN,
+}
diff --git a/core-base/network/src/commonMain/kotlin/template/core/base/network/NetworkResult.kt b/core-base/network/src/commonMain/kotlin/template/core/base/network/NetworkResult.kt
new file mode 100644
index 0000000000..abcf6845ab
--- /dev/null
+++ b/core-base/network/src/commonMain/kotlin/template/core/base/network/NetworkResult.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+/**
+ * Represents the result of a network or remote operation, encapsulating either a success or an error.
+ *
+ * This is a sealed interface with two implementations:
+ * - [Success] indicates the operation completed successfully and contains the resulting data.
+ * - [Error] represents a failure and contains a [NetworkError] describing the error condition.
+ *
+ * @param D The type of data returned on success.
+ * @param E The type of error returned on failure, constrained to [NetworkError].
+ */
+sealed interface NetworkResult {
+
+ /**
+ * Represents a successful result.
+ *
+ * @param D The type of the successful response data.
+ * @property data The actual result of the operation.
+ */
+ data class Success(val data: D) : NetworkResult
+
+ /**
+ * Represents a failed result due to a [NetworkError].
+ *
+ * @param E The specific type of [NetworkError] encountered.
+ * @property error Details about the error that occurred.
+ */
+ data class Error(val error: E) : NetworkResult
+}
diff --git a/core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt b/core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt
new file mode 100644
index 0000000000..64f26cb878
--- /dev/null
+++ b/core-base/network/src/commonMain/kotlin/template/core/base/network/factory/ResultSuspendConverterFactory.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network.factory
+
+import de.jensklingenberg.ktorfit.Ktorfit
+import de.jensklingenberg.ktorfit.converter.Converter
+import de.jensklingenberg.ktorfit.converter.KtorfitResult
+import de.jensklingenberg.ktorfit.converter.TypeData
+import io.ktor.client.call.NoTransformationFoundException
+import io.ktor.client.call.body
+import io.ktor.client.statement.HttpResponse
+import kotlinx.serialization.SerializationException
+import template.core.base.network.NetworkError
+import template.core.base.network.NetworkResult
+
+/**
+ * A custom [Converter.Factory] for Ktorfit that provides a suspend
+ * response converter which wraps successful or error HTTP responses into a
+ * sealed [NetworkResult] type.
+ *
+ * This is useful for abstracting error handling logic across your network
+ * layer while providing strong typing for both success and failure
+ * outcomes.
+ *
+ * This converter handles:
+ * - HTTP 2xx responses by deserializing the response body into the
+ * expected type.
+ * - Known HTTP error codes like 400, 401, 404, etc., by mapping them to
+ * [NetworkError] types.
+ * - Deserialization issues via [SerializationException].
+ * - Unknown failures via [KtorfitResult.Failure].
+ *
+ * Example usage:
+ * ```kotlin
+ * interface ApiService {
+ * @GET("users")
+ * suspend fun getUsers(): Result, RemoteError>
+ * }
+ * ```
+ */
+@Suppress("NestedBlockDepth")
+class ResultSuspendConverterFactory : Converter.Factory {
+
+ /**
+ * Creates a [Converter.SuspendResponseConverter] that wraps an HTTP
+ * response into a [NetworkResult] type.
+ *
+ * @param typeData Metadata about the expected response type.
+ * @param ktorfit The [Ktorfit] instance requesting this converter.
+ * @return A [Converter.SuspendResponseConverter] if the return type is
+ * `Result`, or `null` otherwise.
+ */
+ override fun suspendResponseConverter(
+ typeData: TypeData,
+ ktorfit: Ktorfit,
+ ): Converter.SuspendResponseConverter? {
+ if (typeData.typeInfo.type == NetworkResult::class) {
+ val successType = typeData.typeArgs.first().typeInfo
+ return object :
+ Converter.SuspendResponseConverter> {
+
+ /**
+ * Converts a [KtorfitResult] into a [NetworkResult], handling success and
+ * various failure scenarios.
+ *
+ * @param result The response wrapped in [KtorfitResult].
+ * @return A [NetworkResult.Success] if the response is successful, or a
+ * [NetworkResult.Error] if an error occurred.
+ */
+ override suspend fun convert(result: KtorfitResult): NetworkResult {
+ return when (result) {
+ is KtorfitResult.Failure -> {
+ println("Failure: " + result.throwable.message)
+ NetworkResult.Error(NetworkError.UNKNOWN)
+ }
+
+ is KtorfitResult.Success -> {
+ val status = result.response.status.value
+
+ when (status) {
+ in 200..209 -> {
+ try {
+ val data = result.response.body(successType) as Any
+ NetworkResult.Success(data)
+ } catch (e: NoTransformationFoundException) {
+ NetworkResult.Error(NetworkError.SERIALIZATION)
+ } catch (e: SerializationException) {
+ println("Serialization error: ${e.message}")
+ NetworkResult.Error(NetworkError.SERIALIZATION)
+ }
+ }
+
+ 400 -> NetworkResult.Error(NetworkError.BAD_REQUEST)
+ 401 -> NetworkResult.Error(NetworkError.UNAUTHORIZED)
+ 404 -> NetworkResult.Error(NetworkError.NOT_FOUND)
+ 408 -> NetworkResult.Error(NetworkError.REQUEST_TIMEOUT)
+ 429 -> NetworkResult.Error(NetworkError.TOO_MANY_REQUESTS)
+ in 500..599 -> NetworkResult.Error(NetworkError.SERVER)
+ else -> {
+ println("Status code $status")
+ NetworkResult.Error(NetworkError.UNKNOWN)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return null
+ }
+}
diff --git a/core-base/network/src/desktopMain/kotlin/template/core/base/network/KtorHttpClient.desktop.kt b/core-base/network/src/desktopMain/kotlin/template/core/base/network/KtorHttpClient.desktop.kt
new file mode 100644
index 0000000000..65edfc3301
--- /dev/null
+++ b/core-base/network/src/desktopMain/kotlin/template/core/base/network/KtorHttpClient.desktop.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.engine.okhttp.OkHttp
+
+actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHttp) {
+ config(this)
+}
diff --git a/core-base/network/src/jsMain/kotlin/template/core/base/network/KtorHttpClient.js.kt b/core-base/network/src/jsMain/kotlin/template/core/base/network/KtorHttpClient.js.kt
new file mode 100644
index 0000000000..beaa36ea3a
--- /dev/null
+++ b/core-base/network/src/jsMain/kotlin/template/core/base/network/KtorHttpClient.js.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.engine.js.Js
+
+actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(Js) {
+ config(this)
+}
diff --git a/core-base/network/src/nativeMain/kotlin/template/core/base/network/KtorHttpClient.native.kt b/core-base/network/src/nativeMain/kotlin/template/core/base/network/KtorHttpClient.native.kt
new file mode 100644
index 0000000000..3f6c9a4d4e
--- /dev/null
+++ b/core-base/network/src/nativeMain/kotlin/template/core/base/network/KtorHttpClient.native.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.engine.darwin.Darwin
+
+actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(Darwin) {
+ config(this)
+}
diff --git a/core-base/network/src/wasmJsMain/kotlin/template/core/base/network/KtorHttpClient.wasmJs.kt b/core-base/network/src/wasmJsMain/kotlin/template/core/base/network/KtorHttpClient.wasmJs.kt
new file mode 100644
index 0000000000..beaa36ea3a
--- /dev/null
+++ b/core-base/network/src/wasmJsMain/kotlin/template/core/base/network/KtorHttpClient.wasmJs.kt
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
+ */
+package template.core.base.network
+
+import io.ktor.client.HttpClient
+import io.ktor.client.HttpClientConfig
+import io.ktor.client.engine.js.Js
+
+actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(Js) {
+ config(this)
+}
diff --git a/core-base/platform/build.gradle.kts b/core-base/platform/build.gradle.kts
index c12ae05b54..2c49f22cd3 100644
--- a/core-base/platform/build.gradle.kts
+++ b/core-base/platform/build.gradle.kts
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
import org.gradle.kotlin.dsl.implementation
@@ -19,10 +19,9 @@ import org.gradle.kotlin.dsl.implementation
* See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
- alias(libs.plugins.kmp.library.convention)
+ alias(libs.plugins.kmp.core.base.library.convention)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
- alias(libs.plugins.kotlin.parcelize)
}
android {
@@ -58,4 +57,4 @@ kotlin {
implementation(libs.app.update)
}
}
-}
\ No newline at end of file
+}
diff --git a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/LocalManagerProviders.android.kt b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/LocalManagerProviders.android.kt
index 806fae55b2..5659bb404f 100644
--- a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/LocalManagerProviders.android.kt
+++ b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/LocalManagerProviders.android.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform
diff --git a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/context/AppContext.android.kt b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/context/AppContext.android.kt
index b6b7b976f7..7c80cabff4 100644
--- a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/context/AppContext.android.kt
+++ b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/context/AppContext.android.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.context
diff --git a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.android.kt b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.android.kt
index 66f0cf8333..8fd69b6563 100644
--- a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.android.kt
+++ b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.android.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.garbage
diff --git a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt
index ca9e21a117..e3f04b245b 100644
--- a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt
+++ b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.intent
diff --git a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt
index e193988313..40d0e277e4 100644
--- a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt
+++ b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.review
diff --git a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt
index 6ced78e073..f78a494082 100644
--- a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt
+++ b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.update
diff --git a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/utils/AndroidBuildUtils.kt b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/utils/AndroidBuildUtils.kt
index ad28d97348..fdd9a9f3d2 100644
--- a/core-base/platform/src/androidMain/kotlin/template/core/base/platform/utils/AndroidBuildUtils.kt
+++ b/core-base/platform/src/androidMain/kotlin/template/core/base/platform/utils/AndroidBuildUtils.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.utils
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/LocalManagerProviders.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/LocalManagerProviders.kt
index a292ebcf41..4a78fdce69 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/LocalManagerProviders.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/LocalManagerProviders.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform
@@ -42,7 +42,7 @@ expect fun LocalManagerProvider(
* Provides access to the app review manager throughout the app.
*/
val LocalAppReviewManager: ProvidableCompositionLocal = compositionLocalOf {
- NoOpAppReviewManager
+ error("CompositionLocal AppReviewManager not present")
}
/**
@@ -58,8 +58,3 @@ val LocalIntentManager: ProvidableCompositionLocal = compositionL
val LocalAppUpdateManager: ProvidableCompositionLocal = compositionLocalOf {
error("CompositionLocal LocalAppUpdateManager not present")
}
-
-object NoOpAppReviewManager : AppReviewManager {
- override fun promptForReview() = Unit
- override fun promptForCustomReview() = Unit
-}
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/context/AppContext.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/context/AppContext.kt
index 16f93abb92..cdcd49f8ed 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/context/AppContext.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/context/AppContext.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.context
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/di/PlatformModule.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/di/PlatformModule.kt
index 5d82e77376..35db29eda9 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/di/PlatformModule.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/di/PlatformModule.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.di
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.kt
index 1ee533e5f4..961d451fd1 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.garbage
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManagerImpl.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManagerImpl.kt
index a0f51fe982..73ba123129 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManagerImpl.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.garbage
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/intent/IntentManager.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/intent/IntentManager.kt
index 8dd3abc238..034fb6c9fd 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/intent/IntentManager.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/intent/IntentManager.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.intent
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/model/MimeType.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/model/MimeType.kt
index f5bc7acfaa..f0340940e1 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/model/MimeType.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/model/MimeType.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.model
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/review/AppReviewManager.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/review/AppReviewManager.kt
index 47671a8433..0d6e8de4e5 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/review/AppReviewManager.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/review/AppReviewManager.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.review
diff --git a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/update/AppUpdateManager.kt b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/update/AppUpdateManager.kt
index 29fb0943a2..623381e4b0 100644
--- a/core-base/platform/src/commonMain/kotlin/template/core/base/platform/update/AppUpdateManager.kt
+++ b/core-base/platform/src/commonMain/kotlin/template/core/base/platform/update/AppUpdateManager.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.update
diff --git a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/LocalManagerProviders.native.kt b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/LocalManagerProviders.native.kt
index 2b1486d98e..b824fbc868 100644
--- a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/LocalManagerProviders.native.kt
+++ b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/LocalManagerProviders.native.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform
diff --git a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/context/AppContext.native.kt b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/context/AppContext.native.kt
index 12beabce96..f578dcb253 100644
--- a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/context/AppContext.native.kt
+++ b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/context/AppContext.native.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.context
diff --git a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.nonAndroid.kt b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.nonAndroid.kt
index 97ecbc9f52..2be8bc385c 100644
--- a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.nonAndroid.kt
+++ b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/garbage/GarbageCollectionManager.nonAndroid.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.garbage
diff --git a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt
index c4d8429264..c7da0f592f 100644
--- a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt
+++ b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/intent/IntentManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.intent
diff --git a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt
index 38bde39e89..1a3f6d7d21 100644
--- a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt
+++ b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/review/AppReviewManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.review
diff --git a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt
index 5257ad880b..eb4fc27310 100644
--- a/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt
+++ b/core-base/platform/src/nonAndroidMain/kotlin/template/core/base/platform/update/AppUpdateManagerImpl.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.platform.update
diff --git a/core-base/ui/README.md b/core-base/ui/README.md
index eec63d0235..91ec552b51 100644
--- a/core-base/ui/README.md
+++ b/core-base/ui/README.md
@@ -381,29 +381,6 @@ internal fun rememberDefaultImageLoader(context: PlatformContext): ImageLoader {
The `maxSizePercent` call is crucialβit adapts the cache size to the device's available memory,
ensuring efficient resource usage across a wide range of devices.
-### Request Optimization
-
-The image request builder includes memory cache optimization:
-
-```kotlin
-@Composable
-fun rememberImageRequest(
- context: PlatformContext,
- wallpaper: String,
-): ImageRequest {
- return remember(wallpaper) {
- ImageRequest.Builder(context)
- .data(wallpaper)
- .memoryCacheKey(wallpaper)
- .placeholderMemoryCacheKey(wallpaper)
- .build()
- }
-}
-```
-
-The use of `memoryCacheKey` and `placeholderMemoryCacheKey` ensures that images with the same URL
-share the same cache entry, reducing memory usage and improving load times.
-
### Common Use Patterns
For profile pictures and avatars:
@@ -411,8 +388,7 @@ For profile pictures and avatars:
```kotlin
@Composable
fun CircularProfileImage(url: String, size: Dp = 48.dp) {
- val context = LocalPlatformContext.current
- val imageLoader = rememberImageLoader(context)
+ val imageLoader = rememberImageLoader()
AsyncImage(
model = rememberImageRequest(context, url),
@@ -434,8 +410,7 @@ For background images:
```kotlin
@Composable
fun BackgroundImage(url: String, overlay: Color = Color.Black.copy(alpha = 0.3f)) {
- val context = LocalPlatformContext.current
- val imageLoader = rememberImageLoader(context)
+ val imageLoader = rememberImageLoader()
Box {
AsyncImage(
diff --git a/core-base/ui/build.gradle.kts b/core-base/ui/build.gradle.kts
index 1270cdaccd..ef39c5eac6 100644
--- a/core-base/ui/build.gradle.kts
+++ b/core-base/ui/build.gradle.kts
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
@@ -19,7 +19,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
* See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
plugins {
- alias(libs.plugins.kmp.library.convention)
+ alias(libs.plugins.kmp.core.base.library.convention)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}
@@ -79,4 +79,4 @@ compose.resources {
publicResClass = true
generateResClass = always
packageOfResClass = "template.core.base.ui.generated.resources"
-}
\ No newline at end of file
+}
diff --git a/core-base/ui/src/androidMain/kotlin/template/core/base/ui/JankStatsExtensions.kt b/core-base/ui/src/androidMain/kotlin/template/core/base/ui/JankStatsExtensions.kt
index de96f7e490..7a67823985 100644
--- a/core-base/ui/src/androidMain/kotlin/template/core/base/ui/JankStatsExtensions.kt
+++ b/core-base/ui/src/androidMain/kotlin/template/core/base/ui/JankStatsExtensions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ReportDrawnExt.android.kt b/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ReportDrawnExt.android.kt
index e8d0e5e0d7..de004552f2 100644
--- a/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ReportDrawnExt.android.kt
+++ b/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ReportDrawnExt.android.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt b/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt
index 213a894731..64409d74da 100644
--- a/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt
+++ b/core-base/ui/src/androidMain/kotlin/template/core/base/ui/ShareUtils.android.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
@@ -14,10 +14,12 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
+import android.provider.Settings
import android.util.Log
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.content.FileProvider
+import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.ExperimentalResourceApi
@@ -102,4 +104,85 @@ actual object ShareUtils {
}
}
}
+
+ actual fun openUrl(url: String) {
+ val context = ShareUtils.activityProvider.invoke().application.baseContext
+ val uri = url.let { url.toUri() }
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ data = uri
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ }
+
+ actual fun openAppInfo() {
+ val context = activityProvider.invoke().application.baseContext
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = Uri.parse("package:${context.packageName}")
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ }
+
+ actual fun callPhone(number: String) {
+ val context = activityProvider.invoke().application.baseContext
+ val uri = Uri.parse("tel:$number")
+ val intent = Intent(Intent.ACTION_DIAL).apply {
+ data = uri
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ }
+
+ actual fun sendEmail(to: String, subject: String?, body: String?) {
+ val context = activityProvider.invoke().application.baseContext
+ val uriBuilder = StringBuilder("mailto:").append(to)
+ val query = mutableListOf()
+ subject?.let { query.add("subject=" + Uri.encode(it)) }
+ body?.let { query.add("body=" + Uri.encode(it)) }
+ if (query.isNotEmpty()) {
+ uriBuilder.append("?").append(query.joinToString("&"))
+ }
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse(uriBuilder.toString())
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ }
+
+ actual fun sendViaSMS(number: String, message: String) {
+ val context = activityProvider.invoke().application.baseContext
+ val uri = if (number.isNotEmpty()) {
+ Uri.parse("sms:$number")
+ } else {
+ Uri.parse("sms:")
+ }
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = uri
+ putExtra("sms_body", message)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ }
+
+ actual fun copyText(text: String) {
+ val context = activityProvider.invoke().application.baseContext
+ val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
+ val clip = android.content.ClipData.newPlainText("Copied Text", text)
+ clipboardManager.setPrimaryClip(clip)
+ }
+
+ actual suspend fun shareApp(storeLink: String, message: String) {
+ val shareContent = if (message.isNotEmpty()) {
+ "$message\n$storeLink"
+ } else {
+ storeLink
+ }
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, shareContent)
+ }
+ val intentChooser = Intent.createChooser(intent, null)
+ activityProvider.invoke().startActivity(intentChooser)
+ }
}
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BackgroundEvent.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BackgroundEvent.kt
index f58c3ec641..5a54d833e8 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BackgroundEvent.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BackgroundEvent.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt
index 9604926e8b..2b4b8550ab 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/BaseViewModel.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/EventsEffect.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/EventsEffect.kt
index 766385819f..0a460c0299 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/EventsEffect.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/EventsEffect.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ImageLoaderExt.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ImageLoaderExt.kt
index 9846ea66f2..e5b5d40c63 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ImageLoaderExt.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ImageLoaderExt.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
@@ -15,78 +15,60 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import coil3.ImageLoader
import coil3.PlatformContext
+import coil3.compose.LocalPlatformContext
import coil3.memory.MemoryCache
-import coil3.request.ImageRequest
+import coil3.request.CachePolicy
import coil3.util.DebugLogger
import io.github.vinceglb.filekit.coil.addPlatformFileSupport
/**
- * CompositionLocal that provides access to an [ImageLoader] within the composition hierarchy.
- * Default value is null, requiring an explicit provider or fallback to default implementation.
- */
-internal val LocalAppImageLoader = compositionLocalOf { null }
-
-/**
- * Returns an [ImageLoader] from the current composition or creates a default one if none is available.
+ * A CompositionLocal instance used to provide a shared [ImageLoader] across the composable hierarchy.
+ * This is useful for performing image loading and caching within Jetpack Compose UIs.
*
- * @param context Platform context required for image loading
- * @return An [ImageLoader] instance that can be used for image loading operations
+ * If no [ImageLoader] is provided within the context, an error will be thrown when accessing this CompositionLocal.
*/
-@Composable
-fun rememberImageLoader(context: PlatformContext): ImageLoader {
- return LocalAppImageLoader.current ?: rememberDefaultImageLoader(context)
+internal val LocalAppImageLoader = compositionLocalOf {
+ error("No ImageLoader provided. Have you forgotten the LocalImageLoaderProvider?")
}
/**
- * Creates and remembers a default [ImageLoader] with memory cache and debug logging.
+ * Creates and remembers an instance of the default ImageLoader configured with platform-specific
+ * settings for caching and logging. The ImageLoader is initialized using the current platform context.
*
- * @param context Platform context required for image loading and memory calculations
- * @return A default configured [ImageLoader] instance
+ * @return A remembered instance of the default ImageLoader configured with common settings.
*/
@Composable
-internal fun rememberDefaultImageLoader(context: PlatformContext): ImageLoader {
- return remember(context) {
- ImageLoader.Builder(context)
- .memoryCache {
- MemoryCache.Builder()
- .maxSizePercent(context, 0.25)
- .build()
- }
- .logger(DebugLogger())
- .components {
- addPlatformFileSupport()
- }
- .build()
- }
+fun rememberImageLoader(): ImageLoader {
+ val context = LocalPlatformContext.current
+ return remember(context) { getDefaultImageLoader(context) }
}
/**
- * Creates and remembers an [ImageRequest] for loading the specified wallpaper.
+ * Creates and returns a default instance of an ImageLoader configured with common settings such as
+ * logging, caching policies, memory caching, and platform-specific component support.
*
- * @param context Platform context required for the image request
- * @param wallpaper String identifier for the wallpaper to be loaded
- * @return An [ImageRequest] configured for the specified wallpaper
+ * @param context The platform-specific context required to initialize the ImageLoader
+ * @return A configured ImageLoader instance ready for use
*/
-@Composable
-fun rememberImageRequest(
- context: PlatformContext,
- wallpaper: String,
-): ImageRequest {
- return remember(wallpaper) {
- ImageRequest.Builder(context)
- .data(wallpaper)
- .memoryCacheKey(wallpaper)
- .placeholderMemoryCacheKey(wallpaper)
+fun getDefaultImageLoader(context: PlatformContext): ImageLoader = ImageLoader
+ .Builder(context)
+ .logger(DebugLogger())
+ .networkCachePolicy(CachePolicy.ENABLED)
+ .memoryCachePolicy(CachePolicy.ENABLED)
+ .memoryCache {
+ MemoryCache.Builder()
+ .maxSizePercent(context, 0.25)
.build()
}
-}
+ .components {
+ addPlatformFileSupport()
+ }.build()
/**
- * Provides the specified [ImageLoader] to all composables within the [content] lambda
- * via [CompositionLocalProvider].
+ * Provides a composable local context for an `ImageLoader` to be used within a Composable hierarchy.
*
- * @param imageLoader The [ImageLoader] to provide downstream
- * @param content The composable content that will have access to the provided [ImageLoader]
+ * @param imageLoader The `ImageLoader` instance to be provided to the local composition.
+ * @param content A composable lambda function representing the UI content that can access the provided `ImageLoader`.
*/
@Composable
fun LocalImageLoaderProvider(imageLoader: ImageLoader, content: @Composable () -> Unit) {
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/JankStatsExtension.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/JankStatsExtension.kt
index 18e4a01cfd..aed073cfe6 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/JankStatsExtension.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/JankStatsExtension.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/LifecycleEventEffect.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/LifecycleEventEffect.kt
index 38462d703d..142019344f 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/LifecycleEventEffect.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/LifecycleEventEffect.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/NavGraphBuilderExtensions.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/NavGraphBuilderExtensions.kt
index c648f92764..0b69a46a61 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/NavGraphBuilderExtensions.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/NavGraphBuilderExtensions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ReportDrawnExt.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ReportDrawnExt.kt
index 1eed52d4ae..a51c76d511 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ReportDrawnExt.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ReportDrawnExt.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ShareUtils.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ShareUtils.kt
index 7e8d6bf4be..04e36156cf 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ShareUtils.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/ShareUtils.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
@@ -39,4 +39,61 @@ expect object ShareUtils {
* @param byte The raw image data as ByteArray
*/
suspend fun shareImage(title: String, byte: ByteArray)
+
+ /**
+ * Opens the specified URL in the device's default web browser.
+ *
+ * @param url The URL to open.
+ */
+ fun openUrl(url: String)
+
+ /**
+ * Opens the application info screen in the device settings.
+ *
+ * Typically used to allow users to manage app permissions,
+ * storage, or other app-specific settings.
+ */
+ fun openAppInfo()
+
+ /**
+ * Initiates a phone call using the platform dialer UI.
+ *
+ * @param number The phone number to dial, digits only or including country code.
+ */
+ fun callPhone(number: String)
+
+ /**
+ * Opens the platform email composer with the given parameters.
+ *
+ * @param to Recipient email address
+ * @param subject Optional subject line
+ * @param body Optional email body
+ */
+ fun sendEmail(to: String, subject: String? = null, body: String? = null)
+
+ /**
+ * Opens the platform SMS composer with the given parameters.
+ *
+ * @param number Recipient phone number (can be empty to let user choose)
+ * @param message The SMS message body
+ */
+ fun sendViaSMS(number: String, message: String)
+
+ /**
+ * Copies the given text to the system clipboard.
+ *
+ * @param text The text to copy to clipboard
+ */
+ fun copyText(text: String)
+
+ /**
+ * Shares the app store link with a custom message.
+ *
+ * Opens the platform share sheet with the store link and message combined,
+ * allowing users to share the app with others via various apps.
+ *
+ * @param storeLink The app store URL (Play Store, App Store, etc.)
+ * @param message Optional promotional message to include with the link
+ */
+ suspend fun shareApp(storeLink: String, message: String = "")
}
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/SharedElementExt.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/SharedElementExt.kt
index c683352066..f0ec8aa820 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/SharedElementExt.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/SharedElementExt.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
@file:OptIn(ExperimentalSharedTransitionApi::class)
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/StringExt.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/StringExt.kt
index b1c82e85c1..bc0f0bc115 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/StringExt.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/StringExt.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/Transition.kt b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/Transition.kt
index a3fa461064..875ef57169 100644
--- a/core-base/ui/src/commonMain/kotlin/template/core/base/ui/Transition.kt
+++ b/core-base/ui/src/commonMain/kotlin/template/core/base/ui/Transition.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt b/core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt
index 2d64f7761b..509e2d591d 100644
--- a/core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt
+++ b/core-base/ui/src/desktopMain/java/template/core/base/ui/ShareUtils.desktop.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
@@ -13,6 +13,8 @@ import androidx.compose.ui.graphics.asSkiaBitmap
import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.saveImageToGallery
import kotlinx.coroutines.DelicateCoroutinesApi
+import java.awt.Desktop
+import java.net.URI
actual object ShareUtils {
@OptIn(DelicateCoroutinesApi::class)
@@ -41,4 +43,80 @@ actual object ShareUtils {
filename = "$title.png",
)
}
+
+ actual fun openUrl(url: String) {
+ try {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+ Desktop.getDesktop().browse(URI(url))
+ }
+ } catch (e: Exception) {
+ println("Error opening URL: ${e.message}")
+ }
+ }
+
+ actual fun openAppInfo() {
+ // Not applicable on Desktop; no-op
+ }
+
+ actual fun callPhone(number: String) {
+ try {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+ Desktop.getDesktop().browse(URI("tel:$number"))
+ }
+ } catch (e: Exception) {
+ println("Error opening dialer: ${e.message}")
+ }
+ }
+
+ actual fun sendEmail(to: String, subject: String?, body: String?) {
+ val q = mutableListOf()
+ subject?.let { q.add("subject=" + java.net.URLEncoder.encode(it, Charsets.UTF_8)) }
+ body?.let { q.add("body=" + java.net.URLEncoder.encode(it, Charsets.UTF_8)) }
+ val query = if (q.isNotEmpty()) "?" + q.joinToString("&") else ""
+ val mailto = "mailto:$to$query"
+ try {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+ Desktop.getDesktop().browse(URI(mailto))
+ }
+ } catch (e: Exception) {
+ println("Error opening email client: ${e.message}")
+ }
+ }
+
+ actual fun sendViaSMS(number: String, message: String) {
+ val encodedMessage = java.net.URLEncoder.encode(message, Charsets.UTF_8)
+ val smsUrl = if (number.isNotEmpty()) {
+ "sms:$number?body=$encodedMessage"
+ } else {
+ "sms:?body=$encodedMessage"
+ }
+ try {
+ if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+ Desktop.getDesktop().browse(URI(smsUrl))
+ }
+ } catch (e: Exception) {
+ println("Error opening SMS: ${e.message}")
+ }
+ }
+
+ actual fun copyText(text: String) {
+ try {
+ val clipboard = java.awt.Toolkit.getDefaultToolkit().systemClipboard
+ val stringSelection = java.awt.datatransfer.StringSelection(text)
+ clipboard.setContents(stringSelection, null)
+ } catch (e: Exception) {
+ println("Error copying to clipboard: ${e.message}")
+ }
+ }
+
+ actual suspend fun shareApp(storeLink: String, message: String) {
+ val shareContent = if (message.isNotEmpty()) {
+ "$message\n$storeLink"
+ } else {
+ storeLink
+ }
+ // On Desktop, we copy to clipboard and open the URL
+ copyText(shareContent)
+ openUrl(storeLink)
+ }
}
diff --git a/core-base/ui/src/jsCommonMain/kotlin/template/core/base/ui/ShareUtils.kt b/core-base/ui/src/jsCommonMain/kotlin/template/core/base/ui/ShareUtils.kt
index 45879017ff..e4bd6ec9d8 100644
--- a/core-base/ui/src/jsCommonMain/kotlin/template/core/base/ui/ShareUtils.kt
+++ b/core-base/ui/src/jsCommonMain/kotlin/template/core/base/ui/ShareUtils.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
@@ -42,4 +42,34 @@ actual object ShareUtils {
fileName = "$title.png",
)
}
+
+ actual fun openUrl(url: String) {
+ }
+
+ actual fun openAppInfo() {
+ }
+
+ actual fun callPhone(number: String) {
+ }
+
+ actual fun sendEmail(to: String, subject: String?, body: String?) {
+ }
+
+ actual fun sendViaSMS(number: String, message: String) {
+ }
+
+ actual fun copyText(text: String) {
+ }
+
+ actual suspend fun shareApp(storeLink: String, message: String) {
+ val shareContent = if (message.isNotEmpty()) {
+ "$message\n$storeLink"
+ } else {
+ storeLink
+ }
+ FileKit.download(
+ bytes = shareContent.encodeToByteArray(),
+ fileName = "share_app.txt",
+ )
+ }
}
diff --git a/core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt b/core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt
index 7ca96fe537..ad5a11dd8d 100644
--- a/core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt
+++ b/core-base/ui/src/nativeMain/kotlin/template/core/base/ui/ShareUtils.native.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
@@ -13,10 +13,25 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asSkiaBitmap
import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.saveImageToGallery
+import platform.Foundation.NSCharacterSet
+import platform.Foundation.NSString
+import platform.Foundation.NSURL
+import platform.Foundation.URLQueryAllowedCharacterSet
+import platform.Foundation.stringByAddingPercentEncodingWithAllowedCharacters
import platform.UIKit.UIActivityViewController
import platform.UIKit.UIApplication
+import platform.UIKit.UIApplicationOpenSettingsURLString
+@Suppress("CAST_NEVER_SUCCEEDS")
actual object ShareUtils {
+
+ private fun String.urlEncode(): String {
+ val nsString = this as NSString
+ return nsString.stringByAddingPercentEncodingWithAllowedCharacters(
+ NSCharacterSet.URLQueryAllowedCharacterSet,
+ ) ?: this
+ }
+
actual suspend fun shareText(text: String) {
val currentViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
val activityViewController = UIActivityViewController(listOf(text), null)
@@ -42,4 +57,75 @@ actual object ShareUtils {
filename = "$title.png",
)
}
+
+ actual fun openUrl(url: String) {
+ val nsUrl = NSURL.URLWithString(url)
+ if (nsUrl != null) {
+ UIApplication.sharedApplication.openURL(
+ nsUrl,
+ options = emptyMap(),
+ completionHandler = null,
+ )
+ }
+ }
+
+ actual fun openAppInfo() {
+ val url = NSURL.URLWithString(UIApplicationOpenSettingsURLString)
+ if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
+ UIApplication.sharedApplication.openURL(url)
+ }
+ }
+
+ actual fun callPhone(number: String) {
+ val url = NSURL.URLWithString("tel:$number")
+ if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
+ UIApplication.sharedApplication.openURL(url)
+ }
+ }
+
+ actual fun sendEmail(to: String, subject: String?, body: String?) {
+ val encodedSubject = subject?.urlEncode() ?: ""
+ val encodedBody = body?.urlEncode() ?: ""
+ val query = buildList {
+ if (encodedSubject.isNotEmpty()) add("subject=$encodedSubject")
+ if (encodedBody.isNotEmpty()) add("body=$encodedBody")
+ }.joinToString("&")
+ val mailto = if (query.isNotEmpty()) "mailto:$to?$query" else "mailto:$to"
+ val url = NSURL.URLWithString(mailto)
+ if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
+ UIApplication.sharedApplication.openURL(url)
+ }
+ }
+
+ actual fun sendViaSMS(number: String, message: String) {
+ val encodedMessage = message.urlEncode()
+ val smsUrl = if (number.isNotEmpty()) {
+ "sms:$number&body=$encodedMessage"
+ } else {
+ "sms:&body=$encodedMessage"
+ }
+ val url = NSURL.URLWithString(smsUrl)
+ if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {
+ UIApplication.sharedApplication.openURL(url)
+ }
+ }
+
+ actual fun copyText(text: String) {
+ platform.UIKit.UIPasteboard.generalPasteboard.string = text
+ }
+
+ actual suspend fun shareApp(storeLink: String, message: String) {
+ val shareContent = if (message.isNotEmpty()) {
+ "$message\n$storeLink"
+ } else {
+ storeLink
+ }
+ val currentViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
+ val activityViewController = UIActivityViewController(listOf(shareContent), null)
+ currentViewController?.presentViewController(
+ viewControllerToPresent = activityViewController,
+ animated = true,
+ completion = null,
+ )
+ }
}
diff --git a/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/JankStatsExtension.jvmJs.kt b/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/JankStatsExtension.jvmJs.kt
index 9ae9160029..6cb9e5877f 100644
--- a/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/JankStatsExtension.jvmJs.kt
+++ b/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/JankStatsExtension.jvmJs.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/ReportDrawnExt.jvmJs.kt b/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/ReportDrawnExt.jvmJs.kt
index 233d6b119e..27ee6131e1 100644
--- a/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/ReportDrawnExt.jvmJs.kt
+++ b/core-base/ui/src/nonAndroidMain/kotlin/template/core/base/ui/ReportDrawnExt.jvmJs.kt
@@ -1,11 +1,11 @@
/*
- * Copyright 2026 Mifos Initiative
+ * Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
- * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
+ * See See https://github.com/openMF/kmp-project-template/blob/main/LICENSE
*/
package template.core.base.ui
diff --git a/fastlane/AppFile b/fastlane/AppFile
index 76cfee0160..a355ac5aba 100644
--- a/fastlane/AppFile
+++ b/fastlane/AppFile
@@ -1,2 +1,12 @@
-json_key_file("cmp-android/playStorePublishServiceCredentialsFile.json")
-package_name("org.mifos.mobile") # e.g. org.mifos.mobile
\ No newline at end of file
+# Load project configuration using absolute path
+# require_relative doesn't work in AppFile due to eval context
+project_dir = File.expand_path('..', __dir__)
+require File.join(project_dir, 'fastlane-config', 'project_config')
+
+# Android configuration
+json_key_file(FastlaneConfig::ProjectConfig::ANDROID[:play_store_json_key])
+package_name(FastlaneConfig::ProjectConfig.android_package_name)
+
+# iOS configuration
+apple_id(FastlaneConfig::ProjectConfig::IOS[:app_identifier])
+team_id(FastlaneConfig::ProjectConfig::IOS[:team_id])
diff --git a/fastlane/FastFile b/fastlane/FastFile
index 929a490017..fda9f2df6c 100644
--- a/fastlane/FastFile
+++ b/fastlane/FastFile
@@ -36,7 +36,7 @@ platform :android do
generateVersion = generateVersion()
buildAndSignApp(
- taskName: "bundle",
+ taskName: "assemble",
buildType: "Release",
**signing_config
)
@@ -324,80 +324,108 @@ platform :ios do
# Shared Private Lane Helpers
#############################
- private_lane :setup_ci_if_needed do
- unless ENV['CI']
- UI.message("π₯οΈ Running locally, skipping CI-specific setup.")
- else
- setup_ci
- end
+ private_lane :setup_ci_if_needed do |options|
+ unless ENV['CI']
+ UI.message("π₯οΈ Running locally, skipping CI-specific setup.")
+ else
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ setup_ci(
+ provider: options[:ci_provider] || ios_config[:ci_provider]
+ )
+ end
end
private_lane :load_api_key do |options|
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
- app_store_connect_api_key(
- key_id: options[:appstore_key_id] || ios_config[:key_id],
- issuer_id: options[:appstore_issuer_id] || ios_config[:issuer_id],
- key_filepath: options[:key_filepath] || ios_config[:key_filepath],
- duration: 1200
- )
+ app_store_connect_api_key(
+ key_id: options[:appstore_key_id] || ios_config[:key_id],
+ issuer_id: options[:appstore_issuer_id] || ios_config[:issuer_id],
+ key_filepath: options[:key_filepath] || ios_config[:key_filepath],
+ duration: 1200
+ )
end
private_lane :fetch_certificates_with_match do |options|
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
- match(
- type: options[:match_type] || ios_config[:match_type],
- app_identifier: options[:app_identifier] || ios_config[:app_identifier],
- readonly: true,
- git_url: options[:git_url] || ios_config[:git_url],
- git_branch: options[:git_branch] || ios_config[:git_branch],
- git_private_key: options[:git_private_key] || ios_config[:match_git_private_key],
- force_for_new_devices: true,
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
- )
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ match(
+ type: options[:match_type] || ios_config[:match_type],
+ app_identifier: options[:app_identifier] || ios_config[:app_identifier],
+ readonly: false,
+ git_url: options[:git_url] || ios_config[:git_url],
+ git_branch: options[:git_branch] || ios_config[:git_branch],
+ git_private_key: options[:git_private_key] || ios_config[:match_git_private_key],
+ force_for_new_devices: true,
+ api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
+ )
end
private_lane :build_ios_project do |options|
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
- app_identifier = options[:app_identifier] || ios_config[:app_identifier]
- provisioning_profile_name = options[:provisioning_profile_name] || ios_config[:provisioning_profile_name]
-
- cocoapods(
- podfile: ios_config[:podfile_path],
- clean_install: true,
- repo_update: true
- )
-
- # Manual signing for your main app target
- update_code_signing_settings(
- use_automatic_signing: false,
- path: ios_config[:project_path],
- targets: [ios_config[:target]],
- team_id: ios_config[:team_id],
- code_sign_identity: ios_config[:code_sign_identity],
- profile_name: provisioning_profile_name,
- bundle_identifier: app_identifier
- )
-
- build_ios_app(
- scheme: ios_config[:scheme],
- workspace: ios_config[:workspace_path],
- output_name: ios_config[:output_name],
- output_directory: ios_config[:output_directory],
- configuration: ios_config[:configuration]
- )
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ app_identifier = options[:app_identifier] || ios_config[:app_identifier]
+ provisioning_profile_name = options[:provisioning_profile_name] || ios_config[:provisioning_profile_name]
+ team_id = options[:team_id] || ios_config[:team_id]
+
+ build_ios_app(
+ scheme: ios_config[:scheme],
+ workspace: ios_config[:workspace_path],
+ output_name: ios_config[:output_name],
+ output_directory: ios_config[:output_directory],
+ export_options: {
+ provisioningProfiles: {
+ app_identifier => provisioning_profile_name
+ }
+ },
+ xcargs: "CODE_SIGN_STYLE=Manual CODE_SIGN_IDENTITY=\"Apple Distribution\" DEVELOPMENT_TEAM=#{team_id} PROVISIONING_PROFILE_SPECIFIER=\"#{provisioning_profile_name}\""
+ )
end
- private_lane :set_plist_values do
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
- update_plist(
- plist_path: ios_config[:plist_path],
- block: proc do |plist|
- plist["ITSAppUsesNonExemptEncryption"] = false
- plist["NSCameraUsageDescription"] = "We use the camera to scan QR codes for payments and to add beneficiaries. No images or video are stored."
- plist["NSPhotoLibraryUsageDescription"] = "Allow access to choose a photo or document you decide to upload (e.g., profile photo or ID)."
+ private_lane :get_version_from_gradle do |options|
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+
+ # Generate version.txt from gradle
+ gradle(tasks: ["versionFile"])
+ full_version = File.read("../version.txt").strip rescue ios_config[:version_number]
+
+ # Sanitize for App Store if needed
+ # App Store requires: MAJOR.MINOR.PATCH (1-3 period-separated integers)
+ if options[:sanitize_for_appstore]
+ # Extract commit count from version
+ # Format: 2026.1.1-beta.0.9 β Extract year, month, and commit count
+ # Transform to: 2026.1.9 (Year.Month.CommitCount)
+
+ base_version = full_version.split('-')[0].split('+')[0] # Get 2026.1.1
+ parts = base_version.split('.')
+
+ # Extract commit count from pre-release identifier
+ # -beta.0.9 β extract 9
+ # -rc.0.15 β extract 15
+ commit_count = "0"
+ if full_version.include?('-')
+ pre_release = full_version.split('-')[1].split('+')[0] # Get "beta.0.9"
+ commit_parts = pre_release.split('.')
+ commit_count = commit_parts[-1] if commit_parts.length > 0 # Get last number (9)
+ end
+
+ # Construct App Store version: Year.Month.CommitCount
+ if parts.length >= 2
+ version = "#{parts[0]}.#{parts[1]}.#{commit_count}"
+ else
+ # Fallback if version format is unexpected
+ version = base_version
+ end
+
+ UI.important("π± Full version from Gradle: #{full_version}")
+ UI.important("π± App Store compatible version: #{version}")
+ UI.important(" ββ Year: #{parts[0] if parts.length > 0}")
+ UI.important(" ββ Month: #{parts[1] if parts.length > 1}")
+ UI.important(" ββ Commit Count: #{commit_count}")
+
+ version
+ else
+ UI.important("π± Version from Gradle: #{full_version}")
+ full_version
end
- )
end
###################
@@ -406,37 +434,52 @@ platform :ios do
desc "Build Ios application"
lane :build_ios do |options|
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
- cocoapods(
- podfile: ios_config[:podfile_path],
- clean_install: true,
- repo_update: true
- )
+ # Install CocoaPods dependencies
+ cocoapods(
+ podfile: "cmp-ios/Podfile",
+ try_repo_update_on_error: true
+ )
- build_ios_app(
- scheme: ios_config[:scheme],
- workspace: ios_config[:workspace_path],
- output_name: ios_config[:output_name],
- output_directory: ios_config[:output_directory],
- skip_codesigning: true,
- skip_archive: true
- )
+ build_ios_app(
+ scheme: ios_config[:scheme],
+ workspace: ios_config[:workspace_path],
+ output_name: ios_config[:output_name],
+ output_directory: ios_config[:output_directory],
+ skip_codesigning: true,
+ skip_archive: true
+ )
end
desc "Build Signed Ios application"
lane :build_signed_ios do |options|
- setup_ci_if_needed
- load_api_key(options)
- fetch_certificates_with_match(options)
- build_ios_project(options)
+ # Install CocoaPods dependencies
+ cocoapods(
+ podfile: "cmp-ios/Podfile",
+ try_repo_update_on_error: true
+ )
+
+ setup_ci_if_needed
+ load_api_key(options)
+ fetch_certificates_with_match(options)
+ build_ios_project(options)
end
- desc "Increment build number from latest Firebase release"
+ desc "Increment build number from latest Firebase release and set version number"
lane :increment_version do |options|
firebase_config = FastlaneConfig.get_firebase_config(:ios)
ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ # Get version from gradle - Firebase accepts full semantic version with pre-release identifiers
+ version = get_version_from_gradle(sanitize_for_appstore: false)
+
+ # Set version number to match Android version
+ increment_version_number(
+ xcodeproj: ios_config[:project_path],
+ version_number: version
+ )
+
latest_release = firebase_app_distribution_get_latest_release(
app: firebase_config[:appId],
service_credentials_file: options[:serviceCredsFile] || firebase_config[:serviceCredsFile]
@@ -464,14 +507,15 @@ platform :ios do
desc "Upload iOS application to Firebase App Distribution"
lane :deploy_on_firebase do |options|
firebase_config = FastlaneConfig.get_firebase_config(:ios)
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
increment_version(serviceCredsFile: firebase_config[:serviceCredsFile])
build_signed_ios(
- options.merge(
- match_type: "adhoc",
- provisioning_profile_name: "match AdHoc org.mifos.mobile"
- )
+ options.merge(
+ match_type: ios_config[:match_type],
+ provisioning_profile_name: ios_config[:provisioning_profile_name]
+ )
)
releaseNotes = generateReleaseNote()
@@ -486,239 +530,226 @@ platform :ios do
desc "Upload beta build to TestFlight"
lane :beta do |options|
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ testflight_config = FastlaneConfig::IosConfig::TESTFLIGHT_CONFIG
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ # Install CocoaPods dependencies
+ cocoapods(
+ podfile: "cmp-ios/Podfile",
+ try_repo_update_on_error: true
+ )
- setup_ci_if_needed
- load_api_key(options)
- fetch_certificates_with_match(
- options.merge(match_type: "appstore")
- )
+ setup_ci_if_needed
+ load_api_key(options)
+ fetch_certificates_with_match(
+ options.merge(match_type: "appstore")
+ )
- increment_version_number(
- xcodeproj: ios_config[:project_path],
- version_number: ios_config[:version_number]
- )
+ # Get version from gradle (sanitized for App Store)
+ version = get_version_from_gradle(sanitize_for_appstore: true)
- latest_build_number = latest_testflight_build_number(
- app_identifier: options[:app_identifier] || ios_config[:app_identifier],
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- version: ios_config[:version_number]
- )
+ # Increment version number (sanitized for App Store)
+ increment_version_number(
+ xcodeproj: ios_config[:project_path],
+ version_number: options[:version_number] || version
+ )
- increment_build_number(
- xcodeproj: ios_config[:project_path],
- build_number: latest_build_number + 1
- )
+ # Get latest TestFlight build number and increment
+ latest_build_number = latest_testflight_build_number(
+ app_identifier: options[:app_identifier] || ios_config[:app_identifier],
+ api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
+ )
- set_plist_values
+ increment_build_number(
+ xcodeproj: ios_config[:project_path],
+ build_number: latest_build_number + 1
+ )
- build_ios_project(
- options.merge(
- provisioning_profile_name: "match AppStore org.mifos.mobile"
+ # Build signed IPA
+ build_ios_project(
+ options.merge(
+ provisioning_profile_name: ios_config[:provisioning_profile_appstore]
+ )
)
- )
- pilot(
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- skip_waiting_for_build_processing: true
- )
- end
+ # Generate release notes from git commits
+ releaseNotes = generateReleaseNote()
+
+ # Prepare localized build info with changelog
+ localized_build_info = {
+ "default" => {
+ whats_new: releaseNotes
+ },
+ "en-US" => {
+ whats_new: releaseNotes
+ }
+ }
- desc "Upload iOS Application to AppStore"
- lane :release do |options|
+ # Upload to TestFlight with comprehensive metadata
+ pilot(
+ # API Key
+ api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ # Beta App Review Information (for Apple reviewers)
+ beta_app_review_info: testflight_config[:beta_app_review_info],
- setup_ci_if_needed
- load_api_key(options)
- fetch_certificates_with_match(
- options.merge(match_type: "appstore")
- )
+ # Beta App Configuration
+ beta_app_feedback_email: testflight_config[:beta_app_feedback_email],
+ beta_app_description: testflight_config[:beta_app_description],
+ demo_account_required: testflight_config[:demo_account_required],
- increment_version_number(
- xcodeproj: ios_config[:project_path],
- version_number: ios_config[:version_number]
- )
+ # Distribution Settings
+ distribute_external: testflight_config[:distribute_external],
+ notify_external_testers: testflight_config[:notify_external_testers],
+ groups: testflight_config[:groups], # Tester groups to distribute to
- latest_build_number = latest_testflight_build_number(
- app_identifier: options[:app_identifier] || ios_config[:app_identifier],
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- version: ios_config[:version_number]
- )
+ # Submission Settings
+ skip_submission: testflight_config[:skip_submission],
+ skip_waiting_for_build_processing: testflight_config[:skip_waiting_for_build_processing],
+ submit_beta_review: testflight_config[:submit_beta_review],
- increment_build_number(
- xcodeproj: ios_config[:project_path],
- build_number: latest_build_number + 1
- )
+ # Build Management
+ expire_previous_builds: testflight_config[:expire_previous_builds],
+ reject_build_waiting_for_review: testflight_config[:reject_build_waiting_for_review],
+
+ # Processing Wait Settings
+ wait_processing_interval: testflight_config[:wait_processing_interval],
+ wait_processing_timeout_duration: testflight_config[:wait_processing_timeout_duration],
+
+ # Encryption Compliance
+ uses_non_exempt_encryption: testflight_config[:uses_non_exempt_encryption],
- set_plist_values
+ # Release notes for this build
+ changelog: releaseNotes,
- build_ios_project(
- options.merge(
- provisioning_profile_name: "match AppStore org.mifos.mobile"
+ # Localized information
+ localized_app_info: testflight_config[:localized_app_info],
+ localized_build_info: localized_build_info
)
- )
- deliver(
- screenshots_path: ios_config[:screenshots_ios_path],
- metadata_path: options[:metadata_path] || ios_config[:metadata_path],
- submit_for_review: false, # Set to true if you want to auto-submit for review
- automatic_release: true, # Set to true if you want to auto-release once it approved
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- skip_app_version_update: false,
- force: true, # Skips HTML report verification
- precheck_include_in_app_purchases: false,
- overwrite_screenshots: true,
- reject_if_possible: true,
- app_rating_config_path: ios_config[:app_rating_config_path],
- submission_information: {
- add_id_info_uses_idfa: false,
- add_id_info_limits_tracking: false,
- add_id_info_serves_ads: false,
- add_id_info_tracks_action: false,
- add_id_info_tracks_install: false,
- content_rights_has_rights: true,
- content_rights_contains_third_party_content: false,
- export_compliance_platform: 'ios',
- export_compliance_compliance_required: false,
- export_compliance_encryption_updated: false,
- export_compliance_app_type: nil,
- export_compliance_uses_encryption: false,
- export_compliance_is_exempt: true,
- export_compliance_contains_third_party_cryptography: false,
- export_compliance_contains_proprietary_cryptography: false,
- export_compliance_available_on_french_store: true
- }
- )
+ UI.success("β
Successfully uploaded to TestFlight!")
+ UI.success("π Release Notes: #{releaseNotes}")
end
-end
-platform :mac do
- #############################
- # Shared Private Lane Helpers
- #############################
+ desc "Upload iOS Application to App Store"
+ lane :release do |options|
+ ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ appstore_config = FastlaneConfig::IosConfig::APPSTORE_CONFIG
- private_lane :setup_ci_if_needed do
- if ENV['CI']
- setup_ci
- else
- UI.message("π₯οΈ Running locally, skipping CI-specific setup.")
- end
- end
+ # Install CocoaPods dependencies
+ cocoapods(
+ podfile: "cmp-ios/Podfile",
+ try_repo_update_on_error: true
+ )
- private_lane :load_api_key_macos do |options|
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ setup_ci_if_needed
+ load_api_key(options)
+ fetch_certificates_with_match(
+ options.merge(match_type: "appstore")
+ )
- app_store_connect_api_key(
- key_id: options[:appstore_key_id] || ios_config[:key_id],
- issuer_id: options[:appstore_issuer_id] || ios_config[:issuer_id],
- key_filepath: options[:key_filepath] || ios_config[:key_filepath],
- duration: 1200
- )
- end
+ # Get version from gradle (sanitized for App Store)
+ version = get_version_from_gradle(sanitize_for_appstore: true)
- private_lane :next_macos_build_number do |options|
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
+ # Increment version number (sanitized for App Store)
+ increment_version_number(
+ xcodeproj: ios_config[:project_path],
+ version_number: options[:version_number] || version
+ )
- latest = latest_testflight_build_number(
- app_identifier: options[:app_identifier] || ios_config[:app_identifier],
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- platform: "osx",
- version: ios_config[:version_number]
- )
- (latest.to_i + 1).to_s
- end
+ # Get latest TestFlight build number and increment
+ latest_build_number = latest_testflight_build_number(
+ app_identifier: options[:app_identifier] || ios_config[:app_identifier],
+ api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
+ )
- # Resolve the most-recent generated .pkg path
- private_lane :find_pkg_path do
- project_dir = File.expand_path('..', Dir.pwd)
- Dir[File.join(project_dir, 'cmp-desktop', 'build', 'release', '**', 'pkg', '*.pkg')]
- .max_by { |p| File.mtime(p) } || UI.user_error!('PKG not found!')
- end
+ increment_build_number(
+ xcodeproj: ios_config[:project_path],
+ build_number: latest_build_number + 1
+ )
- ###################
- # Public lanes
- ###################
+ # Update Info.plist with required privacy strings
+ update_plist(
+ plist_path: ios_config[:plist_path],
+ block: proc do |plist|
+ plist['NSContactsUsageDescription'] = 'This app does not access your contacts. This message is required for compliance only.'
+ plist['NSLocationWhenInUseUsageDescription'] = 'This app does not access your location. This message is required for compliance only.'
+ plist['NSBluetoothAlwaysUsageDescription'] = 'This app does not use Bluetooth. This message is required for compliance only.'
+ end
+ )
- desc "Build & upload macOS (.pkg) to TestFlight"
- lane :desktop_testflight do |options|
- setup_ci_if_needed
- load_api_key_macos(options)
+ # Build signed IPA
+ build_ios_project(
+ options.merge(
+ provisioning_profile_name: ios_config[:provisioning_profile_appstore]
+ )
+ )
- new_build_number = next_macos_build_number(options)
+ # Generate release notes from git commits
+ releaseNotes = generateReleaseNote()
- gradle(
- tasks: ["packageReleasePkg"],
- properties: {
- "buildNumber" => new_build_number,
- "macOsAppStoreRelease" => true
- }
- )
+ # Write release notes to metadata file (required when creating new versions)
+ release_notes_path = File.join(ios_config[:metadata_path], "en-US", "release_notes.txt")
+ FileUtils.mkdir_p(File.dirname(release_notes_path))
+ File.write(release_notes_path, releaseNotes)
+ UI.message("π Wrote release notes to: #{release_notes_path}")
- pkg_path = find_pkg_path
- UI.message("Found PKG at: #{pkg_path}")
+ # Log what metadata will be uploaded
+ UI.important("π Metadata files that will be uploaded:")
+ metadata_files = Dir.glob(File.join(ios_config[:metadata_path], "en-US", "*.txt"))
+ if metadata_files.empty?
+ UI.error("β οΈ No metadata files found!")
+ else
+ metadata_files.each do |file|
+ UI.message(" - #{File.basename(file)}")
+ end
+ end
+ UI.important("βΉοΈ Other metadata (description, screenshots, etc.) will be copied from previous version")
- pilot(
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- pkg: pkg_path,
- app_platform: 'osx',
- skip_waiting_for_build_processing: true,
- )
- end
+ # Upload binary and release notes to App Store
+ # Note: Metadata (description, screenshots, age rating) is already configured in App Store Connect
+ # We only upload the IPA, release notes, copyright, and submit for review
+ deliver(
+ # API Key
+ api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- desc "Build & submit macOS app to App Store (non-beta)"
- lane :desktop_release do |options|
- ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
- setup_ci_if_needed
- load_api_key_macos(options)
+ # Copyright (required by precheck)
+ copyright: "#{Time.now.year} #{FastlaneConfig::ProjectConfig::ORGANIZATION_NAME}",
- new_build_number = next_macos_build_number(options)
+ # Metadata path (contains release_notes.txt with "What's New")
+ metadata_path: ios_config[:metadata_path],
- gradle(
- tasks: ["packageReleasePkg"],
- properties: {
- "buildNumber" => new_build_number,
- "macOsAppStoreRelease" => true
- }
- )
+ # Skip uploading other metadata and screenshots (already in App Store Connect)
+ # But we DO upload release_notes.txt for "What's New" requirement
+ skip_metadata: false, # Must be false to upload release_notes.txt
+ skip_screenshots: true,
+ skip_binary_upload: options[:skip_binary_upload] || false,
- # Locate the produced PKG (adjust pattern if you rename)
- pkg_path = find_pkg_path
- UI.message("Found PKG at: #{pkg_path}")
-
- deliver(
- platform: 'osx',
- pkg: pkg_path,
- screenshots_path: ios_config[:screenshots_macos_path],
- metadata_path: options[:metadata_path] || ios_config[:metadata_path],
- submit_for_review: true, # Set to true if you want to auto-submit for review
- automatic_release: true, # Set to true if you want to auto-release once it approved
- api_key: Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY],
- skip_app_version_update: false,
- force: true, # Skips HTML report verification
- precheck_include_in_app_purchases: false,
- overwrite_screenshots: true,
- reject_if_possible: true,
- app_rating_config_path: ios_config[:app_rating_config_path],
- submission_information: {
- add_id_info_uses_idfa: false,
- add_id_info_limits_tracking: false,
- add_id_info_serves_ads: false,
- add_id_info_tracks_action: false,
- add_id_info_tracks_install: false,
- content_rights_has_rights: true,
- content_rights_contains_third_party_content: false,
- export_compliance_platform: 'osx',
- export_compliance_compliance_required: false,
- export_compliance_encryption_updated: false,
- export_compliance_app_type: nil,
- export_compliance_uses_encryption: false,
- export_compliance_is_exempt: true,
- export_compliance_contains_third_party_cryptography: false,
- export_compliance_contains_proprietary_cryptography: false,
- export_compliance_available_on_french_store: true
- }
- )
+ # Only overwrite specific fields we provide (don't clear existing metadata)
+ overwrite_screenshots: false,
+
+ # App Review Information (contact details for Apple reviewers)
+ app_review_information: appstore_config[:app_review_information],
+
+ # Submission settings
+ submit_for_review: options[:submit_for_review] || appstore_config[:submit_for_review],
+ automatic_release: options[:automatic_release] || appstore_config[:automatic_release],
+ phased_release: options[:phased_release] || appstore_config[:phased_release],
+
+ # Version and build management
+ skip_app_version_update: options[:skip_app_version_update] || appstore_config[:skip_app_version_update],
+ reject_if_possible: appstore_config[:reject_if_possible],
+
+ # Processing settings
+ force: appstore_config[:force],
+ precheck_include_in_app_purchases: appstore_config[:precheck_include_in_app_purchases],
+ run_precheck_before_submit: appstore_config[:run_precheck_before_submit],
+
+ # Submission information (compliance, ads, etc.)
+ submission_information: appstore_config[:submission_information]
+ )
+
+ UI.success("β
Successfully deployed to App Store!")
+ UI.success("π Release Notes: #{releaseNotes}")
end
end
\ No newline at end of file
diff --git a/fastlane/README.md b/fastlane/README.md
index c8de7f0021..af00a1e17d 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -114,7 +114,15 @@ Generate full release notes from specified tag or latest release tag
[bundle exec] fastlane ios build_ios
```
-Build iOS application
+Build Ios application
+
+### ios build_signed_ios
+
+```sh
+[bundle exec] fastlane ios build_signed_ios
+```
+
+Build Signed Ios application
### ios increment_version
@@ -122,7 +130,15 @@ Build iOS application
[bundle exec] fastlane ios increment_version
```
+Increment build number from latest Firebase release and set version number
+
+### ios generateReleaseNote
+
+```sh
+[bundle exec] fastlane ios generateReleaseNote
+```
+Generate release notes
### ios deploy_on_firebase
@@ -132,13 +148,21 @@ Build iOS application
Upload iOS application to Firebase App Distribution
-### ios generateReleaseNote
+### ios beta
```sh
-[bundle exec] fastlane ios generateReleaseNote
+[bundle exec] fastlane ios beta
```
-Generate release notes
+Upload beta build to TestFlight
+
+### ios release
+
+```sh
+[bundle exec] fastlane ios release
+```
+
+Upload iOS Application to App Store
----
diff --git a/scripts/check_ios_version.sh b/scripts/check_ios_version.sh
new file mode 100755
index 0000000000..8b023dd6b0
--- /dev/null
+++ b/scripts/check_ios_version.sh
@@ -0,0 +1,135 @@
+#!/bin/bash
+
+# ==============================================================================
+# iOS Version Check Script
+# ==============================================================================
+# This script displays current iOS version configuration
+# ==============================================================================
+
+set -e
+
+# Color codes
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+# Navigate to project root
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+print_section "π± iOS Version Configuration Check"
+
+# Check version.txt (Gradle-generated)
+if [ -f "version.txt" ]; then
+ VERSION_TXT=$(cat version.txt)
+
+ # Extract App Store version: Year.Month.CommitCount
+ # From: 2026.1.1-beta.0.9 β To: 2026.1.9
+ BASE_VERSION=$(echo "$VERSION_TXT" | cut -d'-' -f1 | cut -d'+' -f1)
+ YEAR=$(echo "$BASE_VERSION" | cut -d'.' -f1)
+ MONTH=$(echo "$BASE_VERSION" | cut -d'.' -f2)
+
+ # Extract commit count from pre-release identifier
+ if echo "$VERSION_TXT" | grep -q '\-'; then
+ PRE_RELEASE=$(echo "$VERSION_TXT" | cut -d'-' -f2 | cut -d'+' -f1)
+ COMMIT_COUNT=$(echo "$PRE_RELEASE" | rev | cut -d'.' -f1 | rev)
+ else
+ COMMIT_COUNT="0"
+ fi
+
+ APPSTORE_VERSION="${YEAR}.${MONTH}.${COMMIT_COUNT}"
+
+ print_success "Gradle version.txt: $VERSION_TXT"
+ if [ "$VERSION_TXT" != "$APPSTORE_VERSION" ]; then
+ print_info "App Store sanitized version: $APPSTORE_VERSION"
+ echo " (Format: Year.Month.CommitCount - commit count extracted from pre-release)"
+ fi
+else
+ echo "β οΈ version.txt not found. Run: ./gradlew versionFile"
+fi
+echo
+
+# Check Xcode project settings
+print_info "Xcode Project Settings:"
+MARKETING_VERSION=$(xcodebuild -project cmp-ios/iosApp.xcodeproj -showBuildSettings 2>/dev/null | grep "MARKETING_VERSION" | awk '{print $3}' | head -1)
+CURRENT_PROJECT_VERSION=$(xcodebuild -project cmp-ios/iosApp.xcodeproj -showBuildSettings 2>/dev/null | grep "CURRENT_PROJECT_VERSION" | awk '{print $3}' | head -1)
+
+if [ -n "$MARKETING_VERSION" ]; then
+ print_success "MARKETING_VERSION: $MARKETING_VERSION"
+else
+ echo "β οΈ MARKETING_VERSION not found"
+fi
+
+if [ -n "$CURRENT_PROJECT_VERSION" ]; then
+ print_success "CURRENT_PROJECT_VERSION: $CURRENT_PROJECT_VERSION"
+else
+ echo "β οΈ CURRENT_PROJECT_VERSION not found"
+fi
+echo
+
+# Check Info.plist
+print_info "Info.plist Configuration:"
+SHORT_VERSION=$(plutil -p cmp-ios/iosApp/Info.plist 2>/dev/null | grep CFBundleShortVersionString | awk -F'"' '{print $4}')
+BUNDLE_VERSION=$(plutil -p cmp-ios/iosApp/Info.plist 2>/dev/null | grep "\"CFBundleVersion\"" | awk -F'"' '{print $4}')
+
+echo " CFBundleShortVersionString: $SHORT_VERSION"
+echo " CFBundleVersion: $BUNDLE_VERSION"
+
+if [[ "$SHORT_VERSION" == "\$(MARKETING_VERSION)" ]]; then
+ print_success "Using dynamic versioning (MARKETING_VERSION)"
+else
+ echo " β οΈ Warning: Info.plist has hardcoded version"
+fi
+
+if [[ "$BUNDLE_VERSION" == "\$(CURRENT_PROJECT_VERSION)" ]]; then
+ print_success "Using dynamic build number (CURRENT_PROJECT_VERSION)"
+else
+ echo " β οΈ Warning: Info.plist has hardcoded build number"
+fi
+echo
+
+# Summary
+print_section "π Summary"
+echo "When you deploy iOS:"
+echo " 1. Fastlane runs: gradle(tasks: [\"versionFile\"])"
+echo " 2. Reads version from: version.txt"
+if [ -n "$VERSION_TXT" ] && [ -n "$APPSTORE_VERSION" ]; then
+ echo " β’ Full version: $VERSION_TXT"
+ if [ "$VERSION_TXT" != "$APPSTORE_VERSION" ]; then
+ echo " β’ TestFlight/App Store version: $APPSTORE_VERSION (sanitized)"
+ fi
+else
+ echo " β’ Version: (version.txt not found)"
+fi
+echo " 3. Updates Xcode MARKETING_VERSION accordingly"
+echo " 4. Auto-increments CURRENT_PROJECT_VERSION from TestFlight/Firebase"
+echo " 5. Info.plist uses: \$(MARKETING_VERSION) and \$(CURRENT_PROJECT_VERSION)"
+echo
+echo "Note: Firebase accepts full semantic versions with pre-release identifiers"
+echo " TestFlight/App Store require sanitized versions (MAJOR.MINOR.PATCH only)"
+echo
+
+print_info "To update version for next release:"
+echo " 1. Update version in Gradle (where project.version is defined)"
+echo " 2. Run: ./gradlew versionFile"
+echo " 3. Deploy: bash scripts/deploy_testflight.sh"
+echo
diff --git a/scripts/deploy_appstore.sh b/scripts/deploy_appstore.sh
new file mode 100755
index 0000000000..0d2421c8a7
--- /dev/null
+++ b/scripts/deploy_appstore.sh
@@ -0,0 +1,311 @@
+#!/bin/bash
+
+# ==============================================================================
+# iOS App Store Production Deployment Script
+# ==============================================================================
+# This script deploys your iOS app to the App Store for production release
+# ==============================================================================
+
+set -e # Exit on any error
+
+# Color codes for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+MAGENTA='\033[0;35m'
+NC='\033[0m' # No Color
+
+# Script directory and project root
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+# Print functions
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}β $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_critical() {
+ echo -e "${MAGENTA}β οΈ CRITICAL: $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+# Print banner
+print_section "π iOS App Store Production Deployment"
+
+# Check if running on macOS
+if [[ "$OSTYPE" != "darwin"* ]]; then
+ print_error "This script must be run on macOS"
+ exit 1
+fi
+
+# Validate prerequisites
+print_info "Checking prerequisites..."
+
+# Check for Xcode
+if ! command -v xcodebuild &> /dev/null; then
+ print_error "Xcode is not installed"
+ print_info "Install Xcode from the App Store"
+ exit 1
+fi
+print_success "Xcode installed"
+
+# Check for Ruby
+if ! command -v ruby &> /dev/null; then
+ print_error "Ruby is not installed"
+ exit 1
+fi
+print_success "Ruby installed"
+
+# Check for Bundler
+if ! command -v bundle &> /dev/null; then
+ print_error "Bundler is not installed"
+ print_info "Install with: gem install bundler"
+ exit 1
+fi
+print_success "Bundler installed"
+
+# Validate required files
+print_section "π Validating Configuration"
+
+REQUIRED_FILES=(
+ "secrets/shared_keys.env"
+ "secrets/.match_password"
+ "secrets/match_ci_key"
+ "secrets/AuthKey.p8"
+)
+
+MISSING_FILES=()
+
+for file in "${REQUIRED_FILES[@]}"; do
+ if [ ! -f "$file" ]; then
+ MISSING_FILES+=("$file")
+ else
+ print_success "Found: $file"
+ fi
+done
+
+if [ ${#MISSING_FILES[@]} -gt 0 ]; then
+ print_error "Missing required files:"
+ for file in "${MISSING_FILES[@]}"; do
+ echo " - $file"
+ done
+ echo
+ print_info "Run the iOS setup wizard: bash scripts/setup_ios_complete.sh"
+ exit 1
+fi
+
+# Load shared configuration
+print_info "Loading iOS shared configuration..."
+source secrets/shared_keys.env
+
+# Validate App Store Connect API key configuration
+if [ -z "$APPSTORE_KEY_ID" ] || [ -z "$APPSTORE_ISSUER_ID" ]; then
+ print_error "App Store Connect API credentials not configured"
+ print_info "Update secrets/shared_keys.env with APPSTORE_KEY_ID and APPSTORE_ISSUER_ID"
+ exit 1
+fi
+
+# Load Match password
+export MATCH_PASSWORD=$(cat secrets/.match_password)
+
+# Setup SSH for Match
+export GIT_SSH_COMMAND="ssh -i secrets/match_ci_key -o IdentitiesOnly=yes"
+
+print_success "Configuration loaded"
+
+# Display configuration summary
+print_section "π± Deployment Configuration"
+echo "Team ID: ${TEAM_ID}"
+echo "App Store Connect Key ID: ${APPSTORE_KEY_ID}"
+echo "Match Repository: ${MATCH_GIT_URL}"
+echo "Match Branch: ${MATCH_GIT_BRANCH}"
+echo
+
+# Important warnings for App Store submission
+print_section "β οΈ IMPORTANT: App Store Submission Checklist"
+print_critical "This is a PRODUCTION deployment to the App Store!"
+echo
+print_warning "Before proceeding, ensure you have:"
+echo " β Tested the app thoroughly on physical devices"
+echo " β Verified all features work as expected"
+echo " β Checked for crashes and bugs"
+echo " β Tested on multiple iOS versions (if supporting multiple)"
+echo " β Verified in-app purchases work (if applicable)"
+echo " β Reviewed app metadata in App Store Connect:"
+echo " - App Description"
+echo " - Screenshots (all required sizes)"
+echo " - Privacy Policy URL"
+echo " - Support URL"
+echo " - Age Rating"
+echo " - Keywords"
+echo " β Prepared release notes for this version"
+echo " β Set up app pricing and availability"
+echo
+
+print_section "π App Store Review Process"
+print_info "What happens after submission:"
+echo " 1. Binary upload to App Store Connect (this script)"
+echo " 2. Binary processing by Apple (10-30 minutes)"
+echo " 3. Submission for App Store review (automatic)"
+echo " 4. 'Waiting for Review' status (can take hours to days)"
+echo " 5. 'In Review' status (Apple is reviewing)"
+echo " 6. Review decision:"
+echo " β Approved β App goes live automatically (or on release date)"
+echo " β Rejected β Fix issues and resubmit"
+echo
+print_warning "App Store review typically takes 24-72 hours"
+print_warning "Rejections are common - be prepared to fix issues and resubmit"
+echo
+
+print_section "βοΈ Deployment Options"
+echo "This script will use settings from fastlane-config/project_config.rb:"
+echo " - Submit for Review: ${APPSTORE_SUBMIT_FOR_REVIEW:-true}"
+echo " - Automatic Release: ${APPSTORE_AUTOMATIC_RELEASE:-true}"
+echo
+
+print_info "You can override these with command-line options:"
+echo " --submit-for-review=false Skip auto-submit (manual submit later)"
+echo " --automatic-release=false Manual release after approval"
+echo
+
+# Parse command-line arguments
+SUBMIT_FOR_REVIEW=""
+AUTOMATIC_RELEASE=""
+
+for arg in "$@"; do
+ case $arg in
+ --submit-for-review=*)
+ SUBMIT_FOR_REVIEW="${arg#*=}"
+ ;;
+ --automatic-release=*)
+ AUTOMATIC_RELEASE="${arg#*=}"
+ ;;
+ --help)
+ echo "Usage: bash scripts/deploy_appstore.sh [OPTIONS]"
+ echo
+ echo "Options:"
+ echo " --submit-for-review=true|false Submit for review after upload (default: true)"
+ echo " --automatic-release=true|false Automatically release after approval (default: true)"
+ echo " --help Show this help message"
+ exit 0
+ ;;
+ esac
+done
+
+# Final confirmation with double-check
+print_section "β οΈ FINAL CONFIRMATION"
+print_critical "You are about to deploy to the PRODUCTION App Store!"
+print_critical "This action CANNOT be undone!"
+echo
+print_warning "Are you absolutely sure you want to continue?"
+read -p "Type 'YES' (in capital letters) to confirm: " CONFIRM
+
+if [ "$CONFIRM" != "YES" ]; then
+ print_info "Deployment cancelled"
+ exit 0
+fi
+
+# Second confirmation for extra safety
+read -p "Final confirmation - deploy to App Store? [y/N]: " -n 1 -r
+echo
+
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_info "Deployment cancelled"
+ exit 0
+fi
+
+# Install/Update Fastlane dependencies
+print_section "π¦ Installing Dependencies"
+bundle install
+print_success "Dependencies installed"
+
+# Build deployment command
+FASTLANE_CMD="bundle exec fastlane ios release"
+
+if [ -n "$SUBMIT_FOR_REVIEW" ]; then
+ FASTLANE_CMD="$FASTLANE_CMD submit_for_review:$SUBMIT_FOR_REVIEW"
+fi
+
+if [ -n "$AUTOMATIC_RELEASE" ]; then
+ FASTLANE_CMD="$FASTLANE_CMD automatic_release:$AUTOMATIC_RELEASE"
+fi
+
+# Run Fastlane deployment
+print_section "π Deploying to App Store"
+print_info "This process will:"
+echo " 1. Sync code signing certificates with Match"
+echo " 2. Increment version and build number"
+echo " 3. Update Info.plist with privacy descriptions"
+echo " 4. Build signed IPA"
+echo " 5. Upload binary to App Store Connect"
+echo " 6. Submit for App Store review (if enabled)"
+echo
+
+eval $FASTLANE_CMD
+
+# Check if deployment was successful
+if [ $? -eq 0 ]; then
+ print_section "β
Deployment Successful!"
+ print_success "Your iOS app has been uploaded to App Store Connect"
+
+ if [ "$SUBMIT_FOR_REVIEW" != "false" ]; then
+ print_info "Next steps:"
+ echo " 1. Monitor review status in App Store Connect"
+ echo " 2. Check https://appstoreconnect.apple.com/"
+ echo " 3. Go to App Store β iOS App β App Store"
+ echo " 4. Watch for status changes:"
+ echo " - Processing β Waiting for Review β In Review β Approved/Rejected"
+ echo
+ print_warning "Review typically takes 24-72 hours"
+
+ if [ "$AUTOMATIC_RELEASE" != "false" ]; then
+ print_warning "App will go live AUTOMATICALLY after approval!"
+ else
+ print_info "You'll need to manually release the app after approval"
+ fi
+ else
+ print_info "Binary uploaded but NOT submitted for review"
+ print_info "Go to App Store Connect to manually submit when ready"
+ fi
+
+ echo
+ print_info "Useful links:"
+ echo " - App Store Connect: https://appstoreconnect.apple.com/"
+ echo " - App Store Review Guidelines: https://developer.apple.com/app-store/review/guidelines/"
+ echo " - Common Rejection Reasons: https://developer.apple.com/app-store/review/"
+else
+ print_section "β Deployment Failed"
+ print_error "Please check the error messages above"
+ print_info "Common issues:"
+ echo " - Invalid Match password"
+ echo " - SSH key not added to Match repository"
+ echo " - Invalid App Store Connect API key"
+ echo " - Certificate/provisioning profile issues"
+ echo " - Build number conflicts (already uploaded)"
+ echo " - Missing app metadata in App Store Connect"
+ echo " - Missing required assets (screenshots, app icon, etc.)"
+ echo " - Export compliance information missing"
+ exit 1
+fi
diff --git a/scripts/deploy_firebase.sh b/scripts/deploy_firebase.sh
new file mode 100755
index 0000000000..467b838d2c
--- /dev/null
+++ b/scripts/deploy_firebase.sh
@@ -0,0 +1,166 @@
+#!/bin/bash
+
+# ==============================================================================
+# iOS Firebase App Distribution Deployment Script
+# ==============================================================================
+# This script deploys your iOS app to Firebase App Distribution for testing
+# ==============================================================================
+
+set -e # Exit on any error
+
+# Color codes for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+# Script directory and project root
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+# Print functions
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}β $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+# Print banner
+print_section "π iOS Firebase App Distribution Deployment"
+
+# Check if running on macOS
+if [[ "$OSTYPE" != "darwin"* ]]; then
+ print_error "This script must be run on macOS"
+ exit 1
+fi
+
+# Validate prerequisites
+print_info "Checking prerequisites..."
+
+# Check for Xcode
+if ! command -v xcodebuild &> /dev/null; then
+ print_error "Xcode is not installed"
+ print_info "Install Xcode from the App Store"
+ exit 1
+fi
+print_success "Xcode installed"
+
+# Check for Ruby
+if ! command -v ruby &> /dev/null; then
+ print_error "Ruby is not installed"
+ exit 1
+fi
+print_success "Ruby installed"
+
+# Check for Bundler
+if ! command -v bundle &> /dev/null; then
+ print_error "Bundler is not installed"
+ print_info "Install with: gem install bundler"
+ exit 1
+fi
+print_success "Bundler installed"
+
+# Validate required files
+print_section "π Validating Configuration"
+
+REQUIRED_FILES=(
+ "secrets/shared_keys.env"
+ "secrets/.match_password"
+ "secrets/match_ci_key"
+ "secrets/firebaseAppDistributionServiceCredentialsFile.json"
+)
+
+MISSING_FILES=()
+
+for file in "${REQUIRED_FILES[@]}"; do
+ if [ ! -f "$file" ]; then
+ MISSING_FILES+=("$file")
+ else
+ print_success "Found: $file"
+ fi
+done
+
+if [ ${#MISSING_FILES[@]} -gt 0 ]; then
+ print_error "Missing required files:"
+ for file in "${MISSING_FILES[@]}"; do
+ echo " - $file"
+ done
+ echo
+ print_info "Run the iOS setup wizard: bash scripts/setup_ios_complete.sh"
+ exit 1
+fi
+
+# Load shared configuration
+print_info "Loading iOS shared configuration..."
+source secrets/shared_keys.env
+
+# Load Match password
+export MATCH_PASSWORD=$(cat secrets/.match_password)
+
+# Setup SSH for Match
+export GIT_SSH_COMMAND="ssh -i secrets/match_ci_key -o IdentitiesOnly=yes"
+
+print_success "Configuration loaded"
+
+# Display configuration summary
+print_section "π± Deployment Configuration"
+echo "Team ID: ${TEAM_ID}"
+echo "Match Repository: ${MATCH_GIT_URL}"
+echo "Match Branch: ${MATCH_GIT_BRANCH}"
+echo
+
+# Confirmation prompt
+print_warning "This will build and deploy your iOS app to Firebase App Distribution"
+read -p "Do you want to continue? [y/N]: " -n 1 -r
+echo
+
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_info "Deployment cancelled"
+ exit 0
+fi
+
+# Install/Update Fastlane dependencies
+print_section "π¦ Installing Dependencies"
+bundle install
+print_success "Dependencies installed"
+
+# Run Fastlane deployment
+print_section "π Deploying to Firebase"
+bundle exec fastlane ios deploy_on_firebase
+
+# Check if deployment was successful
+if [ $? -eq 0 ]; then
+ print_section "β
Deployment Successful!"
+ print_success "Your iOS app has been deployed to Firebase App Distribution"
+ print_info "Testers will receive a notification with download instructions"
+else
+ print_section "β Deployment Failed"
+ print_error "Please check the error messages above"
+ print_info "Common issues:"
+ echo " - Invalid Match password"
+ echo " - SSH key not added to Match repository"
+ echo " - Invalid Firebase credentials"
+ echo " - Certificate/provisioning profile issues"
+ exit 1
+fi
diff --git a/scripts/deploy_testflight.sh b/scripts/deploy_testflight.sh
new file mode 100755
index 0000000000..8c38027cab
--- /dev/null
+++ b/scripts/deploy_testflight.sh
@@ -0,0 +1,207 @@
+#!/bin/bash
+
+# ==============================================================================
+# iOS TestFlight Deployment Script
+# ==============================================================================
+# This script deploys your iOS app to TestFlight for beta testing
+# ==============================================================================
+
+set -e # Exit on any error
+
+# Color codes for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+# Script directory and project root
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+# Print functions
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}β $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+# Print banner
+print_section "π iOS TestFlight Beta Deployment"
+
+# Check if running on macOS
+if [[ "$OSTYPE" != "darwin"* ]]; then
+ print_error "This script must be run on macOS"
+ exit 1
+fi
+
+# Validate prerequisites
+print_info "Checking prerequisites..."
+
+# Check for Xcode
+if ! command -v xcodebuild &> /dev/null; then
+ print_error "Xcode is not installed"
+ print_info "Install Xcode from the App Store"
+ exit 1
+fi
+print_success "Xcode installed"
+
+# Check for Ruby
+if ! command -v ruby &> /dev/null; then
+ print_error "Ruby is not installed"
+ exit 1
+fi
+print_success "Ruby installed"
+
+# Check for Bundler
+if ! command -v bundle &> /dev/null; then
+ print_error "Bundler is not installed"
+ print_info "Install with: gem install bundler"
+ exit 1
+fi
+print_success "Bundler installed"
+
+# Validate required files
+print_section "π Validating Configuration"
+
+REQUIRED_FILES=(
+ "secrets/shared_keys.env"
+ "secrets/.match_password"
+ "secrets/match_ci_key"
+ "secrets/AuthKey.p8"
+)
+
+MISSING_FILES=()
+
+for file in "${REQUIRED_FILES[@]}"; do
+ if [ ! -f "$file" ]; then
+ MISSING_FILES+=("$file")
+ else
+ print_success "Found: $file"
+ fi
+done
+
+if [ ${#MISSING_FILES[@]} -gt 0 ]; then
+ print_error "Missing required files:"
+ for file in "${MISSING_FILES[@]}"; do
+ echo " - $file"
+ done
+ echo
+ print_info "Run the iOS setup wizard: bash scripts/setup_ios_complete.sh"
+ exit 1
+fi
+
+# Load shared configuration
+print_info "Loading iOS shared configuration..."
+source secrets/shared_keys.env
+
+# Validate App Store Connect API key configuration
+if [ -z "$APPSTORE_KEY_ID" ] || [ -z "$APPSTORE_ISSUER_ID" ]; then
+ print_error "App Store Connect API credentials not configured"
+ print_info "Update secrets/shared_keys.env with APPSTORE_KEY_ID and APPSTORE_ISSUER_ID"
+ exit 1
+fi
+
+# Load Match password
+export MATCH_PASSWORD=$(cat secrets/.match_password)
+
+# Setup SSH for Match
+export GIT_SSH_COMMAND="ssh -i secrets/match_ci_key -o IdentitiesOnly=yes"
+
+print_success "Configuration loaded"
+
+# Display configuration summary
+print_section "π± Deployment Configuration"
+echo "Team ID: ${TEAM_ID}"
+echo "App Store Connect Key ID: ${APPSTORE_KEY_ID}"
+echo "Match Repository: ${MATCH_GIT_URL}"
+echo "Match Branch: ${MATCH_GIT_BRANCH}"
+echo "TestFlight Groups: ${TESTFLIGHT_GROUPS:-mifos-mobile-apps}"
+echo
+
+# Important warnings
+print_section "β οΈ Important Information"
+print_warning "TestFlight Deployment Process:"
+echo " 1. Build will be uploaded to App Store Connect"
+echo " 2. Build will be submitted for beta review by Apple"
+echo " 3. Review typically takes 24-48 hours"
+echo " 4. Once approved, testers will be notified automatically"
+echo
+
+print_warning "Beta Review Information:"
+echo " - Contact Email: ${TESTFLIGHT_CONTACT_EMAIL:-team@mifos.org}"
+echo " - Contact Name: ${TESTFLIGHT_FIRST_NAME:-Mifos} ${TESTFLIGHT_LAST_NAME:-Initiative}"
+echo " - Contact Phone: ${TESTFLIGHT_PHONE:-+1234567890}"
+echo
+
+# Confirmation prompt
+print_warning "This will build and deploy your iOS app to TestFlight"
+read -p "Do you want to continue? [y/N]: " -n 1 -r
+echo
+
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_info "Deployment cancelled"
+ exit 0
+fi
+
+# Install/Update Fastlane dependencies
+print_section "π¦ Installing Dependencies"
+bundle install
+print_success "Dependencies installed"
+
+# Run Fastlane deployment
+print_section "π Deploying to TestFlight"
+print_info "This process will:"
+echo " 1. Sync code signing certificates with Match"
+echo " 2. Increment version and build number"
+echo " 3. Build signed IPA"
+echo " 4. Upload to App Store Connect"
+echo " 5. Submit for beta review"
+echo
+
+bundle exec fastlane ios beta
+
+# Check if deployment was successful
+if [ $? -eq 0 ]; then
+ print_section "β
Deployment Successful!"
+ print_success "Your iOS app has been uploaded to TestFlight"
+ print_info "Next steps:"
+ echo " 1. Monitor beta review status in App Store Connect"
+ echo " 2. Check https://appstoreconnect.apple.com/"
+ echo " 3. Go to TestFlight β iOS β Builds"
+ echo " 4. Wait for 'Ready to Submit' β 'Waiting for Review' β 'In Review' β 'Approved'"
+ echo " 5. Once approved, testers will receive notifications"
+ echo
+ print_info "Beta review typically takes 24-48 hours"
+else
+ print_section "β Deployment Failed"
+ print_error "Please check the error messages above"
+ print_info "Common issues:"
+ echo " - Invalid Match password"
+ echo " - SSH key not added to Match repository"
+ echo " - Invalid App Store Connect API key"
+ echo " - Certificate/provisioning profile issues"
+ echo " - Build number conflicts (already uploaded)"
+ echo " - Missing beta review information"
+ exit 1
+fi
diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh
index 04c785606b..2f87f220e8 100644
--- a/scripts/pre-commit.sh
+++ b/scripts/pre-commit.sh
@@ -4,7 +4,7 @@
check_current_branch() {
echo "\nπ Checking the current git branch..."
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
- if [ "$CURRENT_BRANCH" = "master" ] || [ "$CURRENT_BRANCH" = "development" ]; then
+ if [ "$CURRENT_BRANCH" = "master" ] || [ "$CURRENT_BRANCH" = "dev" ]; then
echo "π Hold it right there! Committing directly to the '$CURRENT_BRANCH' branch? That's a big no-no!"
echo "π« Direct commits to '$CURRENT_BRANCH' are like trying to use a wrench to write codeβdoesn't work! π"
echo "\nABORTING COMMIT: You must navigate to a feature branch or create a new one to save the day! π¦ΈββοΈπ¦ΈββοΈ\n"
@@ -37,16 +37,19 @@ run_spotless_checks() {
# Function to run ktlint checks
run_dependency_guard() {
printf "\nπ Brace yourself! We're about to generate dependency guard baseline!"
- ./gradlew dependencyGuardBaseline
+ ./gradlew dependencyGuardBaseline > /tmp/dependency-result
KT_EXIT_CODE=$?
if [ ${KT_EXIT_CODE} -ne 0 ]; then
+ cat /tmp/dependency-result
+ rm /tmp/dependency-result
printf "\n*********************************************************************************"
echo " π₯ Oh no! Something went wrong! π₯"
echo " π‘ Unable to generate dependency baseline. π οΈ"
printf "*********************************************************************************\n"
exit ${KT_EXIT_CODE}
else
+ rm /tmp/dependency-result
echo "π Bravo! Dependency baseline has been generated successfully! Keep rocking that clean code! ππ«"
fi
}
diff --git a/scripts/pre-push.sh b/scripts/pre-push.sh
index 71bf5c432f..d628bc07b2 100644
--- a/scripts/pre-push.sh
+++ b/scripts/pre-push.sh
@@ -4,7 +4,7 @@
check_current_branch() {
printf "\nπ Checking the current git branch..."
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
- if [ "$CURRENT_BRANCH" = "master" ] || [ "$CURRENT_BRANCH" = "development" ]; then
+ if [ "$CURRENT_BRANCH" = "master" ] || [ "$CURRENT_BRANCH" = "dev" ]; then
echo "π Hold it right there! Committing directly to the '$CURRENT_BRANCH' branch? That's a big no-no!"
echo "π« Direct commits to '$CURRENT_BRANCH' are like trying to use a wrench to write codeβdoesn't work! π"
printf "\nABORTING COMMIT: You must navigate to a feature branch or create a new one to save the day! π¦ΈββοΈπ¦ΈββοΈ\n"
@@ -28,7 +28,9 @@ run_spotless_checks() {
echo " π‘ Tip: Check the reported issues and fix formatting errors. π οΈ"
echo "*********************************************************************************"
echo "π Attempting to apply Spotless formatting fixes..."
- ./gradlew spotlessApply --daemon
+ ./gradlew spotlessApply --daemon > /tmp/spotless-result
+ rm /tmp/spotless-result
+ echo "π Stellar job! Your code is pristine and has passed Spotless's formatting checks without a hitch! Keep shining bright! β¨π"
else
rm /tmp/spotless-result
echo "π Stellar job! Your code is pristine and has passed Spotless's formatting checks without a hitch! Keep shining bright! β¨π"
@@ -58,18 +60,23 @@ run_detekt_checks() {
# Function to run ktlint checks
run_dependency_guard() {
printf "\nπ Brace yourself! We're about to generate dependency guard baseline!"
- ./gradlew dependencyGuard
+ ./gradlew dependencyGuard > /tmp/dependency-result
KT_EXIT_CODE=$?
if [ ${KT_EXIT_CODE} -ne 0 ]; then
+ cat /tmp/dependency-result
+ rm /tmp/dependency-result
printf "\n*********************************************************************************"
echo " π₯ Oh no! Something went wrong! π₯"
echo " π‘ Unable to generate dependency baseline. π οΈ"
printf "*********************************************************************************\n"
echo "π Attempting to generate dependency baseline again..."
- ./gradlew dependencyGuardBaseline
- else
+ ./gradlew dependencyGuardBaseline > /tmp/dependency-result
+ rm /tmp/dependency-result
echo "π Bravo! Dependency baseline has been generated successfully! Keep rocking that clean code! ππ«"
+ else
+ rm /tmp/dependency-result
+ echo "π Bravo! Dependency baseline has been checked successfully! Keep rocking that clean code! ππ«"
fi
}
diff --git a/scripts/setup_apn_key.sh b/scripts/setup_apn_key.sh
new file mode 100755
index 0000000000..d014e44551
--- /dev/null
+++ b/scripts/setup_apn_key.sh
@@ -0,0 +1,225 @@
+#!/bin/bash
+
+# ==============================================================================
+# APN (Apple Push Notification) Key Setup Script
+# ==============================================================================
+# This script helps configure APN keys for Firebase Cloud Messaging on iOS
+# ==============================================================================
+
+set -e # Exit on any error
+
+# Color codes
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+# Script directory
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+# Print functions
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}β $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+# Print banner
+print_section "π APN (Apple Push Notification) Key Setup"
+
+# Check if shared_keys.env exists
+if [ ! -f "secrets/shared_keys.env" ]; then
+ print_error "secrets/shared_keys.env not found"
+ print_info "Run the iOS setup wizard first: bash scripts/setup_ios_complete.sh"
+ exit 1
+fi
+
+# Load existing configuration
+source secrets/shared_keys.env
+
+# Introduction
+print_info "This script helps you configure Apple Push Notification (APN) keys"
+print_info "APN keys are required if your app uses Firebase Cloud Messaging for push notifications"
+echo
+
+print_warning "If your app does NOT use push notifications, you can skip this setup"
+read -p "Do you want to configure APN keys? [y/N]: " -n 1 -r
+echo
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_info "Skipping APN setup"
+ exit 0
+fi
+
+# Instructions for creating APN key
+print_section "π Creating APN Key in Apple Developer Portal"
+
+print_info "Follow these steps to create an APN key:"
+echo " 1. Go to: https://developer.apple.com/account/resources/authkeys/list"
+echo " 2. Click the '+' button"
+echo " 3. Name it: 'APNs Auth Key' or similar"
+echo " 4. Check the box for 'Apple Push Notifications service (APNs)'"
+echo " 5. Click 'Continue'"
+echo " 6. Click 'Register'"
+echo " 7. Click 'Download' (you can only download once!)"
+echo " 8. Note the Key ID (10 characters shown on the screen)"
+echo
+
+read -p "Have you created and downloaded an APN key? [y/N]: " -n 1 -r
+echo
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_warning "Please create an APN key first, then run this script again"
+ exit 0
+fi
+
+# Get APN Key ID
+print_section "π APN Key Configuration"
+
+read -p "Enter your APN Key ID (10 characters): " APN_KEY_ID
+while [[ ! $APN_KEY_ID =~ ^[A-Z0-9]{10}$ ]]; do
+ print_error "Invalid Key ID format. Must be 10 characters (letters and numbers)"
+ read -p "Enter APN Key ID: " APN_KEY_ID
+done
+print_success "APN Key ID: $APN_KEY_ID"
+echo
+
+# Locate APN key file
+print_info "Looking for APN .p8 key file..."
+
+# Find .p8 files
+P8_FILES=($(find . -maxdepth 2 -name "AuthKey_*.p8" 2>/dev/null | grep -v "secrets/AuthKey.p8"))
+
+if [ ${#P8_FILES[@]} -eq 0 ]; then
+ print_warning "No APN .p8 key files found in current directory"
+ read -p "Enter the full path to your APN .p8 key file: " APN_P8_PATH
+else
+ echo "Found .p8 files:"
+ for i in "${!P8_FILES[@]}"; do
+ echo " $((i+1)). ${P8_FILES[$i]}"
+ done
+ read -p "Select file number (or enter path to another file): " SELECTION
+
+ if [[ $SELECTION =~ ^[0-9]+$ ]] && [ $SELECTION -ge 1 ] && [ $SELECTION -le ${#P8_FILES[@]} ]; then
+ APN_P8_PATH="${P8_FILES[$((SELECTION-1))]}"
+ else
+ APN_P8_PATH="$SELECTION"
+ fi
+fi
+
+# Validate file exists
+while [ ! -f "$APN_P8_PATH" ]; do
+ print_error "File not found: $APN_P8_PATH"
+ read -p "Enter the full path to your APN .p8 key file: " APN_P8_PATH
+done
+
+# Validate it's a valid P8 key
+if ! grep -q "BEGIN PRIVATE KEY" "$APN_P8_PATH"; then
+ print_error "This doesn't appear to be a valid .p8 key file"
+ exit 1
+fi
+
+# Copy to secrets directory
+cp "$APN_P8_PATH" "secrets/APNAuthKey.p8"
+chmod 600 "secrets/APNAuthKey.p8"
+print_success "Copied APN key to secrets/APNAuthKey.p8"
+echo
+
+# Update shared_keys.env
+print_section "πΎ Updating Configuration"
+
+# Check if APN configuration already exists
+if grep -q "^export APN_KEY_ID=" secrets/shared_keys.env; then
+ print_info "Updating existing APN configuration..."
+ # Update existing values
+ sed -i.bak "s/^export APN_KEY_ID=.*/export APN_KEY_ID=\"$APN_KEY_ID\"/" secrets/shared_keys.env
+ sed -i.bak "s|^export APN_KEY_PATH=.*|export APN_KEY_PATH=\"./secrets/APNAuthKey.p8\"|" secrets/shared_keys.env
+ sed -i.bak "s/^export APN_TEAM_ID=.*/export APN_TEAM_ID=\"$TEAM_ID\"/" secrets/shared_keys.env
+ rm -f secrets/shared_keys.env.bak
+else
+ print_info "Adding APN configuration..."
+ # Add APN configuration block
+ cat >> secrets/shared_keys.env << EOF
+
+# ==============================================================================
+# APN (Apple Push Notification) Configuration
+# ==============================================================================
+# For Firebase Cloud Messaging on iOS
+export APN_KEY_ID="$APN_KEY_ID"
+export APN_KEY_PATH="./secrets/APNAuthKey.p8"
+export APN_TEAM_ID="$TEAM_ID"
+EOF
+fi
+
+print_success "Updated secrets/shared_keys.env with APN configuration"
+echo
+
+# Firebase Console Instructions
+print_section "π₯ Uploading APN Key to Firebase Console"
+
+print_info "You need to upload the APN key to Firebase Console for each iOS app:"
+echo
+echo "Steps:"
+echo " 1. Go to: https://console.firebase.google.com/"
+echo " 2. Select your Firebase project"
+echo " 3. Click the gear icon β Project settings"
+echo " 4. Go to the 'Cloud Messaging' tab"
+echo " 5. Scroll to 'Apple app configuration'"
+echo " 6. Under 'APNs authentication key', click 'Upload'"
+echo " 7. Upload: secrets/APNAuthKey.p8"
+echo " 8. Enter Key ID: $APN_KEY_ID"
+echo " 9. Enter Team ID: $TEAM_ID"
+echo " 10. Click 'Upload'"
+echo
+
+print_warning "You need to do this for EACH iOS app in your Firebase project"
+echo
+
+read -p "Press Enter after uploading to Firebase Console..."
+
+# Verification
+print_section "β
APN Setup Complete!"
+
+print_success "APN key configured successfully!"
+echo
+print_info "Configuration Summary:"
+echo " β APN Key ID: $APN_KEY_ID"
+echo " β APN Team ID: $TEAM_ID"
+echo " β APN Key File: secrets/APNAuthKey.p8"
+echo
+
+print_info "Files created/updated:"
+echo " β secrets/APNAuthKey.p8"
+echo " β secrets/shared_keys.env (updated with APN config)"
+echo
+
+print_warning "IMPORTANT: Keep secrets/APNAuthKey.p8 secure and NEVER commit to git!"
+echo
+
+print_info "Next Steps:"
+echo " 1. Verify setup: bash scripts/verify_apn_setup.sh"
+echo " 2. Test push notifications in your app"
+echo " 3. Check Firebase Console for delivery reports"
+echo
+
+print_success "Happy pushing! π"
diff --git a/scripts/setup_ios_complete.sh b/scripts/setup_ios_complete.sh
new file mode 100755
index 0000000000..8cabd1523a
--- /dev/null
+++ b/scripts/setup_ios_complete.sh
@@ -0,0 +1,517 @@
+#!/bin/bash
+
+# ==============================================================================
+# Complete iOS Deployment Setup Wizard
+# ==============================================================================
+# This script sets up everything needed for iOS deployment:
+# - Shared iOS configuration (Team ID, API keys, Match repo)
+# - Code signing certificates via Fastlane Match
+# - SSH keys for Match repository access
+# ==============================================================================
+
+set -e # Exit on any error
+
+# Color codes
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+MAGENTA='\033[0;35m'
+BOLD='\033[1m'
+NC='\033[0m' # No Color
+
+# Script directory
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+# Print functions
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}β $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}${BOLD}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+print_step() {
+ echo -e "${MAGENTA}${BOLD}βΆ Step $1: $2${NC}"
+}
+
+# Print banner
+clear
+echo -e "${BLUE}${BOLD}"
+cat << "EOF"
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β β
+β π iOS Deployment Setup Wizard β
+β β
+β This wizard will guide you through setting up iOS β
+β deployment infrastructure for your project β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+EOF
+echo -e "${NC}"
+
+# Check prerequisites
+print_section "1οΈβ£ Checking Prerequisites"
+
+# Check macOS
+if [[ "$OSTYPE" != "darwin"* ]]; then
+ print_error "This script must be run on macOS"
+ exit 1
+fi
+print_success "Running on macOS"
+
+# Check Xcode
+if ! command -v xcodebuild &> /dev/null; then
+ print_error "Xcode is not installed"
+ print_info "Install Xcode from the App Store"
+ exit 1
+fi
+XCODE_VERSION=$(xcodebuild -version | head -n 1)
+print_success "$XCODE_VERSION installed"
+
+# Check Ruby
+if ! command -v ruby &> /dev/null; then
+ print_error "Ruby is not installed"
+ exit 1
+fi
+RUBY_VERSION=$(ruby -v | awk '{print $2}')
+print_success "Ruby $RUBY_VERSION installed"
+
+# Check Bundler
+if ! command -v bundle &> /dev/null; then
+ print_warning "Bundler not installed, installing..."
+ gem install bundler
+fi
+print_success "Bundler installed"
+
+# Check Git
+if ! command -v git &> /dev/null; then
+ print_error "Git is not installed"
+ exit 1
+fi
+print_success "Git installed"
+
+# Check GitHub CLI (optional but helpful)
+if ! command -v gh &> /dev/null; then
+ print_warning "GitHub CLI (gh) not installed"
+ print_info "Recommended for easier Match repository setup"
+ print_info "Install: brew install gh"
+else
+ print_success "GitHub CLI installed"
+fi
+
+# Create secrets directory
+print_section "2οΈβ£ Setting Up Secrets Directory"
+
+if [ ! -d "secrets" ]; then
+ mkdir -p secrets
+ print_success "Created secrets/ directory"
+else
+ print_success "secrets/ directory exists"
+fi
+
+# Check if shared_keys.env already exists
+if [ -f "secrets/shared_keys.env" ]; then
+ print_warning "secrets/shared_keys.env already exists"
+ read -p "Do you want to overwrite it? [y/N]: " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_info "Using existing secrets/shared_keys.env"
+ print_info "You can edit it manually or delete it and run this script again"
+ exit 0
+ fi
+fi
+
+# Collect iOS configuration
+print_section "3οΈβ£ Apple Developer Account Information"
+
+print_info "You'll need information from your Apple Developer Account"
+print_info "Log in to: https://developer.apple.com/account"
+echo
+
+# Team ID
+print_step "3.1" "Team ID"
+echo "Find your Team ID at: https://developer.apple.com/account -> Membership"
+read -p "Enter your Apple Team ID (10 characters): " TEAM_ID
+while [[ ! $TEAM_ID =~ ^[A-Z0-9]{10}$ ]]; do
+ print_error "Invalid Team ID format. Must be 10 characters (letters and numbers)"
+ read -p "Enter your Apple Team ID: " TEAM_ID
+done
+print_success "Team ID: $TEAM_ID"
+echo
+
+# App Store Connect API
+print_section "4οΈβ£ App Store Connect API Configuration"
+print_info "You need to create an API key for Fastlane to access App Store Connect"
+echo
+print_info "Steps to create API key:"
+echo " 1. Go to: https://appstoreconnect.apple.com/access/api"
+echo " 2. Click the '+' button to create a new key"
+echo " 3. Name it: 'Fastlane Deploy Key'"
+echo " 4. Select role: 'App Manager' or 'Admin'"
+echo " 5. Click 'Generate'"
+echo " 6. Download the .p8 key file (you can only do this once!)"
+echo " 7. Note the Key ID (10 characters)"
+echo " 8. Note the Issuer ID (UUID format)"
+echo
+
+read -p "Have you created an API key? [y/N]: " -n 1 -r
+echo
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_warning "Please create an API key first, then run this script again"
+ exit 0
+fi
+
+# API Key ID
+print_step "4.1" "App Store Connect Key ID"
+read -p "Enter your App Store Connect Key ID (10 characters): " APPSTORE_KEY_ID
+while [[ ! $APPSTORE_KEY_ID =~ ^[A-Z0-9]{10}$ ]]; do
+ print_error "Invalid Key ID format. Must be 10 characters"
+ read -p "Enter Key ID: " APPSTORE_KEY_ID
+done
+print_success "Key ID: $APPSTORE_KEY_ID"
+
+# Issuer ID
+print_step "4.2" "App Store Connect Issuer ID"
+read -p "Enter your Issuer ID (UUID format): " APPSTORE_ISSUER_ID
+while [[ ! $APPSTORE_ISSUER_ID =~ ^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$ ]]; do
+ print_error "Invalid Issuer ID format. Must be UUID format (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
+ read -p "Enter Issuer ID: " APPSTORE_ISSUER_ID
+done
+print_success "Issuer ID: $APPSTORE_ISSUER_ID"
+
+# API Key file
+print_step "4.3" "App Store Connect .p8 Key File"
+print_info "Looking for .p8 key file in current directory..."
+
+# Find .p8 files
+P8_FILES=($(find . -maxdepth 2 -name "AuthKey_*.p8" -o -name "*.p8" 2>/dev/null))
+
+if [ ${#P8_FILES[@]} -eq 0 ]; then
+ print_warning "No .p8 key files found"
+ read -p "Enter the full path to your .p8 key file: " P8_PATH
+else
+ echo "Found .p8 files:"
+ for i in "${!P8_FILES[@]}"; do
+ echo " $((i+1)). ${P8_FILES[$i]}"
+ done
+ read -p "Select file number (or enter path to another file): " SELECTION
+
+ if [[ $SELECTION =~ ^[0-9]+$ ]] && [ $SELECTION -ge 1 ] && [ $SELECTION -le ${#P8_FILES[@]} ]; then
+ P8_PATH="${P8_FILES[$((SELECTION-1))]}"
+ else
+ P8_PATH="$SELECTION"
+ fi
+fi
+
+# Validate file exists
+while [ ! -f "$P8_PATH" ]; do
+ print_error "File not found: $P8_PATH"
+ read -p "Enter the full path to your .p8 key file: " P8_PATH
+done
+
+# Copy to secrets directory
+cp "$P8_PATH" "secrets/AuthKey.p8"
+chmod 600 "secrets/AuthKey.p8"
+print_success "Copied .p8 key to secrets/AuthKey.p8"
+echo
+
+# Fastlane Match Configuration
+print_section "5οΈβ£ Fastlane Match Configuration"
+print_info "Fastlane Match stores your iOS certificates and provisioning profiles"
+print_info "in a private Git repository for sharing across team and CI/CD"
+echo
+
+print_info "You need a private Git repository for Match. Options:"
+echo " 1. Create new private repo on GitHub/GitLab/Bitbucket"
+echo " 2. Use existing repository"
+echo
+
+# Match repository URL
+print_step "5.1" "Match Repository URL"
+read -p "Enter Match repository URL (git@github.com:org/repo.git): " MATCH_GIT_URL
+while [[ ! $MATCH_GIT_URL =~ ^git@.*\.git$ ]]; do
+ print_error "Invalid Git URL format. Must start with 'git@' and end with '.git'"
+ read -p "Enter Match repository URL: " MATCH_GIT_URL
+done
+print_success "Match Repository: $MATCH_GIT_URL"
+
+# Match branch
+print_step "5.2" "Match Repository Branch"
+read -p "Enter Match repository branch [main]: " MATCH_GIT_BRANCH
+MATCH_GIT_BRANCH=${MATCH_GIT_BRANCH:-main}
+print_success "Match Branch: $MATCH_GIT_BRANCH"
+
+# SSH Key for Match
+print_step "5.3" "SSH Key for Match Repository"
+
+if [ -f "secrets/match_ci_key" ]; then
+ print_warning "SSH key already exists: secrets/match_ci_key"
+ read -p "Do you want to generate a new one? [y/N]: " -n 1 -r
+ echo
+ GENERATE_NEW=$REPLY
+else
+ GENERATE_NEW="y"
+fi
+
+if [[ $GENERATE_NEW =~ ^[Yy]$ ]]; then
+ print_info "Generating new SSH key pair for Match..."
+ ssh-keygen -t ed25519 -C "fastlane-match-$TEAM_ID" -f "secrets/match_ci_key" -N ""
+ chmod 600 "secrets/match_ci_key"
+ chmod 644 "secrets/match_ci_key.pub"
+ print_success "Generated SSH key: secrets/match_ci_key"
+fi
+
+print_info "Public key (add this as deploy key to Match repository):"
+echo
+cat "secrets/match_ci_key.pub"
+echo
+print_warning "IMPORTANT: Add the above public key as a deploy key to your Match repository"
+print_info "Steps:"
+echo " 1. Go to your Match repository on GitHub/GitLab"
+echo " 2. Go to Settings β Deploy Keys"
+echo " 3. Click 'Add deploy key'"
+echo " 4. Title: 'Fastlane Match CI'"
+echo " 5. Paste the public key above"
+echo " 6. Check 'Allow write access' (Match needs to push certificates)"
+echo " 7. Click 'Add key'"
+echo
+
+read -p "Press Enter after adding the deploy key..."
+
+# Test SSH connection
+print_info "Testing SSH connection to Match repository..."
+ssh -i secrets/match_ci_key -o StrictHostKeyChecking=no -T git@github.com 2>&1 | grep -q "successfully authenticated" && print_success "SSH connection successful" || print_warning "Could not verify SSH connection"
+echo
+
+# Match password
+print_step "5.4" "Match Encryption Password"
+print_info "Match encrypts your certificates with a password"
+print_warning "IMPORTANT: Store this password securely! You'll need it on all machines and CI/CD"
+echo
+
+if [ -f "secrets/.match_password" ]; then
+ print_warning "Match password file already exists"
+ read -p "Do you want to generate a new password? [y/N]: " -n 1 -r
+ echo
+ GENERATE_NEW_PWD=$REPLY
+else
+ GENERATE_NEW_PWD="y"
+fi
+
+if [[ $GENERATE_NEW_PWD =~ ^[Yy]$ ]]; then
+ # Generate secure random password
+ MATCH_PASSWORD=$(openssl rand -base64 32)
+ echo "$MATCH_PASSWORD" > "secrets/.match_password"
+ chmod 600 "secrets/.match_password"
+ print_success "Generated Match password (stored in secrets/.match_password)"
+ echo
+ print_warning "Match Password: $MATCH_PASSWORD"
+ print_warning "Save this password in your password manager!"
+else
+ MATCH_PASSWORD=$(cat "secrets/.match_password")
+ print_success "Using existing Match password"
+fi
+echo
+
+# TestFlight Beta Review Contact Information
+print_section "6οΈβ£ TestFlight Beta Review Contact Information"
+print_info "This information is shown to Apple reviewers during beta review"
+echo
+
+read -p "TestFlight Contact Email: " TESTFLIGHT_CONTACT_EMAIL
+TESTFLIGHT_CONTACT_EMAIL=${TESTFLIGHT_CONTACT_EMAIL:-team@example.com}
+
+read -p "TestFlight Contact First Name: " TESTFLIGHT_FIRST_NAME
+TESTFLIGHT_FIRST_NAME=${TESTFLIGHT_FIRST_NAME:-Team}
+
+read -p "TestFlight Contact Last Name: " TESTFLIGHT_LAST_NAME
+TESTFLIGHT_LAST_NAME=${TESTFLIGHT_LAST_NAME:-Name}
+
+read -p "TestFlight Contact Phone (with country code): " TESTFLIGHT_PHONE
+TESTFLIGHT_PHONE=${TESTFLIGHT_PHONE:-+1234567890}
+
+read -p "Beta Feedback Email: " BETA_FEEDBACK_EMAIL
+BETA_FEEDBACK_EMAIL=${BETA_FEEDBACK_EMAIL:-$TESTFLIGHT_CONTACT_EMAIL}
+
+read -p "TestFlight Tester Groups (comma-separated): " TESTFLIGHT_GROUPS
+TESTFLIGHT_GROUPS=${TESTFLIGHT_GROUPS:-internal-testers}
+
+print_success "TestFlight configuration collected"
+echo
+
+# App Store Review Contact Information
+print_section "7οΈβ£ App Store Review Contact Information"
+print_info "This information is shown to Apple reviewers during App Store review"
+echo
+
+read -p "App Store Review First Name [$TESTFLIGHT_FIRST_NAME]: " APPSTORE_REVIEW_FIRST_NAME
+APPSTORE_REVIEW_FIRST_NAME=${APPSTORE_REVIEW_FIRST_NAME:-$TESTFLIGHT_FIRST_NAME}
+
+read -p "App Store Review Last Name [$TESTFLIGHT_LAST_NAME]: " APPSTORE_REVIEW_LAST_NAME
+APPSTORE_REVIEW_LAST_NAME=${APPSTORE_REVIEW_LAST_NAME:-$TESTFLIGHT_LAST_NAME}
+
+read -p "App Store Review Phone [$TESTFLIGHT_PHONE]: " APPSTORE_REVIEW_PHONE
+APPSTORE_REVIEW_PHONE=${APPSTORE_REVIEW_PHONE:-$TESTFLIGHT_PHONE}
+
+read -p "App Store Review Email [$TESTFLIGHT_CONTACT_EMAIL]: " APPSTORE_REVIEW_EMAIL
+APPSTORE_REVIEW_EMAIL=${APPSTORE_REVIEW_EMAIL:-$TESTFLIGHT_CONTACT_EMAIL}
+
+print_success "App Store review configuration collected"
+echo
+
+# Write shared_keys.env
+print_section "8οΈβ£ Generating Configuration File"
+
+cat > secrets/shared_keys.env << EOF
+# ==============================================================================
+# Shared iOS Keys - Generated by setup_ios_complete.sh
+# ==============================================================================
+# These keys are SHARED across all your iOS apps.
+# Load this file before running deployment scripts: source secrets/shared_keys.env
+# ==============================================================================
+
+# Apple Developer Team ID
+export TEAM_ID="$TEAM_ID"
+
+# App Store Connect API (Shared across all apps)
+export APPSTORE_KEY_ID="$APPSTORE_KEY_ID"
+export APPSTORE_ISSUER_ID="$APPSTORE_ISSUER_ID"
+export APPSTORE_KEY_PATH="./secrets/AuthKey.p8"
+
+# Fastlane Match Configuration (Shared certificate repository)
+export MATCH_GIT_URL="$MATCH_GIT_URL"
+export MATCH_GIT_BRANCH="$MATCH_GIT_BRANCH"
+export MATCH_SSH_KEY_PATH="./secrets/match_ci_key"
+
+# Match password is stored in: secrets/.match_password
+# Load it with: export MATCH_PASSWORD=\$(cat secrets/.match_password)
+
+# TestFlight Beta Review Configuration
+export TESTFLIGHT_CONTACT_EMAIL="$TESTFLIGHT_CONTACT_EMAIL"
+export TESTFLIGHT_FIRST_NAME="$TESTFLIGHT_FIRST_NAME"
+export TESTFLIGHT_LAST_NAME="$TESTFLIGHT_LAST_NAME"
+export TESTFLIGHT_PHONE="$TESTFLIGHT_PHONE"
+export TESTFLIGHT_DEMO_EMAIL=""
+export TESTFLIGHT_DEMO_PASSWORD=""
+
+# Beta Feedback Configuration
+export BETA_FEEDBACK_EMAIL="$BETA_FEEDBACK_EMAIL"
+
+# TestFlight Tester Groups (comma-separated)
+export TESTFLIGHT_GROUPS="$TESTFLIGHT_GROUPS"
+
+# App Store Review Configuration
+export APPSTORE_REVIEW_FIRST_NAME="$APPSTORE_REVIEW_FIRST_NAME"
+export APPSTORE_REVIEW_LAST_NAME="$APPSTORE_REVIEW_LAST_NAME"
+export APPSTORE_REVIEW_PHONE="$APPSTORE_REVIEW_PHONE"
+export APPSTORE_REVIEW_EMAIL="$APPSTORE_REVIEW_EMAIL"
+export APPSTORE_DEMO_EMAIL=""
+export APPSTORE_DEMO_PASSWORD=""
+
+# App Marketing URLs (customize per app or use shared defaults)
+export APP_MARKETING_URL="https://example.com"
+export APP_PRIVACY_URL="https://example.com/privacy"
+export APP_SUPPORT_URL="https://example.com/support"
+
+# Firebase Configuration (same file for all apps)
+export FIREBASE_SERVICE_CREDS="./secrets/firebaseAppDistributionServiceCredentialsFile.json"
+
+# ==============================================================================
+# Per-App Configuration (DON'T SET HERE - managed by fastlane-config)
+# ==============================================================================
+# These are set in fastlane-config/project_config.rb:
+# - APP_IDENTIFIER (iOS Bundle ID)
+# - FIREBASE_IOS_APP_ID
+# The customizer.sh script updates these when creating a new project.
+EOF
+
+chmod 600 secrets/shared_keys.env
+print_success "Created secrets/shared_keys.env"
+echo
+
+# Initialize Match
+print_section "9οΈβ£ Initializing Fastlane Match"
+print_info "This will sync your code signing certificates from the Match repository"
+print_info "If this is the first time, Match will create new certificates"
+echo
+
+# Load configuration
+source secrets/shared_keys.env
+export MATCH_PASSWORD
+
+# Install Fastlane
+print_info "Installing Fastlane dependencies..."
+bundle install
+
+# Run Match for adhoc
+print_info "Syncing adhoc certificates..."
+bundle exec fastlane ios sync_certificates match_type:adhoc || print_warning "Match sync encountered issues (this is normal for first run)"
+
+# Run Match for appstore
+print_info "Syncing appstore certificates..."
+bundle exec fastlane ios sync_certificates match_type:appstore || print_warning "Match sync encountered issues (this is normal for first run)"
+
+echo
+
+# Summary
+print_section "β
Setup Complete!"
+
+print_success "iOS deployment infrastructure is configured!"
+echo
+print_info "Configuration Summary:"
+echo " β Team ID: $TEAM_ID"
+echo " β App Store Connect API configured"
+echo " β Match repository: $MATCH_GIT_URL"
+echo " β SSH key generated and configured"
+echo " β Match password generated"
+echo " β TestFlight & App Store review info configured"
+echo
+
+print_info "Configuration files created:"
+echo " β secrets/shared_keys.env"
+echo " β secrets/AuthKey.p8"
+echo " β secrets/match_ci_key"
+echo " β secrets/.match_password"
+echo
+
+print_warning "IMPORTANT: Keep these files secure and NEVER commit them to git!"
+print_info "They are already in .gitignore"
+echo
+
+print_info "Next Steps:"
+echo " 1. Test deployment to Firebase:"
+echo " bash scripts/deploy_firebase.sh"
+echo
+echo " 2. Test deployment to TestFlight:"
+echo " bash scripts/deploy_testflight.sh"
+echo
+echo " 3. Deploy to App Store (when ready):"
+echo " bash scripts/deploy_appstore.sh"
+echo
+
+print_info "Optional: Setup APN for push notifications"
+echo " bash scripts/setup_apn_key.sh"
+echo
+
+print_success "Happy deploying! π"
diff --git a/scripts/verify_apn_setup.sh b/scripts/verify_apn_setup.sh
new file mode 100755
index 0000000000..04ed11f78b
--- /dev/null
+++ b/scripts/verify_apn_setup.sh
@@ -0,0 +1,220 @@
+#!/bin/bash
+
+# ==============================================================================
+# APN Setup Verification Script
+# ==============================================================================
+# This script verifies that your APN configuration is correct
+# ==============================================================================
+
+set -e # Exit on any error
+
+# Color codes
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+# Script directory
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+# Print functions
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}β $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+# Print banner
+print_section "π APN Configuration Verification"
+
+ERRORS=0
+WARNINGS=0
+
+# Check if shared_keys.env exists
+print_info "Checking configuration files..."
+if [ ! -f "secrets/shared_keys.env" ]; then
+ print_error "secrets/shared_keys.env not found"
+ print_info "Run: bash scripts/setup_ios_complete.sh"
+ exit 1
+fi
+print_success "secrets/shared_keys.env exists"
+
+# Load configuration
+source secrets/shared_keys.env
+
+# Check APN configuration in shared_keys.env
+print_section "π Checking APN Configuration"
+
+if [ -z "$APN_KEY_ID" ]; then
+ print_error "APN_KEY_ID not set in secrets/shared_keys.env"
+ print_info "Run: bash scripts/setup_apn_key.sh"
+ ((ERRORS++))
+else
+ # Validate format (10 characters)
+ if [[ ! $APN_KEY_ID =~ ^[A-Z0-9]{10}$ ]]; then
+ print_error "APN_KEY_ID has invalid format: $APN_KEY_ID"
+ print_info "Must be 10 characters (letters and numbers)"
+ ((ERRORS++))
+ else
+ print_success "APN_KEY_ID format valid: $APN_KEY_ID"
+ fi
+fi
+
+if [ -z "$APN_TEAM_ID" ]; then
+ print_error "APN_TEAM_ID not set in secrets/shared_keys.env"
+ ((ERRORS++))
+else
+ print_success "APN_TEAM_ID set: $APN_TEAM_ID"
+
+ # Check if it matches TEAM_ID
+ if [ "$APN_TEAM_ID" != "$TEAM_ID" ]; then
+ print_warning "APN_TEAM_ID ($APN_TEAM_ID) differs from TEAM_ID ($TEAM_ID)"
+ print_info "They should usually be the same"
+ ((WARNINGS++))
+ fi
+fi
+
+if [ -z "$APN_KEY_PATH" ]; then
+ print_error "APN_KEY_PATH not set in secrets/shared_keys.env"
+ ((ERRORS++))
+else
+ print_success "APN_KEY_PATH set: $APN_KEY_PATH"
+fi
+
+# Check APN key file
+print_section "π Checking APN Key File"
+
+APN_KEY_FILE="secrets/APNAuthKey.p8"
+
+if [ ! -f "$APN_KEY_FILE" ]; then
+ print_error "APN key file not found: $APN_KEY_FILE"
+ print_info "Run: bash scripts/setup_apn_key.sh"
+ ((ERRORS++))
+else
+ print_success "APN key file exists: $APN_KEY_FILE"
+
+ # Check file permissions
+ PERMS=$(stat -f "%A" "$APN_KEY_FILE" 2>/dev/null || stat -c "%a" "$APN_KEY_FILE" 2>/dev/null)
+ if [ "$PERMS" != "600" ]; then
+ print_warning "APN key file permissions: $PERMS (should be 600)"
+ print_info "Fix with: chmod 600 $APN_KEY_FILE"
+ ((WARNINGS++))
+ else
+ print_success "File permissions correct: 600"
+ fi
+
+ # Validate it's a valid P8 key
+ if ! grep -q "BEGIN PRIVATE KEY" "$APN_KEY_FILE"; then
+ print_error "File doesn't appear to be a valid .p8 key"
+ ((ERRORS++))
+ else
+ print_success "Valid P8 key format detected"
+ fi
+
+ # Check file size (should be around 200-300 bytes for a valid key)
+ FILE_SIZE=$(wc -c < "$APN_KEY_FILE" | tr -d ' ')
+ if [ "$FILE_SIZE" -lt 100 ] || [ "$FILE_SIZE" -gt 500 ]; then
+ print_warning "APN key file size unusual: $FILE_SIZE bytes"
+ print_info "Valid P8 keys are typically 200-300 bytes"
+ ((WARNINGS++))
+ else
+ print_success "File size looks reasonable: $FILE_SIZE bytes"
+ fi
+fi
+
+# Check if key filename matches Key ID
+if [ -f "$APN_KEY_FILE" ] && [ -n "$APN_KEY_ID" ]; then
+ EXPECTED_NAME="AuthKey_$APN_KEY_ID.p8"
+ if [ "$(basename $APN_KEY_FILE)" != "$EXPECTED_NAME" ]; then
+ print_warning "APN key filename: $(basename $APN_KEY_FILE)"
+ print_info "Expected filename based on Key ID: $EXPECTED_NAME"
+ print_info "This is usually OK, but verify the Key ID is correct"
+ ((WARNINGS++))
+ fi
+fi
+
+# Firebase Console Check
+print_section "π₯ Firebase Console Integration"
+
+print_info "Manual verification needed in Firebase Console:"
+echo " 1. Go to: https://console.firebase.google.com/"
+echo " 2. Select your project"
+echo " 3. Settings β Cloud Messaging tab"
+echo " 4. Check 'APNs authentication key' section"
+echo " 5. Verify:"
+if [ -n "$APN_KEY_ID" ]; then
+ echo " - Key ID matches: $APN_KEY_ID"
+fi
+if [ -n "$APN_TEAM_ID" ]; then
+ echo " - Team ID matches: $APN_TEAM_ID"
+fi
+echo
+
+read -p "Have you verified the APN key in Firebase Console? [y/N]: " -n 1 -r
+echo
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ print_warning "Please verify APN configuration in Firebase Console"
+ ((WARNINGS++))
+else
+ print_success "Firebase Console verification confirmed"
+fi
+
+# Summary
+print_section "π Verification Summary"
+
+if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
+ print_success "β
All checks passed!"
+ echo
+ print_info "Your APN configuration is correct and ready to use"
+ echo
+ print_info "Next Steps:"
+ echo " 1. Test push notifications in your app"
+ echo " 2. Check Firebase Console β Cloud Messaging β Send test message"
+ echo " 3. Monitor delivery reports in Firebase Console"
+elif [ $ERRORS -eq 0 ]; then
+ print_warning "β οΈ Configuration OK with $WARNINGS warning(s)"
+ echo
+ print_info "Your APN configuration should work, but review the warnings above"
+else
+ print_error "β Found $ERRORS error(s) and $WARNINGS warning(s)"
+ echo
+ print_info "Please fix the errors above before using APN"
+ print_info "Run: bash scripts/setup_apn_key.sh"
+ exit 1
+fi
+
+print_info "Configuration Details:"
+if [ -n "$APN_KEY_ID" ]; then
+ echo " β’ APN Key ID: $APN_KEY_ID"
+fi
+if [ -n "$APN_TEAM_ID" ]; then
+ echo " β’ APN Team ID: $APN_TEAM_ID"
+fi
+if [ -f "$APN_KEY_FILE" ]; then
+ echo " β’ APN Key File: $APN_KEY_FILE"
+fi
+echo
+
+print_success "Verification complete! π"
diff --git a/scripts/verify_ios_deployment.sh b/scripts/verify_ios_deployment.sh
new file mode 100755
index 0000000000..7b27893576
--- /dev/null
+++ b/scripts/verify_ios_deployment.sh
@@ -0,0 +1,364 @@
+#!/bin/bash
+
+# ==============================================================================
+# iOS Deployment Verification Script
+# ==============================================================================
+# This script performs comprehensive verification of iOS deployment configuration
+# ==============================================================================
+
+set -e
+
+# Color codes for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+# Script directory and project root
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$PROJECT_ROOT"
+
+# Print functions
+print_success() {
+ echo -e "${GREEN}β $1${NC}"
+}
+
+print_error() {
+ echo -e "${RED}β $1${NC}"
+}
+
+print_warning() {
+ echo -e "${YELLOW}β $1${NC}"
+}
+
+print_info() {
+ echo -e "${CYAN}βΉ $1${NC}"
+}
+
+print_section() {
+ echo
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo -e "${BLUE}$1${NC}"
+ echo -e "${BLUE}ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}"
+ echo
+}
+
+# Initialize counters
+TOTAL_CHECKS=0
+PASSED_CHECKS=0
+FAILED_CHECKS=0
+WARNING_CHECKS=0
+
+# Check function
+check() {
+ TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
+ if eval "$2"; then
+ print_success "$1"
+ PASSED_CHECKS=$((PASSED_CHECKS + 1))
+ return 0
+ else
+ print_error "$1"
+ FAILED_CHECKS=$((FAILED_CHECKS + 1))
+ return 1
+ fi
+}
+
+# Warning function
+warn() {
+ TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
+ if eval "$2"; then
+ print_success "$1"
+ PASSED_CHECKS=$((PASSED_CHECKS + 1))
+ return 0
+ else
+ print_warning "$1 (optional)"
+ WARNING_CHECKS=$((WARNING_CHECKS + 1))
+ return 0
+ fi
+}
+
+# Print banner
+print_section "π iOS Deployment Verification"
+
+# ============================================================================
+# 1. Version Handling Verification
+# ============================================================================
+print_section "1. Version Handling"
+
+check "Version check script exists" \
+ "[ -f 'scripts/check_ios_version.sh' ]"
+
+check "version.txt can be generated" \
+ "cd '$PROJECT_ROOT' && ./gradlew versionFile > /dev/null 2>&1 && [ -f 'version.txt' ]"
+
+if [ -f "version.txt" ]; then
+ FULL_VERSION=$(cat version.txt)
+ print_info "Current version: $FULL_VERSION"
+
+ # Verify version format
+ if [[ "$FULL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
+ print_success "Version format is valid"
+ PASSED_CHECKS=$((PASSED_CHECKS + 1))
+ else
+ print_error "Version format is invalid"
+ FAILED_CHECKS=$((FAILED_CHECKS + 1))
+ fi
+ TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
+fi
+
+check "Info.plist uses MARKETING_VERSION variable" \
+ "grep -q '\$(MARKETING_VERSION)' 'cmp-ios/iosApp/Info.plist'"
+
+check "Info.plist uses CURRENT_PROJECT_VERSION variable" \
+ "grep -q '\$(CURRENT_PROJECT_VERSION)' 'cmp-ios/iosApp/Info.plist'"
+
+check "Info.plist has no hardcoded CFBundleShortVersionString" \
+ "! grep -A1 'CFBundleShortVersionString' 'cmp-ios/iosApp/Info.plist' | grep -q '[0-9]'"
+
+check "Xcode project has MARKETING_VERSION in Debug config" \
+ "grep -q 'MARKETING_VERSION' 'cmp-ios/iosApp.xcodeproj/project.pbxproj'"
+
+check "Xcode project has CURRENT_PROJECT_VERSION in Debug config" \
+ "grep -q 'CURRENT_PROJECT_VERSION' 'cmp-ios/iosApp.xcodeproj/project.pbxproj'"
+
+# ============================================================================
+# 2. FastFile Configuration
+# ============================================================================
+print_section "2. FastFile Configuration"
+
+check "FastFile exists" \
+ "[ -f 'fastlane/FastFile' ]"
+
+check "Version sanitization helper exists" \
+ "grep -q 'get_version_from_gradle' 'fastlane/FastFile'"
+
+check "Version sanitization has sanitize_for_appstore parameter" \
+ "grep -q 'sanitize_for_appstore' 'fastlane/FastFile'"
+
+check "Firebase lane exists" \
+ "grep -q 'lane :deploy_on_firebase' 'fastlane/FastFile'"
+
+check "TestFlight beta lane exists" \
+ "grep -q 'lane :beta' 'fastlane/FastFile'"
+
+check "App Store release lane exists" \
+ "grep -q 'lane :release' 'fastlane/FastFile'"
+
+check "Release lane generates release notes" \
+ "grep -q 'generateReleaseNote()' 'fastlane/FastFile'"
+
+check "Release lane creates metadata directory" \
+ "grep -q 'FileUtils.mkdir_p' 'fastlane/FastFile'"
+
+check "Release lane writes release_notes.txt" \
+ "grep -q 'File.write(release_notes_path' 'fastlane/FastFile'"
+
+check "Release lane has copyright parameter" \
+ "grep -q 'copyright:' 'fastlane/FastFile'"
+
+check "Deliver action has skip_metadata: false" \
+ "grep -q 'skip_metadata: false' 'fastlane/FastFile'"
+
+check "Deliver action has skip_screenshots: true" \
+ "grep -q 'skip_screenshots: true' 'fastlane/FastFile'"
+
+# ============================================================================
+# 3. Configuration Files
+# ============================================================================
+print_section "3. Configuration Files"
+
+check "project_config.rb exists" \
+ "[ -f 'fastlane-config/project_config.rb' ]"
+
+check "ios_config.rb exists" \
+ "[ -f 'fastlane-config/ios_config.rb' ]"
+
+check "project_config.rb defines IOS hash" \
+ "grep -q 'IOS = {' 'fastlane-config/project_config.rb'"
+
+check "project_config.rb defines IOS_SHARED hash" \
+ "grep -q 'IOS_SHARED = {' 'fastlane-config/project_config.rb'"
+
+check "project_config.rb has app_identifier" \
+ "grep -q 'app_identifier:' 'fastlane-config/project_config.rb'"
+
+check "project_config.rb has metadata_path" \
+ "grep -q 'metadata_path:' 'fastlane-config/project_config.rb'"
+
+check "skip_app_version_update is false (allows version creation)" \
+ "grep -q 'skip_app_version_update: false' 'fastlane-config/project_config.rb'"
+
+# ============================================================================
+# 4. Export Compliance
+# ============================================================================
+print_section "4. Export Compliance"
+
+check "Info.plist has ITSAppUsesNonExemptEncryption key" \
+ "grep -q 'ITSAppUsesNonExemptEncryption' 'cmp-ios/iosApp/Info.plist'"
+
+check "ITSAppUsesNonExemptEncryption is set to false" \
+ "grep -A1 'ITSAppUsesNonExemptEncryption' 'cmp-ios/iosApp/Info.plist' | grep -q ''"
+
+check "TestFlight lane has uses_non_exempt_encryption parameter" \
+ "grep -q 'uses_non_exempt_encryption:' 'fastlane/FastFile'"
+
+# ============================================================================
+# 5. Required Secret Files
+# ============================================================================
+print_section "5. Required Secret Files"
+
+check "secrets directory exists" \
+ "[ -d 'secrets' ]"
+
+warn "shared_keys.env exists" \
+ "[ -f 'secrets/shared_keys.env' ]"
+
+warn ".match_password exists" \
+ "[ -f 'secrets/.match_password' ]"
+
+warn "match_ci_key (SSH key) exists" \
+ "[ -f 'secrets/match_ci_key' ]"
+
+warn "AuthKey.p8 (App Store Connect API) exists" \
+ "[ -f 'secrets/AuthKey.p8' ]"
+
+warn "Firebase credentials exist" \
+ "[ -f 'secrets/firebaseAppDistributionServiceCredentialsFile.json' ]"
+
+# ============================================================================
+# 6. Deployment Scripts
+# ============================================================================
+print_section "6. Deployment Scripts"
+
+check "deploy_firebase.sh exists" \
+ "[ -f 'scripts/deploy_firebase.sh' ]"
+
+check "deploy_testflight.sh exists" \
+ "[ -f 'scripts/deploy_testflight.sh' ]"
+
+check "deploy_appstore.sh exists" \
+ "[ -f 'scripts/deploy_appstore.sh' ]"
+
+check "check_ios_version.sh exists" \
+ "[ -f 'scripts/check_ios_version.sh' ]"
+
+check "deploy_firebase.sh is executable" \
+ "[ -x 'scripts/deploy_firebase.sh' ]"
+
+check "deploy_testflight.sh is executable" \
+ "[ -x 'scripts/deploy_testflight.sh' ]"
+
+check "deploy_appstore.sh is executable" \
+ "[ -x 'scripts/deploy_appstore.sh' ]"
+
+check "check_ios_version.sh is executable" \
+ "[ -x 'scripts/check_ios_version.sh' ]"
+
+# ============================================================================
+# 7. Documentation
+# ============================================================================
+print_section "7. Documentation"
+
+check "IOS_DEPLOYMENT.md exists" \
+ "[ -f 'docs/IOS_DEPLOYMENT.md' ]"
+
+check "IOS_SETUP.md exists" \
+ "[ -f 'docs/IOS_SETUP.md' ]"
+
+check "IOS_DEPLOYMENT_CHECKLIST.md exists" \
+ "[ -f 'docs/IOS_DEPLOYMENT_CHECKLIST.md' ]"
+
+check "FASTLANE_CONFIGURATION.md exists" \
+ "[ -f 'docs/FASTLANE_CONFIGURATION.md' ]"
+
+# ============================================================================
+# 8. Metadata Configuration
+# ============================================================================
+print_section "8. Metadata Configuration"
+
+check "metadata directory is in .gitignore" \
+ "grep -q '^fastlane/metadata' '.gitignore' || grep -q '^fastlane/metadata$' '.gitignore'"
+
+check "screenshots directory is in .gitignore" \
+ "grep -q '^fastlane/screenshots' '.gitignore' || grep -q '^fastlane/screenshots$' '.gitignore'"
+
+# ============================================================================
+# 9. Ruby & Bundler
+# ============================================================================
+print_section "9. Ruby & Bundler"
+
+check "Ruby is installed" \
+ "command -v ruby > /dev/null 2>&1"
+
+check "Bundler is installed" \
+ "command -v bundle > /dev/null 2>&1"
+
+check "Gemfile exists" \
+ "[ -f 'Gemfile' ]"
+
+check "Gemfile.lock exists" \
+ "[ -f 'Gemfile.lock' ]"
+
+warn "Bundle dependencies are installed" \
+ "bundle check > /dev/null 2>&1"
+
+# ============================================================================
+# 10. Xcode Configuration
+# ============================================================================
+print_section "10. Xcode Configuration"
+
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ check "Xcode is installed" \
+ "command -v xcodebuild > /dev/null 2>&1"
+
+ check "Xcode project exists" \
+ "[ -d 'cmp-ios/iosApp.xcodeproj' ]"
+
+ check "Xcode workspace exists" \
+ "[ -d 'cmp-ios/iosApp.xcworkspace' ]"
+else
+ print_warning "Skipping Xcode checks (not running on macOS)"
+fi
+
+# ============================================================================
+# Summary
+# ============================================================================
+print_section "π Verification Summary"
+
+echo "Total Checks: $TOTAL_CHECKS"
+print_success "Passed: $PASSED_CHECKS"
+if [ $FAILED_CHECKS -gt 0 ]; then
+ print_error "Failed: $FAILED_CHECKS"
+fi
+if [ $WARNING_CHECKS -gt 0 ]; then
+ print_warning "Warnings: $WARNING_CHECKS (optional checks)"
+fi
+echo
+
+# Calculate pass rate
+PASS_RATE=$(awk "BEGIN {printf \"%.1f\", ($PASSED_CHECKS / $TOTAL_CHECKS) * 100}")
+
+if [ $FAILED_CHECKS -eq 0 ]; then
+ print_section "β
All Critical Checks Passed!"
+ echo "Pass Rate: ${PASS_RATE}%"
+ echo
+ print_success "iOS deployment configuration is ready for automatic deployment!"
+ echo
+ if [ $WARNING_CHECKS -gt 0 ]; then
+ print_info "Note: ${WARNING_CHECKS} optional checks failed (secret files)"
+ print_info "These are expected if you haven't set up secrets yet"
+ print_info "Run: bash scripts/setup_ios_complete.sh"
+ fi
+ exit 0
+else
+ print_section "β Verification Failed"
+ echo "Pass Rate: ${PASS_RATE}%"
+ echo
+ print_error "${FAILED_CHECKS} critical check(s) failed"
+ print_info "Please review the errors above and fix the issues"
+ print_info "See docs/IOS_DEPLOYMENT_CHECKLIST.md for detailed troubleshooting"
+ exit 1
+fi