ann-flavor-flutter 0.4.4 → 0.4.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d57c1bc278619e03093217b10da943867c6451ec3f7a9aa5bb1467a4237e23e3
4
- data.tar.gz: 0ba8e70d266810c144431767937f454ea91d20f5d05360a83ed53c92a1b4b688
3
+ metadata.gz: 2884061122825135d9f07bb18db889d23509649848aca1fca2c45b63b9b8c3bd
4
+ data.tar.gz: 5ed44c51bcf6c29195763aaa2615f4bb59b71a5b0908f26fdbc83677419273a4
5
5
  SHA512:
6
- metadata.gz: e4e38b858c0b17060676cd37fe24af154fa01bf9defe1bb104e92e5d4290e26da0eb62a05ff58bbf77145b30054238a1d81ab27f4d8e7891ec6c9796095dd65b
7
- data.tar.gz: 8db5bae8d0e7688a68fe7ed319211cc25d8135c80dbf2e6255c14d5c845bf43568481e94241bf6fe51fbcf615c2f6f8309802aa66464653d0d4f1199ac730b72
6
+ metadata.gz: 8635f03da486e2aa4da6a7c9ed423d76260624ff5710a58c4a174951ecf2922bd874e4ff197edcb551db650ae7ef3fc76725d4f58935a076785f0c3f203e3a81
7
+ data.tar.gz: 0e5a13e1e4c03ddf24b31bb7b7c07fd330934213601b7a373408fdb051b8cfb2bb0bf298a9ad254df1ede039315b4871f6b2ff44a90818c7574a7361f2f197b6
data/.gitignore CHANGED
@@ -1,2 +1,7 @@
1
1
  /pkg/
2
2
  Gemfile.lock
3
+
4
+ # Vendored ann_flavor_core source, generated transiently during publish (DEF-058).
5
+ # Never committed for real — CI/`make publish-fastlane` `git add -f` it briefly
6
+ # so `gem build`'s `git ls-files`-based file list includes it, then discard it.
7
+ /lib/vendor/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.6
4
+
5
+ ### Fixed
6
+ - Loading this gem (`require 'ann_flavor_flutter'` / `require 'fastlane/plugin/ann_flavor_flutter'`) failed with `cannot load such file -- ann_flavor_core` — the 0.4.5 core-delegation change added a hard `require 'ann_flavor_core'` in `utils_spec_loader.rb`, but `ann_flavor_core` is intentionally never published to RubyGems (ADR-008). The published gem now vendors `ann_flavor_core`'s source and resolves it automatically; local monorepo development is unaffected. ([#58](https://github.com/anntech-dev/ann-flavor-tooling/issues/58))
7
+
8
+ ## 0.4.5
9
+
10
+ ### Changed
11
+ - `YamlSpecLoader` and `PodspecBridge` now delegate `annspec.yaml` parsing and resolution to the shared `ann-flavor-core-ruby` gem ([ADR-008](../../docs/02-decisions/008-core-delegation-mandate.md)).
12
+
13
+ ### Fixed
14
+ - Fixed a `service_account` cascade bug where a flavor's or default's top-level Firebase `service_account` override was never consulted — only the two build-type-specific levels were checked.
15
+ - `PodspecBridge#bundle_id` no longer hardcodes the release build type's bundle ID regardless of the actual build type; debug/profile builds now resolve their own correct bundle ID.
16
+
3
17
  ## 0.4.4
4
18
 
5
19
  ### Fixed
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
+
5
+ gem "ann-flavor-core", path: "../../core/ann-flavor-core-ruby"
@@ -1,3 +1,3 @@
1
1
  module AnnFlavorFlutter
2
- VERSION = "0.4.4"
2
+ VERSION = "0.4.6"
3
3
  end
@@ -1,5 +1,13 @@
1
1
  # This file is loaded by require 'annai-flutter-flavor'
2
2
 
3
+ # The published gem vendors ann_flavor_core's source under lib/vendor/ (ADR-008:
4
+ # ann_flavor_core is never published to RubyGems, only path/git dependencies).
5
+ # In the monorepo this directory doesn't exist, and ann_flavor_core resolves via
6
+ # the real gem installed by the local Gemfile's `path:` dependency instead.
7
+ vendored_core_lib = File.expand_path('vendor/ann_flavor_core/lib', __dir__)
8
+ $LOAD_PATH.unshift(vendored_core_lib) if File.directory?(vendored_core_lib) &&
9
+ !$LOAD_PATH.include?(vendored_core_lib)
10
+
3
11
  # Load the version (Good Practice)
