Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ddfbc32
fix(fastlane): Incorporated iOS related changes from template.
techsavvy185 Mar 4, 2026
7dc51d2
Synchronised all files with template.
techsavvy185 Mar 5, 2026
620b17d
Sync iOS fastlane.
techsavvy185 Mar 6, 2026
f73c092
Revert Gemfile.lock to the previous version
techsavvy185 Mar 6, 2026
a01a152
Merge branch 'development' into fastlaneIOS
techsavvy185 Mar 11, 2026
527c6d4
Merge branch 'development' into fastlaneIOS
biplab1 Mar 16, 2026
233faaf
Apply suggestion from @biplab1
techsavvy185 Mar 16, 2026
a780395
Incorporated suggestions.
techsavvy185 Mar 16, 2026
76c82c3
Merge remote-tracking branch 'origin/fastlaneIOS' into fastlaneIOS
techsavvy185 Mar 16, 2026
3747c61
Incorporated suggestions.
techsavvy185 Mar 19, 2026
670ad5e
Merge branch 'development' into fastlaneIOS
techsavvy185 Mar 19, 2026
26c6963
Minor Changes.
techsavvy185 Mar 21, 2026
c19f38d
Minor Changes.
techsavvy185 Mar 23, 2026
77271a7
Minor Changes.
techsavvy185 Mar 24, 2026
af634c0
Merge branch 'development' into fastlaneIOS
techsavvy185 Mar 24, 2026
b510c8c
Merge branch 'development' into fastlaneIOS
niyajali Mar 26, 2026
5d0be19
Merge branch 'development' into fastlaneIOS
techsavvy185 Apr 9, 2026
0ed4562
Merge branch 'development' into fastlaneIOS
techsavvy185 Apr 20, 2026
4168ec2
Incorporated suggestions.
techsavvy185 May 7, 2026
814d004
Merge remote-tracking branch 'origin/fastlaneIOS' into fastlaneIOS
techsavvy185 May 7, 2026
37548bc
Merge branch 'development' into fastlaneIOS
techsavvy185 May 11, 2026
f234577
Merge branch 'development' into fastlaneIOS
niyajali May 12, 2026
234a839
Changed the language to en-US in project_config.rb
techsavvy185 May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fastlane-config/ios_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module IosConfig
version_number: ProjectConfig::IOS[:version_number],
metadata_path: ProjectConfig::IOS[:metadata_path],
app_rating_config_path: ProjectConfig::IOS[:age_rating_config_path],
primary_locale: ProjectConfig::IOS[:primary_locale],

# Shared (from IOS_SHARED)
team_id: ProjectConfig::IOS_SHARED[:team_id],
Expand Down
5 changes: 4 additions & 1 deletion fastlane-config/project_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ module ProjectConfig
metadata_path: "./fastlane/metadata",
age_rating_config_path: "./fastlane/age_rating.json",

# Primary locale — must match App Store Connect primary language
primary_locale: "en-GB",
Comment thread
techsavvy185 marked this conversation as resolved.
Outdated

