ann-flavor-flutter 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/Gemfile +3 -0
  4. data/README.md +274 -0
  5. data/Rakefile +6 -0
  6. data/ann-flavor-flutter.gemspec +26 -0
  7. data/lib/ann_flavor_flutter/version.rb +3 -0
  8. data/lib/ann_flavor_flutter.rb +10 -0
  9. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_compile_build_action.rb +219 -0
  10. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_download_from_app_store_action.rb +183 -0
  11. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_download_from_play_store_action.rb +168 -0
  12. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_emulators_action.rb +180 -0
  13. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_run_tests_action.rb +128 -0
  14. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_setup_action.rb +137 -0
  15. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_upload_to_app_store_action.rb +280 -0
  16. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_upload_to_firebase_action.rb +199 -0
  17. data/lib/fastlane/plugin/ann_flavor_flutter/actions/ann_upload_to_play_store_action.rb +248 -0
  18. data/lib/fastlane/plugin/ann_flavor_flutter/helper/lanes_android.rb +120 -0
  19. data/lib/fastlane/plugin/ann_flavor_flutter/helper/lanes_annai.rb +502 -0
  20. data/lib/fastlane/plugin/ann_flavor_flutter/helper/lanes_ios.rb +157 -0
  21. data/lib/fastlane/plugin/ann_flavor_flutter/helper/lanes_setup.rb +161 -0
  22. data/lib/fastlane/plugin/ann_flavor_flutter/helper/podspec_bridge.rb +153 -0
  23. data/lib/fastlane/plugin/ann_flavor_flutter/helper/test_integration.rb +346 -0
  24. data/lib/fastlane/plugin/ann_flavor_flutter/helper/utils_project_config.rb +102 -0
  25. data/lib/fastlane/plugin/ann_flavor_flutter/helper/utils_spec_loader.rb +339 -0
  26. data/lib/fastlane/plugin/ann_flavor_flutter/helper/utils_status.rb +115 -0
  27. data/lib/fastlane/plugin/ann_flavor_flutter.rb +23 -0
  28. data/plugin_manager.sh +140 -0
  29. metadata +126 -0