4
12
  require 'ann_flavor_flutter/version'
5
13
 
@@ -118,7 +118,7 @@ module FastlaneFlutterFlavor
118
118
  end
119
119
 
120
120
  def bundle_id
121
- @loader.get_package_id(@platform, @flavor)
121
+ @loader.get_package_id(@platform, @flavor, @build_type)
122
122
  end
123
123
 
124
124
  def version_name
@@ -1,39 +1,37 @@
1
1
  require 'yaml'
2
2
  require 'fileutils'
3
3
  require 'fastlane/action'
4
+ require 'ann_flavor_core'
4
5
 
5
6
  module FastlaneFlutterFlavor
6
7
 
7
8
  class YamlSpecLoader
8
9
 
10
+ STANDARD_BUILD_TYPES = %w[debug release profile].freeze
11
+
9
12
  # Initialize the loader with the project root folder context and load the spec file.
10
13
  def initialize(root_folder, file_path)
11
14
  @root_folder = root_folder
12
15
  @file_path = file_path # The file path, potentially overridden by ENV outside this class
13
16
 
14
17
  # Load the file content immediately and store it
15
- @spec_data = load_file
18
+ @spec = load_file
16
19
  end
17
20
 
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.
21
+ # Loads and parses annspec.yaml via the shared core (ADR-008) the single
22
+ # source of truth for parsing, cascade merging, and resolution.
23
+ # @return [AnnFlavorCore::AnnSpec, nil] The parsed spec, or nil on error.
20
24
  def load_file
21
25
  begin
22
26
  # Ensure the file path is resolved relative to the project root
23
27
  full_path = File.expand_path(@file_path, @root_folder)
24
28
  Fastlane::UI.verbose("Reading Annai spec from: #{full_path}")
25
29
 
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
30
+ spec = AnnFlavorCore::Parser.parse_file(full_path)
33
31
 
34
- check_removed_firebase_fields(yaml_content)
32
+ check_removed_firebase_fields(spec)
35
33
 
36
- return yaml_content
34
+ return spec
37
35
  rescue Fastlane::Shell::ShellError => e
38
36
  raise e
39
37
  rescue => e
@@ -44,9 +42,13 @@ module FastlaneFlutterFlavor
44
42
  end
45
43
 
46
44
  # Raises UI.user_error! if firebase_app_id or build_target are present in any iOS firebase block.
47
- # Both fields were removed — they are derived automatically now.
48
- def check_removed_firebase_fields(spec_data)
49
- ios_app = spec_data.dig('app', 'ios')
45
+ # Both fields were removed — they are derived automatically now. The core's model has no fields
46
+ # for them at all, so this reads the raw YAML directly (a narrow, deliberate exception to
47
+ # delegation — it is a migration guard, not spec parsing/merging/resolution, per ADR-008).
48
+ def check_removed_firebase_fields(spec)
49
+ full_path = File.expand_path(@file_path, @root_folder)
50
+ raw = YAML.safe_load(File.read(full_path), aliases: true) || {}
51
+ ios_app = raw.dig('app', 'ios')
50
52
  return unless ios_app.is_a?(Hash)
51
53
 
52
54
  sections = {}
@@ -82,263 +84,199 @@ module FastlaneFlutterFlavor
82
84
  end
83
85
  end
84
86
 
85
- # Recursively merges defaults into a flavor's data.
86
- # This function prioritizes 'current_data' (the flavor-specific value).
87
- # If a key is present in 'default_data' but missing in 'current_data',
88
- # the default value is used.
89
- #
90
- # @param current_data [Object] The flavor-specific data (Hash or scalar).
91
- # @param default_data [Object] The default data to merge (Hash or scalar).
92
- # @return [Object] The merged data.
93
- def deep_merge_defaults(current_data, default_data)
94
-
95
- # FIX: If current_data is a scalar, it means the flavor explicitly set this value.
96
- # It overrides the default, so we return it immediately.
97
- return current_data unless current_data.is_a?(Hash)
98
-
99
- # Base case 2: If default_data is not a hash, there's nothing left to merge deeply.
100
- return current_data unless default_data.is_a?(Hash)
101
-
102
- # Recursive case: Both are hashes, perform the merge
103
- default_data.each do |key, default_value|
104
- current_value = current_data[key]
105
-
106
- if current_data.key?(key)
107
- # Key exists in flavor (current_data), recursively merge
108
- current_data[key] = deep_merge_defaults(current_value, default_value)
109
- else
110
- # The key is missing in the current flavor data. Use the default value.
111
- current_data[key] = default_value
112
- end
113
- end
114
-
115
- return current_data
116
- end
117
-
118
87
  def enabled?