# Version configuration (fallback only - actual version read from version.txt)
# The versionFile gradle task generates version.txt from project.version
# Fastlane lanes read version.txt to sync iOS version with Android
Expand Down Expand Up @@ -160,7 +163,7 @@ module ProjectConfig
privacy_policy_url: ENV['APP_PRIVACY_URL'] || "https://mifos.org/privacy",
description: "Kotlin Multiplatform mobile application"
},
"en-US" => {
"en-GB" => {
Comment thread
techsavvy185 marked this conversation as resolved.
Outdated
feedback_email: ENV['BETA_FEEDBACK_EMAIL'] || "team@mifos.org",
marketing_url: ENV['APP_MARKETING_URL'] || "https://mifos.org",
privacy_policy_url: ENV['APP_PRIVACY_URL'] || "https://mifos.org/privacy",
Expand Down
126 changes: 106 additions & 20 deletions fastlane/FastFile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ require_relative File.join(project_dir, 'fastlane-config', 'android_config')
require_relative File.join(project_dir, 'fastlane-config', 'ios_config')
require_relative './config/config_helpers'

# Helper: reject empty-string option values so || fallbacks work correctly.
# GitHub Actions custom actions pass "" for unset optional parameters,
# but Ruby treats "" as truthy ("" || "fallback" returns "", not "fallback").
def sanitize_options(options)
options.reject { |_, v| v.is_a?(String) && v.strip.empty? }
end

default_platform(:android)

platform :android do
Expand Down Expand Up @@ -325,6 +332,7 @@ platform :ios do
#############################

private_lane :setup_ci_if_needed do |options|
options = sanitize_options(options)
unless ENV['CI']
UI.message("🖥️ Running locally, skipping CI-specific setup.")
else
Expand All @@ -336,6 +344,7 @@ platform :ios do
end

private_lane :load_api_key do |options|
options = sanitize_options(options)
ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG

app_store_connect_api_key(
Expand All @@ -347,6 +356,7 @@ platform :ios do
end

private_lane :fetch_certificates_with_match do |options|
options = sanitize_options(options)
ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
match(
type: options[:match_type] || ios_config[:match_type],
Expand All @@ -361,6 +371,7 @@ platform :ios do
end

private_lane :build_ios_project do |options|
options = sanitize_options(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]
Expand Down Expand Up @@ -435,6 +446,13 @@ platform :ios do
desc "Build Ios application"
lane :build_ios do |options|
ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG

# 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],
Expand All @@ -447,6 +465,13 @@ platform :ios do

desc "Build Signed Ios application"
lane :build_signed_ios do |options|
options = sanitize_options(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)
Expand Down Expand Up @@ -493,6 +518,7 @@ platform :ios do

desc "Upload iOS application to Firebase App Distribution"
lane :deploy_on_firebase do |options|
options = sanitize_options(options)
firebase_config = FastlaneConfig.get_firebase_config(:ios)
ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG

Expand All @@ -517,30 +543,58 @@ platform :ios do

desc "Upload beta build to TestFlight"
lane :beta do |options|
options = sanitize_options(options)
ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
testflight_config = FastlaneConfig::IosConfig::TESTFLIGHT_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")
)

# Get version from gradle (sanitized for App Store)
version = get_version_from_gradle(sanitize_for_appstore: true)
gradle_version = get_version_from_gradle(sanitize_for_appstore: true)

# Increment version number (sanitized for App Store)
increment_version_number(
xcodeproj: ios_config[:project_path],
version_number: options[:version_number] || version
)

# Get latest TestFlight build number and increment
# Get latest TestFlight build number and version FIRST
# so we can ensure our version is always higher
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]
)
latest_version = Actions.lane_context[SharedValues::LATEST_TESTFLIGHT_VERSION]

# Determine final version: must be higher than latest TestFlight version
version = options[:version_number] || gradle_version
if latest_version && !latest_version.to_s.empty?
begin
if Gem::Version.new(version) <= Gem::Version.new(latest_version)
# Bump: increment patch of latest TestFlight version
parts = latest_version.split('.')
parts[-1] = (parts[-1].to_i + 1).to_s
bumped = parts.join('.')
UI.important("⚠️ Gradle version #{version} <= TestFlight #{latest_version}, bumping to #{bumped}")
version = bumped
end
rescue ArgumentError => e
UI.error("Version comparison failed: #{e.message}, using gradle version #{version}")
end
end
UI.important("📱 Final App Store version: #{version}")

# Set version in Xcode project
increment_version_number(
xcodeproj: ios_config[:project_path],
version_number: version
)

# Increment build number
increment_build_number(
xcodeproj: ios_config[:project_path],
build_number: latest_build_number + 1
Expand All @@ -557,11 +611,12 @@ platform :ios do
releaseNotes = generateReleaseNote()

# Prepare localized build info with changelog
locale = ios_config[:primary_locale]
localized_build_info = {
"default" => {
whats_new: releaseNotes
},
"en-US" => {
locale => {
whats_new: releaseNotes
}
}
Expand Down Expand Up @@ -614,30 +669,57 @@ platform :ios do

desc "Upload iOS Application to App Store"
lane :release do |options|
options = sanitize_options(options)
ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG
appstore_config = FastlaneConfig::IosConfig::APPSTORE_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")
)

# Get version from gradle (sanitized for App Store)
version = get_version_from_gradle(sanitize_for_appstore: true)

# Increment version number (sanitized for App Store)
increment_version_number(
xcodeproj: ios_config[:project_path],
version_number: options[:version_number] || version
)
gradle_version = get_version_from_gradle(sanitize_for_appstore: true)

# Get latest TestFlight build number and increment
# Get latest TestFlight build number and version FIRST
# so we can ensure our version is always higher
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]
)
latest_version = Actions.lane_context[SharedValues::LATEST_TESTFLIGHT_VERSION]

# Determine final version: must be higher than latest TestFlight version
version = options[:version_number] || gradle_version
if latest_version && !latest_version.to_s.empty?
begin
if Gem::Version.new(version) <= Gem::Version.new(latest_version)
parts = latest_version.split('.')
parts[-1] = (parts[-1].to_i + 1).to_s
bumped = parts.join('.')
UI.important("⚠️ Gradle version #{version} <= TestFlight #{latest_version}, bumping to #{bumped}")
version = bumped
end
rescue ArgumentError => e
UI.error("Version comparison failed: #{e.message}, using gradle version #{version}")
end
end
UI.important("📱 Final App Store version: #{version}")

# Set version in Xcode project
increment_version_number(
xcodeproj: ios_config[:project_path],
version_number: version
)

