ann-flutter-flavor 0.1.2

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/Gemfile.lock +271 -0
  5. data/Rakefile +6 -0
  6. data/ann-flutter-flavor.gemspec +25 -0
  7. data/lib/ann-flutter-flavor.rb +10 -0
  8. data/lib/ann_flutter_flavor/version.rb +3 -0
  9. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_compile_build_action.rb +219 -0
  10. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_download_from_app_store_action.rb +183 -0
  11. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_download_from_play_store_action.rb +168 -0
  12. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_emulators_action.rb +180 -0
  13. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_run_tests_action.rb +128 -0
  14. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_setup_action.rb +137 -0
  15. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_upload_to_app_store_action.rb +280 -0
  16. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_upload_to_firebase_action.rb +199 -0
  17. data/lib/fastlane/plugin/ann_flutter_flavor/actions/ann_upload_to_play_store_action.rb +248 -0
  18. data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_android.rb +120 -0
  19. data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_annai.rb +502 -0
  20. data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_ios.rb +157 -0
  21. data/lib/fastlane/plugin/ann_flutter_flavor/helper/lanes_setup.rb +161 -0
  22. data/lib/fastlane/plugin/ann_flutter_flavor/helper/podspec_bridge.rb +153 -0
  23. data/lib/fastlane/plugin/ann_flutter_flavor/helper/test_integration.rb +346 -0
  24. data/lib/fastlane/plugin/ann_flutter_flavor/helper/utils_project_config.rb +96 -0
  25. data/lib/fastlane/plugin/ann_flutter_flavor/helper/utils_spec_loader.rb +363 -0
  26. data/lib/fastlane/plugin/ann_flutter_flavor/helper/utils_status.rb +115 -0
  27. data/lib/fastlane/plugin/ann_flutter_flavor.rb +23 -0
  28. data/plugin_manager.sh +140 -0
  29. metadata +112 -0