119
- return false unless @spec_data
120
- @spec_data.fetch('enabled', true) != false
88
+ return false unless @spec
89
+ @spec.enabled != false
121
90
  end
122
91
 
123
- # Retrieves the configuration for a specific flavor, merged with the platform defaults.
124
- # This function uses the spec data loaded into the instance variable @spec_data.
125
- #
126
- # @param platform [Symbol] The target platform (:ios or :android).
127
- # @param flavor_name [String] The name of the flavor (e.g., 'dev', 'staging').
128
- # @return [Hash, nil] The fully merged configuration for the flavor, or nil if missing.
129
- def get_merged_flavor_config(platform, flavor_name)
130
- return nil unless @spec_data && enabled?
131
-
132
- platform_key = platform.to_s
133
- flavor_key = flavor_name.to_s
134
-
135
- # 1. Extract platform data from instance spec data
136
- platform_data = @spec_data.dig('app', platform_key)
137
- unless platform_data.is_a?(Hash)
138
- Fastlane::UI.verbose("Annai spec missing 'app' or platform '#{platform_key}'.")
139
- return nil
140
- end
92
+ # Returns the [flavor, defaults] structs for a platform, or nil if the
93
+ # platform/flavor cannot be resolved.
94
+ def platform_flavor_and_defaults(platform, flavor_name)
95
+ return nil unless @spec && enabled?
141
96
 
142
- # 2. Extract specific flavor data and defaults
143
- flavor_data = platform_data.dig('flavor', flavor_key)
144
- default_data = platform_data.dig('default')
97
+ platform_data = platform_for(@spec, platform)
98
+ return nil if platform_data.nil?
145
99
 
146
- # If flavor data is missing entirely, and we can't merge, return nil
147
- if flavor_data.nil? && default_data.nil?
148
- Fastlane::UI.verbose("Flavor '#{flavor_key}' and platform default config are missing for platform '#{platform_key}'.")
149
- return nil
150
- end
100
+ flavor = platform_data.flavors[flavor_name.to_s]
101
+ return nil if flavor.nil? && platform_data.default.nil?
102
+
103
+ [flavor, platform_data.default]
104
+ end
151
105
 
152
- # 3. Deep merge the flavor data (current) over the default data (base)
153
- current_config = flavor_data || {}
106
+ def platform_for(spec, platform)
107
+ case platform.to_s
108
+ when 'android' then spec.app&.android
109
+ when 'ios' then spec.app&.ios
110
+ when 'web' then spec.app&.web
111
+ when 'windows' then spec.app&.windows
112
+ end
113
+ end
154
114
 
155
- # Perform the merge operation. We clone current_config to avoid modifying the original spec hash.
156
- merged_config = deep_merge_defaults(current_config.dup, default_data || {})
115
+ # Resolves a flavor against a build type via the core resolver. Returns nil
116
+ # when the platform/flavor cannot be resolved.
117
+ def resolve(platform, flavor_name, build_type)
118
+ pair = platform_flavor_and_defaults(platform, flavor_name)
119
+ return nil if pair.nil?
120
+
121
+ flavor, defaults = pair
122
+ flavor ||= empty_flavor_for(platform)
123
+ defaults ||= empty_defaults_for(platform)
124
+
125
+ case platform.to_s
126
+ when 'android' then AnnFlavorCore::Resolver.resolve_android_flavor(flavor, defaults, build_type.to_s)
127
+ when 'ios' then AnnFlavorCore::Resolver.resolve_ios_flavor(flavor, defaults, build_type.to_s)
128
+ when 'web' then AnnFlavorCore::Resolver.resolve_web_flavor(flavor, defaults, build_type.to_s)
129
+ when 'windows' then AnnFlavorCore::Resolver.resolve_windows_flavor(flavor, defaults, build_type.to_s)
130
+ end
131
+ end
157
132
 