# Increment build number
increment_build_number(
xcodeproj: ios_config[:project_path],
build_number: latest_build_number + 1
Expand All @@ -664,14 +746,15 @@ platform :ios do
releaseNotes = generateReleaseNote()

# 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")
locale = ios_config[:primary_locale]
release_notes_path = File.join(ios_config[:metadata_path], locale, "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}")

# 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"))
metadata_files = Dir.glob(File.join(ios_config[:metadata_path], locale, "*.txt"))
if metadata_files.empty?
UI.error("⚠️ No metadata files found!")
else
Expand Down Expand Up @@ -721,7 +804,10 @@ platform :ios do
run_precheck_before_submit: appstore_config[:run_precheck_before_submit],

# Submission information (compliance, ads, etc.)
submission_information: appstore_config[:submission_information]
submission_information: appstore_config[:submission_information],

# Age rating declarations (lootBox, parentalControls, advertising, etc.)
app_rating_config_path: ios_config[:app_rating_config_path]
Comment on lines +809 to +810

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify age-rating file existence and validation logic
echo "=== Searching for age_rating files ==="
fd -a 'age_rating.json' 2>/dev/null || echo "No age_rating.json found"

echo ""
echo "=== Searching for validation and configuration references ==="
rg -n -C3 'validate_config|required_files|age_rating_config_path|app_rating_config_path' fastlane-config/project_config.rb fastlane/FastFile 2>/dev/null || echo "Search may have failed - files might not exist"

echo ""
echo "=== Checking if fastlane/FastFile exists and examining lines 809-815 ==="
if [ -f fastlane/FastFile ]; then
  echo "File exists. Reading context around lines 809-815:"
  sed -n '800,820p' fastlane/FastFile
else
  echo "fastlane/FastFile not found"
fi

echo ""
echo "=== Checking project_config validation ==="
if [ -f fastlane-config/project_config.rb ]; then
  echo "File exists. Searching for validation methods:"
  rg -n 'def.*valid|def.*required|def.*check' fastlane-config/project_config.rb
else
  echo "fastlane-config/project_config.rb not found"
fi

Repository: openMF/mifos-x-field-officer-app

Length of output: 3426


🏁 Script executed:

#!/bin/bash
# Check for any validation between validate_config call and deliver call
echo "=== Looking for validate_config call and subsequent validation ==="
rg -n -B5 -A15 'validate_config' fastlane/FastFile

echo ""
echo "=== Checking full validate_config implementation ==="
sed -n '262,290p' fastlane-config/project_config.rb

Repository: openMF/mifos-x-field-officer-app

Length of output: 1334


🏁 Script executed:

#!/bin/bash
# Search for where validate_config is called and look for any other validations
echo "=== Searching for validate_config call in FastFile ==="
rg -n 'validate_config' fastlane/FastFile

echo ""
echo "=== Looking at the deploy lane structure around line 810 ==="
sed -n '750,815p' fastlane/FastFile

echo ""
echo "=== Check if app_rating_config_path is used anywhere else ==="
rg -n 'app_rating_config_path' fastlane/
</parameter>

Repository: openMF/mifos-x-field-officer-app

Length of output: 3650


Add pre-check for age-rating config file before deliver call.

Line 810 passes ios_config[:app_rating_config_path] to deliver() without validation. The validate_config method does not check this file, so a missing or invalid path will cause a downstream Fastlane error instead of failing fast with a clear message.

Suggested guard
+      unless File.exist?(ios_config[:app_rating_config_path])
+        UI.user_error!("Missing age rating config: #{ios_config[:app_rating_config_path]}")
+      end
+
       deliver(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fastlane/FastFile` around lines 809 - 810, Before calling deliver in
FastFile, add a pre-check that verifies ios_config[:app_rating_config_path]
exists and is a readable file and fail fast with a clear error if not; locate
the deliver invocation and validate the value of
ios_config[:app_rating_config_path] (and/or add this check into validate_config)
and raise a user-friendly error (e.g., via UI.user_error or abort) when the path
is nil, missing, or not a file so deliver is only called with a valid age-rating
config path.

)

UI.success("✅ Successfully deployed to App Store!")
Expand Down
28 changes: 28 additions & 0 deletions fastlane/age_rating.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
"contests": "NONE",
"gamblingSimulated": "NONE",
"gunsOrOtherWeapons": "NONE",
"horrorOrFearThemes": "NONE",
"matureOrSuggestiveThemes": "NONE",
"medicalOrTreatmentInformation": "NONE",
"profanityOrCrudeHumor": "NONE",
"sexualContentGraphicAndNudity": "NONE",
"sexualContentOrNudity": "NONE",
"violenceCartoonOrFantasy": "NONE",
"violenceRealistic": "NONE",
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
"advertising": false,
"ageAssurance": false,
"gambling": false,
"healthOrWellnessTopics": false,
"lootBox": false,
"messagingAndChat": false,
"parentalControls": false,
"unrestrictedWebAccess": false,
"userGeneratedContent": false,
"ageRatingOverrideV2": "NONE",
"koreaAgeRatingOverride": "NONE",
"kidsAgeBand": null,
"developerAgeRatingInfoUrl": null
}