@@ -0,0 +1,102 @@
1
+ require 'fastlane/action'
2
+ require 'fileutils'
3
+
4
+ module FastlaneFlutterFlavor
5
+ # ------------------------------------
6
+ # ProjectUtil (Helper Module)
7
+ # Contains logic to locate the Flutter project root directory and key config files.
8
+ # ------------------------------------
9
+ module ProjectUtil
10
+
11
+ # ===============================================
12
+ # Find Flutter Project Root
13
+ # Searches upwards from the current directory for a pubspec.yaml file.
14
+ # ===============================================
15
+ def self.find_flutter_root
16
+ current_dir = Dir.pwd
17
+ while current_dir != '/' do
18
+ pubspec_path = File.join(current_dir, 'pubspec.yaml')
19
+ if File.exist?(pubspec_path)
20
+ Fastlane::UI.verbose("Found pubspec.yaml at: #{current_dir}")
21
+ return current_dir
22
+ end
23
+ parent_dir = File.dirname(current_dir)
24
+ # Prevent infinite loop if we hit the root and haven't found it
25
+ break if parent_dir == current_dir
26
+ current_dir = parent_dir
27
+ end
28
+
29
+ # Fallback to the original logic if pubspec.yaml isn't found
30
+ Fastlane::UI.important("pubspec.yaml not found by searching up. Falling back to current directory.")
31
+ return Dir.pwd
32
+ end
33
+
34
+ # ===============================================
35
+ # Internal method to resolve config path with precedence
36
+ # Precedence: User Parameter > Environment Variable > Default Path
37
+ #
38
+ # ===============================================
39
+ def self.resolve_config_path(filename, user_defined_path, env_var_name)
40
+ chosen_path_value = nil
41
+ source = "default in project root"
42
+
43
+ # Precedence 1: User-passed parameter (from action/lane)
44
+ # Check if user passed an explicit path (i.e., not the action's default filename string)
45
+ if user_defined_path && user_defined_path != filename
46
+ chosen_path_value = user_defined_path
47
+ source = "user parameter"
48
+ end
49
+
50
+ # Precedence 2: Environment Variable
51
+ if chosen_path_value.nil?
52
+ env_path = ENV[env_var_name]
53
+ if env_path && !env_path.empty?
54
+ chosen_path_value = env_path
55
+ source = "environment variable (#{env_var_name})"
56
+ end
57
+ end
58
+
59
+ # Precedence 3: Default in Project Root
60
+ if chosen_path_value.nil?
61
+ root = find_flutter_root
62
+ chosen_path_value = File.join(root, filename)
63
+ # source remains "default in project root"
64
+ end
65
+
66
+ # Resolve the path and check for existence
67
+ resolved_path = File.expand_path(chosen_path_value)
68
+
69
+ if File.exist?(resolved_path)
70
+ Fastlane::UI.verbose("Found #{filename} path from #{source}: #{resolved_path}")
71
+ else
72
+ Fastlane::UI.warning("#{filename} file not found at resolved path: #{resolved_path} (Source: #{source}).")
73
+ # Note: We still return the path, as it might be created later or is only referenced for metadata.
74
+ end
75
+
76
+ return resolved_path
77
+ end
78
+
79
+
80
+ # ===============================================
81
+ # Find Annai Spec File Path (annspec.yaml)
82
+ # Uses ANN_SPEC_FILE environment variable.
83
+ # Falls back to the legacy name annaispec.yaml if annspec.yaml is not found.
84
+ # ===============================================
85
+ def self.find_annai_spec_path(user_defined_path = nil)
86
+ path = resolve_config_path('annspec.yaml', user_defined_path, 'ANN_SPEC_FILE')
87
+ return path if File.exist?(path)
88
+ resolve_config_path('annaispec.yaml', user_defined_path, 'ANNAI_SPEC_FILE')
89
+ end
90
+
91
+ # ===============================================
92
+ # Find Test Spec File Path (anntestspec.yaml)
93
+ # Uses ANN_TEST_SPEC_FILE environment variable.
94
+ # Falls back to the legacy name annaitestspec.yaml if anntestspec.yaml is not found.
95
+ # ===============================================
96
+ def self.find_annai_test_spec_path(user_defined_path = nil)
97
+ path = resolve_config_path('anntestspec.yaml', user_defined_path, 'ANN_TEST_SPEC_FILE')
98
+ return path if File.exist?(path)
99
+ resolve_config_path('annaitestspec.yaml', user_defined_path, 'ANNAI_TEST_SPEC_FILE')
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,339 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'fastlane/action'
4
+
5
+ module FastlaneFlutterFlavor
6
+
7
+ class YamlSpecLoader
8
+
9
+ # Initialize the loader with the project root folder context and load the spec file.
10
+ def initialize(root_folder, file_path)
11
+ @root_folder = root_folder
12
+ @file_path = file_path # The file path, potentially overridden by ENV outside this class
13
+
14
+ # Load the file content immediately and store it
15
+ @spec_data = load_file
16
+ end
17
+
18
+ # Loads and parses the raw YAML spec file using the instance file path.
19
+ # @return [Hash, nil] The parsed YAML data, or nil on error.
20
+ def load_file
21
+ begin
22
+ # Ensure the file path is resolved relative to the project root
23
+ full_path = File.expand_path(@file_path, @root_folder)
24
+ Fastlane::UI.verbose("Reading Annai spec from: #{full_path}")
25
+
26
+ # Use safe_load to prevent arbitrary code execution from YAML files
27
+ yaml_content = YAML.safe_load(File.read(full_path), aliases: true)
28
+
29
+ # Ensure the content is a Hash before returning
30
+ unless yaml_content.is_a?(Hash)
31
+ raise "YAML content must be a top-level Hash."
32
+ end
33
+ return yaml_content
34
+ rescue => e
35
+ # Use @file_path for reporting the error path
36
+ Fastlane::UI.error("Failed to load or parse annai spec YAML file at #{@file_path}: #{e.message}")
37
+ return nil
38
+ end
39
+ end
40
+
41
+ # Recursively merges defaults into a flavor's data.
42
+ # This function prioritizes 'current_data' (the flavor-specific value).
43
+ # If a key is present in 'default_data' but missing in 'current_data',
44
+ # the default value is used.
45
+ #
46
+ # @param current_data [Object] The flavor-specific data (Hash or scalar).
47
+ # @param default_data [Object] The default data to merge (Hash or scalar).
48
+ # @return [Object] The merged data.
49
+ def deep_merge_defaults(current_data, default_data)
50
+
51
+ # FIX: If current_data is a scalar, it means the flavor explicitly set this value.
52
+ # It overrides the default, so we return it immediately.
53
+ return current_data unless current_data.is_a?(Hash)
54
+
55
+ # Base case 2: If default_data is not a hash, there's nothing left to merge deeply.
56
+ return current_data unless default_data.is_a?(Hash)
57
+
58
+ # Recursive case: Both are hashes, perform the merge
59
+ default_data.each do |key, default_value|
60
+ current_value = current_data[key]
61
+
62
+ if current_data.key?(key)
63
+ # Key exists in flavor (current_data), recursively merge
64
+ current_data[key] = deep_merge_defaults(current_value, default_value)
65
+ else
66
+ # The key is missing in the current flavor data. Use the default value.
67
+ current_data[key] = default_value
68
+ end
69
+ end
70
+
71
+ return current_data
72
+ end
73
+
74
+ # Retrieves the configuration for a specific flavor, merged with the platform defaults.
75
+ # This function uses the spec data loaded into the instance variable @spec_data.
76
+ #
77
+ # @param platform [Symbol] The target platform (:ios or :android).
78
+ # @param flavor_name [String] The name of the flavor (e.g., 'dev', 'staging').
79
+ # @return [Hash, nil] The fully merged configuration for the flavor, or nil if missing.
80
+ def get_merged_flavor_config(platform, flavor_name)
81
+ return nil unless @spec_data # Check if file was loaded successfully
82
+
83
+ platform_key = platform.to_s
84
+ flavor_key = flavor_name.to_s
85
+
86
+ # 1. Extract platform data from instance spec data
87
+ platform_data = @spec_data.dig('annai_app', platform_key)
88
+ unless platform_data.is_a?(Hash)
89
+ Fastlane::UI.verbose("Annai spec missing 'annai_app' or platform '#{platform_key}'.")
90
+ return nil
91
+ end
92
+
93
+ # 2. Extract specific flavor data and defaults
94
+ flavor_data = platform_data.dig('flavor', flavor_key)
95
+ default_data = platform_data.dig('default')
96
+
97
+ # If flavor data is missing entirely, and we can't merge, return nil
98
+ if flavor_data.nil? && default_data.nil?
99
+ Fastlane::UI.verbose("Flavor '#{flavor_key}' and platform default config are missing for platform '#{platform_key}'.")
100
+ return nil
101
+ end
102
+
103
+ # 3. Deep merge the flavor data (current) over the default data (base)
104
+ current_config = flavor_data || {}
105
+
106
+ # Perform the merge operation. We clone current_config to avoid modifying the original spec hash.
107
+ merged_config = deep_merge_defaults(current_config.dup, default_data || {})
108
+
109
+ # Add the flavor name itself for consistency in the returned config hash
110
+ merged_config['flavor'] = flavor_key
111
+
112
+ return merged_config
113
+ end
114
+
115
+ # --- GETTER WRAPPERS ---
116
+
117
+ # Retrieves the hash of all defined flavors for a given platform.
118
+ # Uses the instance spec data.
119
+ #
120
+ # @param platform [Symbol] The target platform (:ios or :android).
121
+ # @return [Hash] A hash where keys are flavor names (String) and values are their raw configs (Hash).
122
+ def get_platform_flavors(platform)
123
+ return {} unless @spec_data
124
+ platform_key = platform.to_s
125
+
126
+ # Use dig to safely retrieve the flavors hash from @spec_data
127
+ flavors = @spec_data.dig('annai_app', platform_key, 'flavor')
128
+
129
+ # Return the flavors hash, or an empty hash if no flavors are defined
130
+ return flavors.is_a?(Hash) ? flavors : {}
131
+ end
132
+
133
+ # @return [String, nil] The flavor name (just for consistency with other getters).
134
+ def get_flavor_name(platform, flavor_name)
135
+ config = get_merged_flavor_config(platform, flavor_name)
136
+ config.dig('flavor') if config
137
+ end
138
+
139
+ # @return [String, nil] The merged package ID (bundle ID or application ID).
140
+ # It combines 'id' and 'id_suffix' (if present) via direct concatenation.
141
+ # NOTE: It is assumed that the 'id_suffix' defined in the configuration
142
+ def get_package_id(platform, flavor_name)
143
+ config = get_merged_flavor_config(platform, flavor_name)
144
+ return nil unless config
145
+
146
+ base_id = config.dig('id')
147
+ return nil unless base_id
148
+
149
+ # Check for an optional suffix
150
+ suffix = config.dig('id_suffix')
151
+
152
+ # If a suffix is present, combine it directly with the base ID.
153
+ # This relies on the suffix string itself containing the correct separator (like ".")
154
+ # to correctly form the final package ID (e.g., "com.base" + ".dev" -> "com.base.dev").
155
+ if suffix && !suffix.empty?
156
+ return base_id + suffix
157
+ else
158
+ # Return the base ID if no suffix is present.
159
+ return base_id
160
+ end
161
+ end
162
+
163
+ # @return [String, nil] The merged app name.
164
+ # It combines 'name' and 'name_suffix' (if present) via direct concatenation.
165
+ # NOTE: It is assumed that the 'name_suffix' defined in the configuration
166
+ def get_display_name(platform, flavor_name)
167
+ config = get_merged_flavor_config(platform, flavor_name)
168
+ return nil unless config
169
+
170
+ base_name = config.dig('name')
171
+ return nil unless base_name
172
+
173
+ # Check for an optional suffix
174
+ suffix = config.dig('name_suffix')
175
+
176
+ # If a suffix is present, combine it directly with the base Name.
177
+ # This relies on the suffix string itself containing the correct separator
178
+ # to correctly form the final Name (e.g., "Test" + " App" -> "Test App").
179
+ if suffix && !suffix.empty?
180
+ return base_name + suffix
181
+ else
182
+ # Return the base Name if no suffix is present.
183
+ return base_name
184
+ end
185
+ end
186
+
187
+ # @return [String, nil] The merged version name.
188
+ def get_version_name(platform, flavor_name)
189
+ config = get_merged_flavor_config(platform, flavor_name)
190
+ return nil unless config
191
+
192
+ config.dig('version_name')
193
+ end
194
+
195
+ # @return [String, nil] The path to the version_code.
196
+ def get_version_code(platform, flavor_name)
197
+ config = get_merged_flavor_config(platform, flavor_name)
198
+ if config
199
+ version_code_value = config.dig('version_code').to_s.strip
200
+ return version_code_value.to_i(10).to_s
201
+ end
202
+ return nil
203
+ end
204
+
205
+ # @return [String, nil] The path to the main Dart file.
206
+ def get_main_file(platform, flavor_name)
207
+ config = get_merged_flavor_config(platform, flavor_name)
208
+ config.dig('main_file') if config
209
+ end
210
+
211
+ # @return [String, nil] The Google Play update priority for the flavor.
212
+ def get_priority(platform, flavor_name)
213
+ config = get_merged_flavor_config(platform, flavor_name)
214
+ config.dig('stores', 'google_play', 'priority') if config
215
+ end
216
+
217
+ # @return [String, nil] The Apple ID (numeric) for the flavor from App Store stores config.
218
+ def get_apple_id(platform, flavor_name)
219
+ config = get_merged_flavor_config(platform, flavor_name)
220
+ config.dig('stores', 'app_store', 'apple_id') if config
221
+ end
222
+
223
+ # @return [String, nil] The API key path (platform-level credential for uploading to the store).
224
+ def get_api_key_path(platform, flavor_name)
225
+ return nil unless @spec_data
226
+
227
+ platform_key = platform.to_s
228
+ case platform
229
+ when :android
230
+ @spec_data.dig('annai_app', platform_key, 'stores', 'google_play', 'api_key')
231
+ when :ios
232
+ @spec_data.dig('annai_app', platform_key, 'stores', 'app_store', 'api_key')
233
+ else
234
+ nil
235
+ end
236
+ end
237
+
238
+ # @return [String, nil] The export options plist path (platform-level App Store credential).
239
+ def get_export_option_file(platform, flavor_name)
240
+ return nil unless @spec_data
241
+
242
+ platform_key = platform.to_s
243
+ case platform
244
+ when :ios
245
+ @spec_data.dig('annai_app', platform_key, 'stores', 'app_store', 'export_options_plist')
246
+ else
247
+ nil
248
+ end
249
+ end
250
+
251
+ # @return [String, nil] The App Store Team ID for the flavor (flavor-level override, falls back to platform default).
252
+ def get_team_id(platform, flavor_name)
253
+ return nil unless @spec_data
254
+
255
+ platform_key = platform.to_s
256
+ case platform
257
+ when :ios
258
+ # Flavor-level override takes precedence
259
+ config = get_merged_flavor_config(platform, flavor_name)
260
+ flavor_team_id = config&.dig('stores', 'app_store', 'team_id')
261
+ return flavor_team_id if flavor_team_id && !flavor_team_id.empty?
262
+ # Fall back to platform default team_id
263
+ @spec_data.dig('annai_app', platform_key, 'default', 'team_id')
264
+ else
265
+ nil
266
+ end
267
+ end
268
+
269
+ # @return [String, nil] The Firebase Project ID.
270
+ def get_firebase_project_id(platform, flavor_name, build_config)
271
+ # 1. Get the merged configuration for the specific flavor
272
+ config = get_merged_flavor_config(platform, flavor_name)
273
+
274
+ config.dig('firebase', build_config, 'project_id') if config
275
+ end
276
+
277
+ # Retrieves the firebase token from a external .properties file
278
+ # as defined in annai_app -> general -> firebase_token_file
279
+ # @return [String, nil] The token value, or nil if not found/error.
280
+ def get_firebase_token_from_properties
281
+ return nil unless @spec_data
282
+
283
+ # 1. Get the properties file path from the YAML spec
284
+ # Path: annai_app -> general -> firebase_token_file
285
+ rel_props_path = @spec_data.dig('annai_app', 'general', 'firebase_token_file')
286
+
287
+ if rel_props_path.nil? || rel_props_path.empty?
288
+ Fastlane::UI.verbose("No 'firebase_token_file' defined in annai_app -> general.")
289
+ return nil
290
+ end
291
+
292
+ # 2. Resolve the full path relative to project root
293
+ full_props_path = File.expand_path(rel_props_path, @root_folder)
294
+
295
+ unless File.exist?(full_props_path)
296
+ Fastlane::UI.error("Firebase token file not found at: #{full_props_path}")
297
+ return nil
298
+ end
299
+
300
+ # 3. Parse the .properties file for FIREBASE_TOKEN
301
+ begin
302
+ File.foreach(full_props_path) do |line|
303
+ # Remove whitespace and skip comments/empty lines
304
+ line = line.strip
305
+ next if line.empty? || line.start_with?('#', '!')
306
+
307
+ # Match KEY=VALUE (handles both = and : as separators)
308
+ if line =~ /^\s*FIREBASE_TOKEN\s*[=:]\s*(.*)$/
309
+ token = $1.strip
310
+ # Remove optional surrounding quotes if present
311
+ return token.gsub(/^['"]|['"]$/, '')
312
+ end
313
+ end
314
+ rescue => e
315
+ Fastlane::UI.error("Error reading properties file at #{rel_props_path}: #{e.message}")
316
+ end
317
+
318
+ Fastlane::UI.verbose("FIREBASE_TOKEN key not found in #{rel_props_path}")
319
+ return nil
320
+ end
321
+
322
+ # For AdMob (annai_ads)
323
+ def get_gms_ads_id(platform, flavor_name)
324
+ config = get_merged_flavor_config(platform, flavor_name)
325
+ config.dig('gms_ads_id') if config
326
+ end
327
+
328
+ # For Google Sign-In (annai_auth)
329
+ def get_auth_client_id(platform, flavor_name, build_config)
330
+ config = get_merged_flavor_config(platform, flavor_name)
331
+ config.dig('auth', build_config, 'clientId') if config
332
+ end
333
+
334
+ def get_auth_reversed_client_id(platform, flavor_name, build_config)
335
+ config = get_merged_flavor_config(platform, flavor_name)
336
+ config.dig('auth', build_config, 'reversedClientId') if config
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,115 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'fastlane/action'
4
+
5
+ module FastlaneFlutterFlavor
6
+
7
+ # Inject the custom colors used in the StatusManager (since they aren't standard UI functions)
8
+ def red(text) ; "\e[31m#{text}\e[0m" ; end
9
+ def green(text) ; "\e[32m#{text}\e[0m" ; end
10
+ def amber(text) ; "\e[33m#{text}\e[0m" ; end
11
+
12
+ # ------------------------------------
13
+ # StatusManager (Helper Class)
14
+ # ------------------------------------
15
+ class StatusManager
16
+
17
+ def initialize(lane:)
18
+ @status = Hash.new
19
+ @lane = lane
20
+ end
21
+
22
+ def red(text) ; "\e[31m#{text}\e[0m" ; end
23
+ def green(text) ; "\e[32m#{text}\e[0m" ; end
24
+ def amber(text) ; "\e[33m#{text}\e[0m" ; end
25
+
26
+ def logError(flavor, laneName, variant, message)
27
+ key = [flavor, laneName, variant]
28
+
29
+ errorPresent = false
30
+ if @status.key?(key)
31
+ errorPresent = @status[key][0] == "err"
32
+ end
33
+
34
+ if errorPresent
35
+ message = "#{@status[key][1]}\n#{message}"
36
+ end
37
+
38
+ @status[key] = ["err", message]
39
+ end
40
+
41
+ def logWarning(flavor, laneName, variant, message)
42
+ key = [flavor, laneName, variant]
43
+
44
+ errorPresent = false
45
+ if @status.key?(key)
46
+ errorPresent = @status[key][0] == "err"
47
+ end
48
+
49
+ unless errorPresent
50
+ @status[key] = ["warn", message]
51
+ end
52
+ end
53
+
54
+ def logSuccess(flavor, laneName, variant)
55
+ key = [flavor, laneName, variant]
56
+
57
+ errorPresent = false
58
+ if @status.key?(key)
59
+ errorPresent = @status[key][0] == "err" || @status[key][0] == "warn"
60
+ end
61
+
62
+ unless errorPresent
63
+ @status[key] = ["info", ""]
64
+ end
65
+ end
66
+
67
+ def displayStatus()
68
+ errMsg = ""
69
+ message = "\n"
70
+ message += "+-------------------+-------------------------------+-------------------------------+----------------+\n"
71
+ message += "| #{green('Annai Fastlane summary')} |\n"
72
+ message += "+-------------------+-------------------------------+-------------------------------+----------------+\n"
73
+
74
+ message += "| Flavor | Variant | Lane | Status |\n"
75
+ message += "+-------------------+-------------------------------+-------------------------------+----------------+\n"
76
+
77
+ @status.each { |key,value|
78
+ flavor = key[0].to_s
79
+ laneName = key[1].to_s
80
+ variantName = key[2].to_s
81
+ statusStr = value[0]
82
+ statusMessage = value[1]
83
+
84
+ if statusStr == 'info'
85
+ sts = green("Success".ljust(15))
86
+ elsif statusStr == 'warn'
87
+ sts = amber("Warning".ljust(15))
88
+ if errMsg != "" then
89
+ errMsg += "\n"
90
+ end
91
+ errMsg += "Flavor - #{flavor} : "
92
+ errMsg += "#{statusMessage}"
93
+ else
94
+ sts = red("Failure".ljust(15))
95
+ if errMsg != "" then
96
+ errMsg += "\n"
97
+ end
98
+ errMsg += "Flavor - #{flavor} : "
99
+ errMsg += "#{statusMessage}"
100
+ end
101
+ message += "| #{flavor.ljust(18)}| #{variantName.ljust(30)}| #{laneName.ljust(30)}| #{sts}|\n"
102
+ }
103
+ message += "+-------------------+-------------------------------+-------------------------------+----------------+"
104
+
105
+ if errMsg != "" then
106
+ Fastlane::UI.message("Summary of all the Errors/Warnings")
107
+ Fastlane::UI.error(errMsg)
108
+ # Mailgun calls removed, assuming external communication is handled outside the core action
109
+ else
110
+ # Mailgun calls removed
111
+ end
112
+ Fastlane::UI.message(message)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,23 @@
1
+ module Fastlane
2
+ module AnnFlavorFlutter
3
+ def self.all_classes
4
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
5
+ end
6
+ end
7
+ end
8
+
9
+ # Import all available actions and helpers
10
+ Fastlane::AnnFlavorFlutter.all_classes.each { |f| require f }
11
+
12
+ # --- Global Helper for CocoaPods Podfile ---
13
+
14
+ # Accessible directly in the Podfile's post_install block.
15
+ def annai_ios_podfile_setup(installer)
16
+ if defined?(FastlaneFlutterFlavor::PodspecBridge)
17
+ FastlaneFlutterFlavor::PodspecBridge.apply_build_settings(installer)
18
+ else
19
+ # Fallback if Fastlane UI isn't loaded
20
+ msg = "[Annai] Bridge not found. Check gem installation."
21
+ defined?(UI) ? UI.error(msg) : puts("\e[31m#{msg}\e[0m")
22
+ end
23
+ end