158
- # Add the flavor name itself for consistency in the returned config hash
159
- merged_config['flavor'] = flavor_key
133
+ def empty_flavor_for(platform)
134
+ case platform.to_s
135
+ when 'android' then AnnFlavorCore::AndroidFlavor.defaults
136
+ when 'ios' then AnnFlavorCore::IosFlavor.defaults
137
+ when 'web' then AnnFlavorCore::WebFlavor.defaults
138
+ when 'windows' then AnnFlavorCore::WindowsFlavor.defaults
139
+ end
140
+ end
160
141
 
161
- return merged_config
142
+ def empty_defaults_for(platform)
143
+ case platform.to_s
144
+ when 'android' then AnnFlavorCore::AndroidDefault.defaults
145
+ when 'ios' then AnnFlavorCore::IosDefault.defaults
146
+ when 'web' then AnnFlavorCore::WebDefault.defaults
147
+ when 'windows' then AnnFlavorCore::WindowsDefault.defaults
148
+ end
162
149
  end
163
150
 
164
151
  # --- GETTER WRAPPERS ---
165
152
 
166
153
  # Retrieves the hash of all defined flavors for a given platform.
167
- # Uses the instance spec data.
168
- #
169
154
  # @param platform [Symbol] The target platform (:ios or :android).
170
- # @return [Hash] A hash where keys are flavor names (String) and values are their raw configs (Hash).
155
+ # @return [Hash] flavor name => core flavor struct. Callers only rely on
156
+ # .empty? and .keys, so the struct-valued Hash is a compatible superset
157
+ # of the previous raw-Hash-valued return.
171
158
  def get_platform_flavors(platform)
172
- return {} unless @spec_data && enabled?
173
- platform_key = platform.to_s
174
-
175
- # Use dig to safely retrieve the flavors hash from @spec_data
176
- flavors = @spec_data.dig('app', platform_key, 'flavor')
159
+ return {} unless @spec && enabled?
160
+ platform_data = platform_for(@spec, platform)
161
+ return {} if platform_data.nil?
177
162
 
178
- # Return the flavors hash, or an empty hash if no flavors are defined
179
- return flavors.is_a?(Hash) ? flavors : {}
163
+ platform_data.flavors
180
164
  end
181
165
 
182
166
  # @return [String, nil] The flavor name (just for consistency with other getters).
183
167
  def get_flavor_name(platform, flavor_name)
184
- config = get_merged_flavor_config(platform, flavor_name)
185
- config.dig('flavor') if config
168
+ pair = platform_flavor_and_defaults(platform, flavor_name)
169
+ pair ? flavor_name.to_s : nil
186
170
  end
187
171
 
188
- # @return [String, nil] The merged package ID (bundle ID or application ID).
189
- # It combines 'id' and 'id_suffix' (if present) via direct concatenation.
190
- # NOTE: It is assumed that the 'id_suffix' defined in the configuration
191
- def get_package_id(platform, flavor_name)
192
- config = get_merged_flavor_config(platform, flavor_name)
193
- return nil unless config
194
-
195
- base_id = config.dig('id')
196
- return nil unless base_id
197
-
198
- # Check for an optional suffix
199
- suffix = config.dig('id_suffix')
200
-
201
- # If a suffix is present, combine it directly with the base ID.
202
- # This relies on the suffix string itself containing the correct separator (like ".")
203
- # to correctly form the final package ID (e.g., "com.base" + ".dev" -> "com.base.dev").
204
- if suffix && !suffix.empty?
205
- return base_id + suffix
206
- else
207
- # Return the base ID if no suffix is present.
208
- return base_id
209
- end
172
+ # @return [String, nil] The merged package ID (bundle ID or application ID)
173
+ # for the given build type. Was previously hardcoded to 'release'
174
+ # regardless of the caller's actual build corrected via plan-030
175
+ # Phase 6 / REQ-CDEL-00070 (PodspecBridge#bundle_id now threads its real
176
+ # @build_type through instead of relying on this default).
177
+ def get_package_id(platform, flavor_name, build_type = 'release')
178
+ resolved = resolve(platform, flavor_name, build_type)
179
+ resolved&.effective_id
210
180
  end