@@ -0,0 +1,96 @@
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
+ # ===============================================
84
+ def self.find_annai_spec_path(user_defined_path = nil)
85
+ return resolve_config_path('annspec.yaml', user_defined_path, 'ANN_SPEC_FILE')
86
+ end
87
+
88
+ # ===============================================
89
+ # Find Annai Test Spec File Path (annaitestspec.yaml)
90
+ # Uses ANNAI_TEST_SPEC_FILE environment variable.
91
+ # ===============================================
92
+ def self.find_annai_test_spec_path(user_defined_path = nil)
93
+ return resolve_config_path('annaitestspec.yaml', user_defined_path, 'ANNAI_TEST_SPEC_FILE')
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,363 @@
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
+ # It combines 'version_name' and 'version_name_suffix' (if present) via direct concatenation.
189
+ # NOTE: It is assumed that the 'version_name_suffix' defined in the configuration
190
+ def get_version_name(platform, flavor_name)
191
+ config = get_merged_flavor_config(platform, flavor_name)
192
+ return nil unless config
193
+
194
+ base_version_name = config.dig('version_name')
195
+ return nil unless base_version_name
196
+
197
+ # Check for an optional suffix
198
+ suffix = config.dig('version_name_suffix')
199
+
200
+ # If a suffix is present, combine it directly with the base version name.
201
+ # This relies on the suffix string itself containing the correct separator
202
+ # to correctly form the final version name (e.g., "V" + "1.0.1" -> "V1.0.1").
203
+ if suffix && !suffix.empty?
204
+ return base_version_name + suffix
205
+ else
206
+ # Return the base version name if no suffix is present.
207
+ return base_version_name
208
+ end
209
+ end
210
+
211
+ # @return [String, nil] The path to the version_code.
212
+ def get_version_code(platform, flavor_name)
213
+ config = get_merged_flavor_config(platform, flavor_name)
214
+ if config
215
+ version_code_value = config.dig('version_code').to_s.strip
216
+ return version_code_value.to_i(10).to_s
217
+ end
218
+ return nil
219
+ end
220
+
221
+ # @return [String, nil] The path to the main Dart file.
222
+ def get_main_file(platform, flavor_name)
223
+ config = get_merged_flavor_config(platform, flavor_name)
224
+ config.dig('main_file') if config
225
+ end
226
+
227
+ # @return [String, nil] The priority level.
228
+ def get_priority(platform, flavor_name)
229
+ config = get_merged_flavor_config(platform, flavor_name)
230
+ config.dig('priority') if config
231
+ end
232
+
233
+ # @return [String, nil] The API key path (e.g., path to Google Service Account JSON or App Store Connect API Key JSON).
234
+ def get_api_key_path(platform, flavor_name)
235
+ # 1. Get the merged configuration for the specific flavor
236
+ config = get_merged_flavor_config(platform, flavor_name)
237
+
238
+ # Return nil early if configuration is missing
239
+ return nil unless config
240
+
241
+ # 2. Extract the key based on the platform
242
+ case platform
243
+ when :android
244
+ # For Android, we look for 'google_api_key'
245
+ config.dig('fastlane', 'google_api_key')
246
+ when :ios
247
+ # For iOS, we look for 'apple_api_key'
248
+ config.dig('fastlane', 'apple_api_key')
249
+ else
250
+ # Return nil for other platforms
251
+ nil
252
+ end
253
+ end
254
+
255
+ # @return [String, nil] The export_option_file.
256
+ def get_export_option_file(platform, flavor_name)
257
+ # 1. Get the merged configuration for the specific flavor
258
+ config = get_merged_flavor_config(platform, flavor_name)
259
+
260
+ # Return nil early if configuration is missing
261
+ return nil unless config
262
+
263
+ # 2. Extract the key based on the platform
264
+ case platform
265
+ when :ios
266
+ # For iOS, we look for 'export_options_plist'
267
+ config.dig('fastlane', 'export_options_plist')
268
+ else
269
+ # Return nil for other platforms
270
+ nil
271
+ end
272
+ end
273
+
274
+ # @return [String, nil] The App store Team ID.
275
+ def get_team_id(platform, flavor_name)
276
+ # 1. Get the merged configuration for the specific flavor
277
+ config = get_merged_flavor_config(platform, flavor_name)
278
+
279
+ # Return nil early if configuration is missing
280
+ return nil unless config
281
+
282
+ # 2. Extract the key based on the platform
283
+ case platform
284
+ when :ios
285
+ # For iOS, we look for 'export_option_file'
286
+ config.dig('team_id')
287
+ else
288
+ # Return nil for other platforms
289
+ nil
290
+ end
291
+ end
292
+
293
+ # @return [String, nil] The Firebase Project ID.
294
+ def get_firebase_project_id(platform, flavor_name, build_config)
295
+ # 1. Get the merged configuration for the specific flavor
296
+ config = get_merged_flavor_config(platform, flavor_name)
297
+
298
+ config.dig('firebase', build_config, 'project_id') if config
299
+ end
300
+
301
+ # Retrieves the firebase token from a external .properties file
302
+ # as defined in annai_app -> general -> firebase_token_file
303
+ # @return [String, nil] The token value, or nil if not found/error.
304
+ def get_firebase_token_from_properties
305
+ return nil unless @spec_data
306
+
307
+ # 1. Get the properties file path from the YAML spec
308
+ # Path: annai_app -> general -> firebase_token_file
309
+ rel_props_path = @spec_data.dig('annai_app', 'general', 'firebase_token_file')
310
+
311
+ if rel_props_path.nil? || rel_props_path.empty?
312
+ Fastlane::UI.verbose("No 'firebase_token_file' defined in annai_app -> general.")
313
+ return nil
314
+ end
315
+
316
+ # 2. Resolve the full path relative to project root
317
+ full_props_path = File.expand_path(rel_props_path, @root_folder)
318
+
319
+ unless File.exist?(full_props_path)
320
+ Fastlane::UI.error("Firebase token file not found at: #{full_props_path}")
321
+ return nil
322
+ end
323
+
324
+ # 3. Parse the .properties file for FIREBASE_TOKEN
325
+ begin
326
+ File.foreach(full_props_path) do |line|
327
+ # Remove whitespace and skip comments/empty lines
328
+ line = line.strip
329
+ next if line.empty? || line.start_with?('#', '!')
330
+
331
+ # Match KEY=VALUE (handles both = and : as separators)
332
+ if line =~ /^\s*FIREBASE_TOKEN\s*[=:]\s*(.*)$/
333
+ token = $1.strip
334
+ # Remove optional surrounding quotes if present
335
+ return token.gsub(/^['"]|['"]$/, '')
336
+ end
337
+ end
338
+ rescue => e
339
+ Fastlane::UI.error("Error reading properties file at #{rel_props_path}: #{e.message}")
340
+ end
341
+
342
+ Fastlane::UI.verbose("FIREBASE_TOKEN key not found in #{rel_props_path}")
343
+ return nil
344
+ end
345
+
346
+ # For AdMob (annai_ads)
347
+ def get_gms_ads_id(platform, flavor_name)
348
+ config = get_merged_flavor_config(platform, flavor_name)
349
+ config.dig('gms_ads_id') if config
350
+ end
351
+
352
+ # For Google Sign-In (annai_auth)
353
+ def get_auth_client_id(platform, flavor_name, build_config)
354
+ config = get_merged_flavor_config(platform, flavor_name)
355
+ config.dig('auth', build_config, 'clientId') if config
356
+ end
357
+
358
+ def get_auth_reversed_client_id(platform, flavor_name, build_config)
359
+ config = get_merged_flavor_config(platform, flavor_name)
360
+ config.dig('auth', build_config, 'reversedClientId') if config
361
+ end
362
+ end
363
+ 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 AnnFlutterFlavor
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::AnnFlutterFlavor.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