211
181
 
212
182
  # @return [String, nil] The merged app name.
213
- # It combines 'name' and 'name_suffix' (if present) via direct concatenation.
214
- # NOTE: It is assumed that the 'name_suffix' defined in the configuration
215
183
  def get_display_name(platform, flavor_name)
216
- config = get_merged_flavor_config(platform, flavor_name)
217
- return nil unless config
218
-
219
- base_name = config.dig('name')
220
- return nil unless base_name
221
-
222
- # Check for an optional suffix
223
- suffix = config.dig('name_suffix')
224
-
225
- # If a suffix is present, combine it directly with the base Name.
226
- # This relies on the suffix string itself containing the correct separator
227
- # to correctly form the final Name (e.g., "Test" + " App" -> "Test App").
228
- if suffix && !suffix.empty?
229
- return base_name + suffix
230
- else
231
- # Return the base Name if no suffix is present.
232
- return base_name
233
- end
184
+ resolved = resolve(platform, flavor_name, 'release')
185
+ resolved&.effective_name
234
186
  end
235
187
 
236
188
  # @return [String, nil] The merged version name.
237
189
  def get_version_name(platform, flavor_name)
238
- config = get_merged_flavor_config(platform, flavor_name)
239
- return nil unless config
240
-
241
- config.dig('version_name')
190
+ resolved = resolve(platform, flavor_name, 'release')
191
+ resolved&.effective_version_name
242
192
  end
243
193
 
244
194
  # @return [String, nil] The path to the version_code.
245
195
  def get_version_code(platform, flavor_name)
246
- config = get_merged_flavor_config(platform, flavor_name)
247
- if config
248
- version_code_value = config.dig('version_code').to_s.strip
249
- return version_code_value.to_i(10).to_s
250
- end
251
- return nil
196
+ resolved = resolve(platform, flavor_name, 'release')
197
+ return nil unless resolved
198
+ resolved.effective_version_code.to_i.to_s
252
199
  end
253
200
 
254
201
  # @return [String, nil] The path to the main Dart file.
255
202
  def get_main_file(platform, flavor_name)
256
- config = get_merged_flavor_config(platform, flavor_name)
257
- config.dig('main_file') if config
203
+ resolved = resolve(platform, flavor_name, 'release')
204
+ resolved&.effective_main_file
258
205
  end
259
206
 
260
207
  # @return [String, nil] The Google Play update priority for the flavor.
261
208
  def get_priority(platform, flavor_name)
262
- config = get_merged_flavor_config(platform, flavor_name)
263
- config.dig('stores', 'google_play', 'priority') if config
209
+ pair = platform_flavor_and_defaults(platform, flavor_name)
210
+ return nil unless pair
211
+ flavor, = pair
212
+ flavor&.stores&.google_play&.priority
264
213
  end
265
214
 
266
215
  # @return [String, nil] The Apple ID (numeric) for the flavor from App Store stores config.
267
216
  def get_apple_id(platform, flavor_name)
268
- config = get_merged_flavor_config(platform, flavor_name)
269
- config.dig('stores', 'app_store', 'apple_id') if config
217
+ pair = platform_flavor_and_defaults(platform, flavor_name)
218
+ return nil unless pair
219
+ flavor, = pair
220
+ flavor&.stores&.app_store&.apple_id
270
221
  end
271
222
 
272
223
  # @return [String, nil] The API key path (platform-level credential for uploading to the store).
273
224
  def get_api_key_path(platform, flavor_name)
274
- return nil unless @spec_data
275
-
276
- platform_key = platform.to_s
277
- case platform
278
- when :android
279
- @spec_data.dig('app', platform_key, 'default', 'credentials', 'google_play', 'api_key')
280
- when :ios
281
- @spec_data.dig('app', platform_key, 'default', 'credentials', 'app_store', 'api_key')
282
- else
283
- nil
225
+ return nil unless @spec
226
+
227
+ platform_data = platform_for(@spec, platform)
228
+ return nil unless platform_data
229
+
230
+ case platform.to_s
231
+ when 'android'
232
+ platform_data.default&.credentials&.google_play&.api_key
233
+ when 'ios'
234
+ platform_data.default&.credentials&.app_store&.api_key
284
235
  end
285
236
  end
286
237
 
287
238
  # @return [String, nil] The export options plist path (inside default.credentials.app_store).
288
239
  def get_export_option_file(platform, flavor_name)
289
- return nil unless @spec_data
290
-
291
- platform_key = platform.to_s
292
- case platform
293
- when :ios
294
- @spec_data.dig('app', platform_key, 'default', 'credentials', 'app_store', 'export_options_plist')
295
- else
296
- nil
297
- end
240
+ return nil unless @spec
241
+ return nil unless platform.to_s == 'ios'
242
+
243
+ ios = @spec.app&.ios
244
+ ios&.default&.credentials&.app_store&.export_options_plist
298
245
  end
299
246
 
300
247
  # @return [String, nil] The Apple Team ID for the flavor (flavor-level credentials override, falls back to default).
301
248
  def get_team_id(platform, flavor_name)
302
- return nil unless @spec_data
303
-
304
- platform_key = platform.to_s
305
- case platform
306
- when :ios
307
- # Flavor-level credentials.signing.team_id takes precedence
308
- config = get_merged_flavor_config(platform, flavor_name)
309
- flavor_team_id = config&.dig('credentials', 'signing', 'team_id')
310
- return flavor_team_id if flavor_team_id && !flavor_team_id.empty?
311
- # Fall back to default credentials.signing.team_id
312
- @spec_data.dig('app', platform_key, 'default', 'credentials', 'signing', 'team_id')
313
- else
314
- nil
315
- end
249
+ return nil unless @spec
250
+ return nil unless platform.to_s == 'ios'
251
+
252
+ ios = @spec.app&.ios
253
+ return nil unless ios
254
+
255
+ flavor_team_id = ios.flavors[flavor_name.to_s]&.credentials&.signing&.team_id
256
+ return flavor_team_id if flavor_team_id && !flavor_team_id.empty?
257
+
258
+ ios.default&.credentials&.signing&.team_id
316
259
  end
317
260
 
318
261
  # @return [String, nil] The Firebase Project ID.
319
262
  def get_firebase_project_id(platform, flavor_name, build_config)
320
- # 1. Get the merged configuration for the specific flavor
321
- config = get_merged_flavor_config(platform, flavor_name)
322
-
323
- config.dig('build_types', build_config, 'firebase', 'project_id') if config
263
+ resolved = resolve(platform, flavor_name, build_config)
264
+ resolved&.effective_firebase&.project_id
324
265
  end
325
266
 
326
267
  # Returns the resolved service_account path for the given platform/flavor/build_config,
327
- # cascading from flavor build_type default build_type.
268
+ # cascading through the core's full 4-level resolution:
269
+ # flavor.build_types[bt].firebase.service_account (most specific)
270
+ # flavor.firebase.service_account
271
+ # default.build_types[bt].firebase.service_account
272
+ # default.firebase.service_account (least specific)
273
+ # Corrected via ADR-008/REQ-CDEL-00060 — the pre-delegation implementation only
274
+ # checked the first and third levels, silently skipping the flavor-top-level and
275
+ # default-top-level fallbacks.
328
276
  # @return [String, nil]
329
277
  def get_service_account(platform, flavor_name, build_config)
330
- return nil unless @spec_data
331
-
332
- platform_data = @spec_data.dig('app', platform)
333
- return nil unless platform_data
334
-
335
- flavor_data = platform_data.dig('flavor', flavor_name) || {}
336
- default_data = platform_data['default'] || {}
337
-
338
- flavor_bt_sa = flavor_data.dig('build_types', build_config, 'firebase', 'service_account')
339
- default_bt_sa = default_data.dig('build_types', build_config, 'firebase', 'service_account')
340
-
341
- path = flavor_bt_sa || default_bt_sa
278
+ resolved = resolve(platform, flavor_name, build_config)
279
+ path = resolved&.effective_firebase&.service_account
342
280
  return nil if path.nil? || path.empty?
343
281
 
344
282
  File.expand_path(path, @root_folder)
@@ -346,68 +284,50 @@ module FastlaneFlutterFlavor
346
284
 
347
285
  # For AdMob (annai_ads)
348
286
  # Returns the full app ID for a specific build type, applying build_types[build_type].id_suffix.
349
- # Falls back to get_package_id if no build-type-specific suffix is defined.
350
287
  def get_effective_id(platform, flavor_name, build_type)
351
- config = get_merged_flavor_config(platform, flavor_name)
352
- return nil unless config
353
-
354
- base_id = config.dig('id')
355
- return nil unless base_id
356
-
357
- id_suffix = config.dig('id_suffix') || ''
358
- bt_id_suffix = config.dig('build_types', build_type.to_s, 'id_suffix') || ''
359
- base_id + id_suffix + bt_id_suffix
288
+ resolved = resolve(platform, flavor_name, build_type)
289
+ resolved&.effective_id
360
290
  end
361
291
 
362
292
  # Returns the full display name for a specific build type, applying build_types[build_type].name_suffix.
363
293
  def get_effective_name(platform, flavor_name, build_type)
364
- config = get_merged_flavor_config(platform, flavor_name)
365
- return nil unless config
366
-
367
- base_name = config.dig('name')
368
- return nil unless base_name
369
-
370
- name_suffix = config.dig('name_suffix') || ''
371
- bt_name_suffix = config.dig('build_types', build_type.to_s, 'name_suffix') || ''
372
- base_name + name_suffix + bt_name_suffix
294
+ resolved = resolve(platform, flavor_name, build_type)
295
+ resolved&.effective_name
373
296
  end
374
297
 
375
298
  def get_gms_ads_id(platform, flavor_name)
376
- config = get_merged_flavor_config(platform, flavor_name)
377
- config&.dig('admob', 'gms_ads_id') || config&.dig('gms_ads_id')
299
+ resolved = resolve(platform, flavor_name, 'release')
300
+ resolved&.effective_gms_ads_id
378
301
  end
379
302
 
380
303
  # Returns the resolved Cloudflare config for a web flavor as a Hash with keys:
381
304
  # project_name, account_id, branch, mode — cascaded flavor → default → hardcoded fallback.
382
- # project_name is flavor-only (never inherited from default).
383
305
  # @return [Hash] Always returns a Hash; values may be nil if not configured.
384
306
  def get_cloudflare_config(platform, flavor_name)
385
- return {} unless @spec_data
386
-
387
- platform_key = platform.to_s
388
- platform_data = @spec_data.dig('app', platform_key)
389
- return {} unless platform_data.is_a?(Hash)
307
+ return {} unless @spec
308
+ return {} unless platform.to_s == 'web'
390
309
 
391
- flavor_cf = platform_data.dig('flavor', flavor_name.to_s, 'cloudflare') || {}
392
- default_cf = platform_data.dig('default', 'cloudflare') || {}
310
+ resolved = resolve(platform, flavor_name, 'release')
311
+ cf = resolved&.effective_cloudflare
312
+ return {} unless cf
393
313
 
394
314
  {
395
- 'project_name' => flavor_cf['project_name'],
396
- 'account_id' => flavor_cf['account_id'] || default_cf['account_id'],
397
- 'branch' => flavor_cf['branch'] || default_cf['branch'] || 'main',
398
- 'mode' => flavor_cf['mode'] || default_cf['mode'] || 'pages',
315
+ 'project_name' => cf.project_name,
316
+ 'account_id' => cf.account_id,
317
+ 'branch' => cf.branch,
318
+ 'mode' => cf.mode,
399
319
  }
400
320
  end
401
321
 
402
322
  # For Google Sign-In (annai_auth)
403
323
  def get_auth_client_id(platform, flavor_name, build_config)
404
- config = get_merged_flavor_config(platform, flavor_name)
405
- config.dig('build_types', build_config, 'auth', 'clientId') if config
324
+ resolved = resolve(platform, flavor_name, build_config)
325
+ resolved&.effective_auth&.client_id
406
326
  end
407
327
 
408
328
  def get_auth_reversed_client_id(platform, flavor_name, build_config)
409
- config = get_merged_flavor_config(platform, flavor_name)
410
- config.dig('build_types', build_config, 'auth', 'reversedClientId') if config
329
+ resolved = resolve(platform, flavor_name, build_config)
330
+ resolved&.effective_auth&.reversed_client_id
411
331
  end
412
332
  end
413
- end
333
+ end