From 05aec3a16204961037940b7b887d5ec7597f99f2 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Aug 2026 13:18:08 +0545 Subject: [PATCH 1/5] Move Android fastlane logic into the play_publisher plugin The Android Fastfile had grown to ~600 lines of Supply::Client workarounds for draft apps and the crashing supply Uploader/Setup paths. That code now lives in fastlane-plugin-play_publisher, so the Fastfile is just lane names. Lane names and behaviour are unchanged, except play_build now fails fast when android/key.properties is missing instead of silently producing a debug-signed AAB. Also adds the missing changelog for versionCode 1, a Releasing section to the README, and fastlane's generated artifacts to .gitignore. --- .gitignore | 9 + README.md | 32 + android/Gemfile | 6 + android/fastlane/Fastfile | 558 +----------------- .../metadata/android/en-US/changelogs/1.txt | 1 + 5 files changed, 61 insertions(+), 545 deletions(-) create mode 100644 android/fastlane/metadata/android/en-US/changelogs/1.txt diff --git a/.gitignore b/.gitignore index 8955ca8..6252cab 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,12 @@ app.*.map.json # Local-only documentation (design specs, setup notes) — kept out of the repo. docs/ + +# Fastlane — generated docs, run reports and locally installed gems. +**/fastlane/README.md +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/test_output/ +**/.bundle/ +android/vendor/ +ios/vendor/ diff --git a/README.md b/README.md index 8eab232..9f15e0a 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,38 @@ and the topic/provider ids in `lib/core/config/app_config.dart`. Release signing is read from `android/key.properties` (gitignored); see `android/key.properties.example`. +## Releasing + +Both stores are driven by [fastlane](https://fastlane.tools), configured per +platform under `android/fastlane/` and `ios/fastlane/`. Store listing text lives +in `fastlane/metadata/` alongside the app. + +```bash +cd android && bundle install # or: cd ios && bundle install +bundle exec fastlane lanes +``` + +Shared lane names: `build` (no upload), `beta` (Play open testing / TestFlight), +`release` (Play production / App Store review), plus `upload_metadata`, +`upload_screenshots`, `upload_listing`, `download_metadata` and `screenshots`. +Android also has `internal` for the Play internal track. + +Everything project-specific is read from environment variables, so no secrets +are committed — set them in your CI provider or a local, gitignored `.env`: + +- **Android** — `APP_IDENTIFIER`, plus `PLAY_STORE_JSON_KEY_PATH` or + `PLAY_STORE_JSON_KEY_DATA`. Release signing still comes from + `android/key.properties`. The build/upload logic lives in + [fastlane-plugin-play_publisher](https://github.com/popupbits/fastlane-plugin-play_publisher), + which documents the optional vars (`SUPPLY_RELEASE_STATUS=draft` for a + first-ever release, `SKIP_FLUTTER_BUILD=1`, …). +- **iOS** — `APP_IDENTIFIER`, `APPLE_ID`, `TEAM_ID`, `ITC_TEAM_ID`, + `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`, + `APP_STORE_CONNECT_KEY_CONTENT` (base64 `.p8`), and `MATCH_GIT_URL` / + `MATCH_PASSWORD` for the private + [match](https://docs.fastlane.tools/actions/match/) certificates repo. + `fastlane fetch_apple_info` prints your `ITC_TEAM_ID`. + ## License [MIT](LICENSE) — consistent with the diff --git a/android/Gemfile b/android/Gemfile index b63de10..2728fe3 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -2,5 +2,11 @@ source "https://rubygems.org" gem "fastlane", "~> 2.225" +# All Android build/publish logic lives in this plugin — the Fastfile in +# this repo is only lane names. +gem "fastlane-plugin-play_publisher", + git: "https://github.com/popupbits/fastlane-plugin-play_publisher", + tag: "v1.0.0" + # Optional plugins — uncomment as needed # gem "fastlane-plugin-firebase_app_distribution" diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 76058ff..0d040ed 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -1,589 +1,57 @@ # --------------------------------------------------------------- # Fastfile — Android build & deployment lanes # --------------------------------------------------------------- -# This file is designed to be project-agnostic. All project-specific -# values (package name) are read from environment variables so the -# same file can be dropped into any Flutter project. +# All logic lives in fastlane-plugin-play_publisher. This file only +# names the lanes. See that plugin's README for the full env var +# reference and the draft-app behaviour. # -# Required env vars: -# APP_IDENTIFIER — Android application ID (package name) -# -# And one of: -# PLAY_STORE_JSON_KEY_PATH — Path to service account JSON key -# PLAY_STORE_JSON_KEY_DATA — Base64-encoded service account JSON +# Required env: APP_IDENTIFIER, and one of +# PLAY_STORE_JSON_KEY_PATH / PLAY_STORE_JSON_KEY_DATA # --------------------------------------------------------------- default_platform(:android) -def android_package_name - value = ENV["APP_IDENTIFIER"] - UI.user_error!("APP_IDENTIFIER env var must be set (Android applicationId)") if value.nil? || value.empty? - value -end - platform :android do - # ------------------------------------------------------------------ - # Helper: Resolve the Play Store JSON key - # ------------------------------------------------------------------ - desc "Resolve the Google Play service account JSON key" - private_lane :resolve_play_store_key do - key_path = ENV["PLAY_STORE_JSON_KEY_PATH"] - key_data = ENV["PLAY_STORE_JSON_KEY_DATA"] - - if key_path && !key_path.empty? && File.exist?(key_path) - UI.message("Using Play Store JSON key from PLAY_STORE_JSON_KEY_PATH: #{key_path}") - key_path - elsif key_data && !key_data.empty? - # Decode base64 data to a temp file - require "tempfile" - require "base64" - tmp = Tempfile.new(["play_store_key", ".json"]) - tmp.write(Base64.decode64(key_data)) - tmp.close - UI.message("Decoded Play Store JSON key from PLAY_STORE_JSON_KEY_DATA to #{tmp.path}") - tmp.path - else - UI.user_error!( - "Neither PLAY_STORE_JSON_KEY_PATH nor PLAY_STORE_JSON_KEY_DATA is set. " \ - "Provide one of them to authenticate with Google Play." - ) - end - end - - # ------------------------------------------------------------------ - # Build: Build AAB only (no upload) - # ------------------------------------------------------------------ - # Set SKIP_FLUTTER_BUILD=1 to reuse an already-built AAB at - # build/app/outputs/bundle/release/app-release.aab (e.g. when - # android_publish.sh was given an explicit aab path). - # ------------------------------------------------------------------ desc "Build release AAB only (no upload)" lane :build do - expected_aab = File.expand_path("../../build/app/outputs/bundle/release/app-release.aab", __dir__) - - if ENV["SKIP_FLUTTER_BUILD"] == "1" - UI.user_error!("SKIP_FLUTTER_BUILD=1 but no AAB found at #{expected_aab}") unless File.exist?(expected_aab) - UI.message("SKIP_FLUTTER_BUILD=1 — reusing existing AAB at #{expected_aab}") - else - sh("cd ../.. && flutter build appbundle --release") - UI.success("AAB built successfully at #{expected_aab}") - end - end - - # ------------------------------------------------------------------ - # Resolve the release_status to pass to the `supply` upload action. - # Defaults to "completed", but Google Play rejects that for apps that - # have never passed first review ("draft apps") with the error: - # "Only releases with status draft may be created on draft app." - # Set SUPPLY_RELEASE_STATUS=draft for a first-time release; after - # Google finishes the first review the restriction is lifted and you - # can drop the env var. - # Valid values: completed, draft, halted, inProgress - # ------------------------------------------------------------------ - def supply_release_status - value = (ENV["SUPPLY_RELEASE_STATUS"] || "completed").strip - allowed = %w[completed draft halted inProgress] - unless allowed.include?(value) - UI.user_error!("SUPPLY_RELEASE_STATUS=#{value} is invalid — must be one of: #{allowed.join(', ')}") - end - value + play_build end - # ------------------------------------------------------------------ - # Internal: Build AAB + upload to Google Play internal testing - # ------------------------------------------------------------------ desc "Build and upload to Google Play internal testing track" lane :internal do - build - - key_file = resolve_play_store_key - - supply( - track: "internal", - release_status: supply_release_status, - aab: "../build/app/outputs/bundle/release/app-release.aab", - json_key: key_file, - package_name: android_package_name, - skip_upload_metadata: true, - skip_upload_changelogs: false, - skip_upload_images: true, - skip_upload_screenshots: true, - ) + play_publish(track: "internal") end - # ------------------------------------------------------------------ - # Beta: Build AAB + upload to Google Play open testing (beta) - # ------------------------------------------------------------------ desc "Build and upload to Google Play beta (open testing) track" lane :beta do - build - - key_file = resolve_play_store_key - - supply( - track: "beta", - release_status: supply_release_status, - aab: "../build/app/outputs/bundle/release/app-release.aab", - json_key: key_file, - package_name: android_package_name, - skip_upload_metadata: true, - skip_upload_changelogs: false, - skip_upload_images: true, - skip_upload_screenshots: true, - ) + play_publish(track: "beta") end - # ------------------------------------------------------------------ - # Release: Build AAB + upload to Google Play production - # ------------------------------------------------------------------ desc "Build and upload to Google Play production track" lane :release do - build - - key_file = resolve_play_store_key - - supply( - track: "production", - release_status: supply_release_status, - aab: "../build/app/outputs/bundle/release/app-release.aab", - json_key: key_file, - package_name: android_package_name, - skip_upload_metadata: false, - skip_upload_changelogs: false, - skip_upload_images: true, - skip_upload_screenshots: true, - ) + play_publish(track: "production", upload_metadata: true) end - # ------------------------------------------------------------------ - # Upload lanes — drive Supply::Client directly - # ------------------------------------------------------------------ - # We bypass fastlane's `supply` upload action because its Uploader - # class crashes with `undefined method 'size' for nil:NilClass` - # (fastlane/fastlane#21530) on apps whose production track has no - # releases yet. Store listing text and images are app-level on - # Google Play — they aren't tied to a release — so driving the - # Publisher Edits API via Supply::Client lets us upload cleanly on - # brand-new apps AND on fully published ones. - # - # Three public lanes: - # upload_metadata — text fields only - # upload_screenshots — images + screenshots only - # upload_listing — text + images + screenshots - # All three share the helpers below. - # ------------------------------------------------------------------ - desc "Upload only text metadata (title, descriptions) to Play Store" lane :upload_metadata do - play_store_listing_upload(upload_text: true, upload_images: false) + play_upload_listing(text: true, images: false) end desc "Upload only screenshots + feature graphic / icon to Play Store" lane :upload_screenshots do - play_store_listing_upload(upload_text: false, upload_images: true) + play_upload_listing(text: false, images: true) end desc "Upload text metadata + images + screenshots to Play Store" lane :upload_listing do - play_store_listing_upload(upload_text: true, upload_images: true) - end - - # ------------------------------------------------------------------ - # Shared upload helper - # ------------------------------------------------------------------ - def play_store_listing_upload(upload_text:, upload_images:) - require "supply" - require "supply/client" - - metadata_dir = File.expand_path("./metadata/android", __dir__) - UI.user_error!("metadata directory does not exist: #{metadata_dir}") unless Dir.exist?(metadata_dir) - - locales = Dir.children(metadata_dir) - .select { |d| File.directory?(File.join(metadata_dir, d)) } - .reject { |d| d.start_with?(".") } - UI.user_error!("No locale directories under #{metadata_dir}") if locales.empty? - - # changes_not_sent_for_review=true tells Google to commit the Edit to - # the Play Console dashboard WITHOUT attempting to submit it for review. - # This is the supported escape hatch for: - # - draft apps (apps that have never passed first review), where any - # auto-submit attempt trips "Only releases with status draft may be - # created on draft app", - # - published apps where the developer prefers to review and submit - # changes manually through the Play Console UI before they go live. - # The developer can still manually click "Send for review" in the - # Play Console whenever they're ready. Opt out with - # SUPPLY_SUBMIT_FOR_REVIEW=1 if you want Google to auto-submit the edit - # (only works on already-published apps). - auto_submit_for_review = ENV["SUPPLY_SUBMIT_FOR_REVIEW"] == "1" - - Supply.config = FastlaneCore::Configuration.create( - Supply::Options.available_options, - { - package_name: android_package_name, - json_key: resolve_play_store_key, - changes_not_sent_for_review: !auto_submit_for_review, - }, - ) - - client = Supply::Client.make_from_config - client.begin_edit(package_name: android_package_name) - - begin - UI.message("Uploading to Play Store (locales: #{locales.join(', ')})") - locales.each do |lang| - upload_text_for_locale(client, metadata_dir, lang) if upload_text - upload_images_for_locale(client, metadata_dir, lang) if upload_images - end - - commit_edit_with_draft_app_fallback(client, auto_submit_for_review: auto_submit_for_review) - - if auto_submit_for_review - UI.success("Upload committed and submitted for review.") - else - UI.success("Upload committed. Open Play Console to review and send for review when ready.") - end - rescue => ex - safely_abort_edit(client) - explain_play_store_commit_error(ex) - raise - end - end - - # Commit the edit. On draft apps (never past first review), Google rejects - # the commit because the inherited release state includes non-draft - # releases. Catch that specific error, rewrite the inherited non-draft - # releases to draft within the same edit (leaving live release state - # untouched since changes_not_sent_for_review=true keeps everything - # pending), and retry. Published apps take the fast path on the first - # attempt without ever touching track state. - def commit_edit_with_draft_app_fallback(client, auto_submit_for_review:) - if auto_submit_for_review - UI.message("Committing edit and submitting for review...") - else - UI.message("Committing edit (staged for manual review)...") - end - - begin - client.commit_current_edit! - rescue => ex - raise ex unless ex.message.to_s.include?("Only releases with status draft may be created on draft app") - raise ex if auto_submit_for_review # can't use this workaround if submitting for review - - UI.important("Draft app detected — rewriting inherited non-draft releases to 'draft' status within this edit.") - UI.important("Live testers are NOT affected: changes_not_sent_for_review=true keeps everything pending until you click 'Send for review' in Play Console.") - - downgraded_any = rewrite_inherited_releases_to_draft!(client) - unless downgraded_any - UI.error("No non-draft inherited releases found to rewrite — the draft-app error is coming from somewhere else.") - raise ex - end - - UI.message("Retrying commit after downgrade...") - client.begin_edit(package_name: android_package_name) if client.current_edit.nil? - client.commit_current_edit! - end - end - - # Walk every track, find releases whose status isn't already "draft", - # flip them to draft, and PUT the track back via update_track. Returns - # true if any track was modified. - def rewrite_inherited_releases_to_draft!(client) - modified = false - tracks = client.tracks - tracks.each do |track| - next if track.releases.nil? || track.releases.empty? - needs_update = track.releases.any? { |r| r.status.to_s != "draft" } - next unless needs_update - - track.releases.each do |release| - next if release.status.to_s == "draft" - UI.important(" #{track.track}: #{release.name || '(no name)'} → draft (was: #{release.status})") - release.status = "draft" - end - - client.update_track(track.track, track) - modified = true - end - modified - end - - # Translate the common opaque Google Play API errors into something the - # user can act on without going to the GitHub issues tracker first. - def explain_play_store_commit_error(ex) - message = ex.message.to_s - - if message.include?("Only releases with status draft may be created on draft app") - UI.error("Upload failed: this app is still in \"draft\" state on Google Play.") - UI.important(<<~MSG.strip) - When an app has never had a production release submitted for review, - Google Play treats it as a draft app and rejects any Edit whose - inherited release state is not also \"draft\". Any internal/beta release - you uploaded directly through the Play Console or a prior `fastlane - internal` run is now blocking this Edit. - - Fix one of the following in the Play Console, then re-run the upload: - - 1. Complete the initial app setup checklist (Store presence → - Main store listing, plus any \"Set up your app\" tasks shown on - the Dashboard) and submit the first internal release for review. - Once Google finishes that first review, the draft-app restriction - is lifted permanently. - - 2. OR open Testing → Internal testing → Releases, find the current - release, and change its status to \"Draft\". Then re-run this lane. - - 3. OR promote the app straight to production with a real release - (`./scripts/android_publish.sh release`). - - This is a Google Play Console restriction, not a fastlane bug. - MSG - return - end - - if message.include?("The caller does not have permission") - UI.error("Upload failed: service account lacks store listing permissions.") - UI.important(<<~MSG.strip) - Go to Play Console → Users and permissions, open the service account - (`*@*.iam.gserviceaccount.com`), and grant this app the permission - \"Manage store presence\" (and \"Manage testing track releases\" if - you also want to upload AABs). Save, then wait ~5–10 minutes for - Google's permission cache to refresh before retrying. - MSG - return - end - - UI.error("Upload failed: #{ex.message}") - end - - # Upload title / short_description / full_description / video for one locale. - def upload_text_for_locale(client, metadata_dir, lang) - lang_dir = File.join(metadata_dir, lang) - fields = { - title: read_metadata_field(lang_dir, "title"), - short_description: read_metadata_field(lang_dir, "short_description"), - full_description: read_metadata_field(lang_dir, "full_description"), - video: read_metadata_field(lang_dir, "video"), - } - - if fields.values.all?(&:nil?) - UI.important(" #{lang}: no text fields to upload — skipping") - return - end - - client.update_listing_for_language(language: lang, **fields) - UI.success(" #{lang}: text ok") - end - - # Upload feature graphic + icon + tv banner + all screenshot sets for one locale. - def upload_images_for_locale(client, metadata_dir, lang) - images_root = File.join(metadata_dir, lang, Supply::IMAGES_FOLDER_NAME) - return unless Dir.exist?(images_root) - - # Single-file image types (featureGraphic, icon, tvBanner) - Supply::IMAGES_TYPES.each do |image_type| - image_path = Dir.glob(File.join(images_root, "#{image_type}.*")).first - next unless image_path - - begin - client.upload_image(image_path: image_path, image_type: image_type, language: lang) - UI.success(" #{lang}/#{image_type}") - rescue => ex - UI.error(" #{lang}/#{image_type}: failed — #{ex.message}") - end - end - - # Multi-file screenshot types (phone/7in/10in/tv/wear) - Supply::SCREENSHOT_TYPES.each do |image_type| - type_dir = File.join(images_root, image_type) - next unless Dir.exist?(type_dir) - - files = Dir.glob(File.join(type_dir, "*.{png,jpg,jpeg}")).sort - next if files.empty? - - begin - client.clear_screenshots(image_type: image_type, language: lang) - files.each { |f| client.upload_image(image_path: f, image_type: image_type, language: lang) } - UI.success(" #{lang}/#{image_type}: #{files.size}") - rescue => ex - UI.error(" #{lang}/#{image_type}: failed — #{ex.message}") - end - end - end - - # Read one metadata .txt file, returning nil if the file is missing or - # blank (so the Publisher API receives nil for "unchanged field" semantics - # instead of blowing away the existing value with an empty string). - def read_metadata_field(lang_dir, field) - path = File.join(lang_dir, "#{field}.txt") - return nil unless File.file?(path) - content = File.read(path, encoding: "UTF-8") - content.strip.empty? ? nil : content - end - - # Abort the current Edit without letting a cleanup failure mask the real - # upload error that triggered this call. - def safely_abort_edit(client) - client.abort_current_edit - rescue StandardError - nil + play_upload_listing end - # ------------------------------------------------------------------ - # Download: Fetch current metadata from Play Store - # ------------------------------------------------------------------ - # Drives Supply::Client directly instead of Supply::Setup#perform_download - # for three reasons: - # - # 1. `perform_download` calls `client.latest_version` which crashes with - # NoMethodError on apps that have no production release - # (fastlane/fastlane#21529) — the `releases` array on internal/beta - # tracks can contain nil entries and the upstream `reject` block - # doesn't compact them first. - # - # 2. `perform_download` silently no-ops if `metadata_path` already exists. - # We want a clean download every time, so we write into a tempdir and - # atomically swap into place. - # - # 3. `perform_download` occasionally misses locales that exist in the - # Play Console UI but haven't been touched via the Edit API. We merge - # the API-discovered locales with locales found in the existing local - # metadata dir and any listed in SUPPLY_DOWNLOAD_LANGUAGES. - # - # Existing local metadata is preserved as a timestamped .bak on each run. - # ------------------------------------------------------------------ desc "Download current metadata from Play Store" lane :download_metadata do - require "supply" - require "supply/client" - require "fileutils" - require "tmpdir" - - key_file = resolve_play_store_key - metadata_dir = File.expand_path("./metadata/android", __dir__) - - local_langs = if Dir.exist?(metadata_dir) - Dir.children(metadata_dir).select { |d| File.directory?(File.join(metadata_dir, d)) } - else - [] - end - forced_langs = (ENV["SUPPLY_DOWNLOAD_LANGUAGES"] || "") - .split(",").map(&:strip).reject(&:empty?) - - Supply.config = FastlaneCore::Configuration.create( - Supply::Options.available_options, - { - package_name: android_package_name, - json_key: key_file, - }, - ) - - client = Supply::Client.make_from_config - client.begin_edit(package_name: android_package_name) - - Dir.mktmpdir("supply-download-") do |tmp| - tmp_metadata = File.join(tmp, "android") - FileUtils.mkdir_p(tmp_metadata) - - begin - api_langs = client.listings.map(&:language) - all_langs = (api_langs + local_langs + forced_langs).uniq - - if all_langs.empty? - UI.user_error!( - "No locales to download — Play Store returned none and no local/forced " \ - "locales given. If you know a locale exists in the Console, pass it via " \ - "SUPPLY_DOWNLOAD_LANGUAGES=en-US,ne-NP (etc)." - ) - end - - UI.message("Downloading listings for: #{all_langs.join(', ')}") - - all_langs.each do |lang| - listing = client.listing_for_language(lang) - - if Supply::AVAILABLE_METADATA_FIELDS.all? { |k| listing.send(k).to_s.strip.empty? } - UI.important(" #{lang}: no data on Play Store — skipping") - next - end - - containing = File.join(tmp_metadata, lang) - FileUtils.mkdir_p(containing) - Supply::AVAILABLE_METADATA_FIELDS.each do |key| - path = File.join(containing, "#{key}.txt") - File.open(path, "w:UTF-8") { |f| f.write(listing.send(key).to_s) } - end - UI.success(" #{lang}") - - # Download images + screenshots for this locale. Best-effort — any - # individual image_type that fails (no asset of that kind uploaded, - # transient HTTP error, etc.) is logged and skipped without aborting - # the whole lane. Set SUPPLY_SKIP_IMAGE_DOWNLOAD=1 to turn this off. - unless ENV["SUPPLY_SKIP_IMAGE_DOWNLOAD"] == "1" - require "net/http" - require "fastimage" - - UI.message(" Checking images for #{lang}...") - image_download_total = 0 - (Supply::IMAGES_TYPES + Supply::SCREENSHOT_TYPES).each do |image_type| - begin - urls = client.fetch_images(image_type: image_type, language: lang).map(&:url) - if urls.nil? || urls.empty? - UI.message(" #{image_type}: none") - next - end - - is_single = Supply::IMAGES_TYPES.include?(image_type) - base_path = File.join(tmp_metadata, lang, Supply::IMAGES_FOLDER_NAME, image_type) - - if is_single - FileUtils.mkdir_p(File.dirname(base_path)) - else - FileUtils.mkdir_p(base_path) - end - - urls.each_with_index do |url, idx| - ext = FastImage.type(url) || "png" - file_path = if is_single - "#{base_path}.#{ext}" - else - File.join(base_path, "#{idx + 1}_#{lang}.#{ext}") - end - File.binwrite(file_path, Net::HTTP.get(URI.parse(url))) - end - - image_download_total += urls.size - UI.success(" #{image_type}: #{urls.size}") - rescue => ex - UI.error(" #{image_type}: failed — #{ex.message}") - end - end - UI.message(" Images downloaded for #{lang}: #{image_download_total}") - end - end - ensure - client.abort_current_edit - end - - unless Dir.exist?(tmp_metadata) && !Dir.empty?(tmp_metadata) - UI.user_error!("Downloaded no metadata into #{tmp_metadata}") - end - - if Dir.exist?(metadata_dir) - backup = "#{metadata_dir}.bak.#{Time.now.to_i}" - FileUtils.mv(metadata_dir, backup) - UI.message("Backed up existing local metadata to #{backup}") - end - - FileUtils.mkdir_p(File.dirname(metadata_dir)) - FileUtils.mv(tmp_metadata, metadata_dir) - UI.success("Downloaded Play Store metadata to #{metadata_dir}") - end + play_download_metadata end - # ------------------------------------------------------------------ - # Screenshots: Frame raw integration-test screenshots with frameit - # ------------------------------------------------------------------ desc "Frame raw Android screenshots using frameit" lane :screenshots do frameit( diff --git a/android/fastlane/metadata/android/en-US/changelogs/1.txt b/android/fastlane/metadata/android/en-US/changelogs/1.txt new file mode 100644 index 0000000..81fe995 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/1.txt @@ -0,0 +1 @@ +Initial release of Refetch: a curated tech-news feed with discussions, voting, submissions, and push notifications — for mobile and desktop. From b388dab224cd4321e502509bd67d8f1349fcc3a6 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Aug 2026 13:26:13 +0545 Subject: [PATCH 2/5] Add manual release workflows for Play Store and App Store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android Release and iOS Release both run on workflow_dispatch and drive the existing fastlane lanes. The popupbits apps call a shared reusable workflow for this, but that lives in a private repo and GitHub will not let a public repo in another org call it, so the steps are inlined here. Commits both Gemfile.locks — Bundler runs frozen in CI, so the release jobs need them — plus a Refresh Gemfile.lock workflow to regenerate them on a runner after a Gemfile change. match cannot install a certificate on a CI runner without an unlocked keychain, so sync_signing now calls setup_ci. --- .github/workflows/android-release.yml | 111 ++++++ .github/workflows/ios-release.yml | 101 ++++++ .github/workflows/refresh-gemfile-lock.yml | 56 +++ README.md | 6 + android/Gemfile.lock | 375 +++++++++++++++++++++ ios/Gemfile.lock | 366 ++++++++++++++++++++ ios/fastlane/Fastfile | 4 + 7 files changed, 1019 insertions(+) create mode 100644 .github/workflows/android-release.yml create mode 100644 .github/workflows/ios-release.yml create mode 100644 .github/workflows/refresh-gemfile-lock.yml create mode 100644 android/Gemfile.lock create mode 100644 ios/Gemfile.lock diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml new file mode 100644 index 0000000..0b627b6 --- /dev/null +++ b/.github/workflows/android-release.yml @@ -0,0 +1,111 @@ +# Publish Refetch to Google Play. +# +# Builds a signed App Bundle and uploads it through the Fastlane lanes in +# `android/fastlane/`. Manual only — there is no push trigger. +# +# Repository secrets (Settings → Secrets and variables → Actions): +# +# APP_IDENTIFIER Android applicationId (io.appwrite.refetch) +# PLAY_STORE_JSON_KEY_DATA Base64 Google Play service account JSON key +# ANDROID_KEYSTORE_BASE64 Base64 release keystore (.jks) +# ANDROID_KEYSTORE_PASSWORD Keystore password +# ANDROID_KEY_ALIAS Key alias inside the keystore +# ANDROID_KEY_PASSWORD Key password +# +# Encode the binary secrets with `base64 -w0 file` (Linux) or +# `base64 -i file` (macOS). + +name: Android Release + +on: + workflow_dispatch: + inputs: + release_type: + description: "Play Store track to publish to" + required: true + type: choice + default: internal + options: + - internal + - beta + - release + release_status: + description: "Release status (use 'draft' for the first-ever release on a draft app)" + required: false + type: choice + default: completed + options: + - completed + - draft + - halted + - inProgress + +jobs: + publish: + name: Build and upload to Play Store (${{ inputs.release_type }}) + runs-on: ubuntu-latest + timeout-minutes: 60 + + env: + # Read by android/fastlane/{Appfile,Fastfile} and the play_publisher plugin. + APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }} + PLAY_STORE_JSON_KEY_DATA: ${{ secrets.PLAY_STORE_JSON_KEY_DATA }} + SUPPLY_RELEASE_STATUS: ${{ inputs.release_status }} + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + # Pinned to the version we develop against locally — bump in lockstep. + flutter-version: "3.44.6" + channel: stable + cache: true + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + bundler-cache: true + working-directory: android + + - name: Decode release keystore + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + mkdir -p android/app/keystore + KS="$PWD/android/app/keystore/upload-keystore.jks" + # `tr -d '\r\n'` defends against CRLF or wrapped secrets: base64 + # silently folds stray bytes into the output and the resulting + # "keystore" then trips the JDK's ASN.1 parser inside Gradle. + printf '%s' "$KEYSTORE_BASE64" | tr -d '\r\n' | base64 -d > "$KS" + if [ "$(stat -c%s "$KS")" -lt 1000 ]; then + echo "Decoded keystore is implausibly small — check ANDROID_KEYSTORE_BASE64." >&2 + exit 1 + fi + # Absolute storeFile: build.gradle.kts resolves a relative path + # against android/app, an absolute one is passed through unchanged. + printf "storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=%s\n" \ + "$ANDROID_KEYSTORE_PASSWORD" "$ANDROID_KEY_PASSWORD" "$ANDROID_KEY_ALIAS" \ + "$KS" > android/key.properties + + - name: Install Flutter dependencies + run: flutter pub get + + - name: Run tests + run: flutter test + + - name: Run Fastlane ${{ inputs.release_type }} + working-directory: android + run: bundle exec fastlane ${{ inputs.release_type }} diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml new file mode 100644 index 0000000..77061b7 --- /dev/null +++ b/.github/workflows/ios-release.yml @@ -0,0 +1,101 @@ +# Publish Refetch to TestFlight / the App Store. +# +# Builds a signed IPA and uploads it through the Fastlane lanes in +# `ios/fastlane/`. Manual only — there is no push trigger. +# +# Repository secrets (Settings → Secrets and variables → Actions): +# +# APP_IDENTIFIER iOS bundle id (io.appwrite.refetch) +# APPLE_ID Apple Developer account email +# TEAM_ID Apple Developer Team ID (10 chars) +# ITC_TEAM_ID App Store Connect team id (numeric) +# APP_STORE_CONNECT_KEY_ID App Store Connect API key id +# APP_STORE_CONNECT_ISSUER_ID App Store Connect issuer id +# APP_STORE_CONNECT_KEY_CONTENT Base64-encoded .p8 private key +# MATCH_GIT_URL HTTPS URL of the private certificates repo +# MATCH_PASSWORD Passphrase the match repo is encrypted with +# MATCH_GIT_BASIC_AUTHORIZATION Base64 of "user:personal-access-token", +# used to clone MATCH_GIT_URL over HTTPS +# +# `bundle exec fastlane fetch_apple_info` prints ITC_TEAM_ID locally. +# If you prefer an SSH match URL, drop MATCH_GIT_BASIC_AUTHORIZATION and add +# an ssh-agent step instead. + +name: iOS Release + +on: + workflow_dispatch: + inputs: + release_type: + description: "Where to send the build" + required: true + type: choice + default: beta + options: + - beta # TestFlight + - release # App Store review + - build # signed IPA only, no upload + +jobs: + publish: + name: Build and upload to App Store Connect (${{ inputs.release_type }}) + runs-on: macos-latest + timeout-minutes: 90 + + env: + # Read by ios/fastlane/{Appfile,Matchfile,Fastfile}. + APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }} + APPLE_ID: ${{ secrets.APPLE_ID }} + TEAM_ID: ${{ secrets.TEAM_ID }} + ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }} + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }} + MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + # Keep fastlane's output readable in the Actions log. + FASTLANE_SKIP_UPDATE_CHECK: "1" + FASTLANE_HIDE_CHANGELOG: "1" + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + # Pinned to the version we develop against locally — bump in lockstep. + flutter-version: "3.44.6" + channel: stable + cache: true + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + bundler-cache: true + working-directory: ios + + - name: Install Flutter dependencies + run: flutter pub get + + - name: Run tests + run: flutter test + + # Generates Runner.xcworkspace and installs the pods that fastlane's + # build_app then archives with the match-provisioned signing identity. + - name: Build Flutter iOS + run: flutter build ios --release --no-codesign + + - name: Run Fastlane ${{ inputs.release_type }} + working-directory: ios + run: bundle exec fastlane ${{ inputs.release_type }} + + - name: Upload IPA artifact + if: success() + uses: actions/upload-artifact@v4 + with: + name: refetch-ios-${{ inputs.release_type }} + path: build/ios/*.ipa + if-no-files-found: warn diff --git a/.github/workflows/refresh-gemfile-lock.yml b/.github/workflows/refresh-gemfile-lock.yml new file mode 100644 index 0000000..12e2e34 --- /dev/null +++ b/.github/workflows/refresh-gemfile-lock.yml @@ -0,0 +1,56 @@ +# Regenerate android/Gemfile.lock and ios/Gemfile.lock on a runner and commit +# them back. Needed after any Gemfile change: Bundler runs frozen by default in +# CI, so the release workflows fail on a stale lock. + +name: Refresh Gemfile.lock + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + refresh: + name: Resolve ${{ matrix.platform }}/Gemfile.lock + runs-on: ubuntu-latest + strategy: + # Serial: both jobs commit to the same branch. + max-parallel: 1 + matrix: + platform: [android, ios] + + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + # Deliberately no bundler-cache: that is what enforces frozen mode, + # and resolving a new lockfile is the entire point of this job. + bundler-cache: false + + - name: Resolve dependencies + working-directory: ${{ matrix.platform }} + run: | + bundle config set --local frozen false + bundle lock + # The iOS lanes run on macOS runners and developers are on macOS too, + # so keep the generic "ruby" platform in the lock alongside Linux. + bundle lock --add-platform ruby x86_64-linux + + - name: Commit the updated lockfile + working-directory: ${{ matrix.platform }} + run: | + # Stage first: `git diff` ignores untracked files, so a missing + # Gemfile.lock would look unchanged and be silently dropped. + git add Gemfile.lock + if git diff --cached --quiet -- Gemfile.lock; then + echo "${{ matrix.platform }}/Gemfile.lock already up to date — nothing to commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore(fastlane): refresh ${{ matrix.platform }}/Gemfile.lock" + git pull --rebase + git push diff --git a/README.md b/README.md index 9f15e0a..0eb9bba 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,12 @@ are committed — set them in your CI provider or a local, gitignored `.env`: [match](https://docs.fastlane.tools/actions/match/) certificates repo. `fastlane fetch_apple_info` prints your `ITC_TEAM_ID`. +In CI the same lanes run from **Actions → Android Release / iOS Release**, both +manual (`workflow_dispatch`) — pick a track and go. The secrets each one needs +are listed at the top of `.github/workflows/*.yml`. After changing either +`Gemfile`, run **Refresh Gemfile.lock** so the committed locks stay in sync; +Bundler runs frozen in CI and a stale lock fails the release. + ## License [MIT](LICENSE) — consistent with the diff --git a/android/Gemfile.lock b/android/Gemfile.lock new file mode 100644 index 0000000..8768397 --- /dev/null +++ b/android/Gemfile.lock @@ -0,0 +1,375 @@ +GIT + remote: https://github.com/popupbits/fastlane-plugin-play_publisher + revision: b73f84e1e1620b2d606f726c826ed9c35e111edd + tag: v1.0.0 + specs: + fastlane-plugin-play_publisher (1.0.0) + +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1281.0) + aws-sdk-core (3.254.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.229.0) + aws-sdk-core (~> 3, >= 3.254.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.6) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + erb (6.0.7) + excon (1.6.0) + logger + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-follow_redirects (0.5.0) + faraday (>= 1, < 3) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.4) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) + fastimage (2.4.1) + fastlane (2.238.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 2.0.0) + faraday (~> 2.7) + faraday-cookie_jar (~> 0.0.8) + faraday-follow_redirects (~> 0.3) + faraday-multipart (~> 1.0) + faraday-retry (~> 2.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + irb (>= 1.8) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 4) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.106.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (1.2.5) + addressable (~> 2.9) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) + multi_json (~> 1.11) + representable (~> 3.0) + retriable (>= 3.1, < 5.0) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.66.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.0) + googleauth (1.17.3) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + io-console (0.9.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jmespath (1.6.2) + json (2.21.2) + jwt (3.2.0) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + net-http (0.9.1) + uri (>= 0.11.1) + nkf (0.3.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + pstore (0.2.1) + public_suffix (7.0.5) + rake (13.4.2) + rbs (4.1.3) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.7.0) + io-console (~> 0.5) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (4.2.0) + rexml (3.4.4) + rouge (3.28.0) + rubyzip (2.4.1) + security (0.1.5) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + trailblazer-option (0.1.2) + tsort (0.2.0) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + word_wrap (1.0.0) + xcodeproj (1.28.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + base64 + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + nkf + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + aarch64-linux + ruby + x86_64-linux + +DEPENDENCIES + fastlane (~> 2.225) + fastlane-plugin-play_publisher! + +CHECKSUMS + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 + atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1281.0) sha256=97fcc7d6a068ab790a1efcd8c6587c08fb108b4bed7ff1409cb47e0d642782ec + aws-sdk-core (3.254.1) sha256=518089e32134c3478cd4ec63d07fb966546a45e9e4cbf5f80d2cf16d5699d29b + aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749 + aws-sdk-s3 (1.229.0) sha256=7dd11a111875b3505d2ec0a0a383a6aab6c1badb3d4e94e7fbb958a24451f596 + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac + claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e + colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c + colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a + commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 + dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 + emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b + erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 + excon (1.6.0) sha256=ebe2ab83c055ef8e117c62c91a3535b966a59a64868188219d49ab90cd83b65f + faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb + faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe + fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 + fastlane (2.238.0) sha256=78e9252df2224c7e427012638e41051edfa29b133a40fda883fd2f1c13a77b98 + fastlane-plugin-play_publisher (1.0.0) + fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 + gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 + google-apis-androidpublisher_v3 (0.106.0) sha256=b90b5312b70f8f731def88f24a2b8fb791fe1b979a7c2c14a905cb6cae19cf3f + google-apis-core (1.2.5) sha256=e21562b5627a0fc6a0c3165e429c3afed0f6a628eaa514e83f7204dda1adff69 + google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 + google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254 + google-apis-storage_v1 (0.66.0) sha256=fdf6d65d3fc77e7046b3494333a818ece9e2afd763bb161e1a7a9e70cf198351 + google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 + google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7 + google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3 + google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330 + google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b + googleauth (1.17.3) sha256=9a655455885c0c17a8b70537acd2a992a7c7842c43c06c6612dfb0d93e755d58 + highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 + http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 + io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08 + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 + naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895 + optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbs (4.1.3) sha256=0c4474a9751cdc14364bfad0b3e53678323bbdc2c31683b0445932867dbab8c4 + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d + representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + retriable (4.2.0) sha256=90c3b257472a1c02f2a3dfa64dd35a420f7a624cef1a5981e20fdac5730f8cdc + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 + simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b + terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 + xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc + xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 + xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93 + +BUNDLED WITH + 4.0.18 diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock new file mode 100644 index 0000000..e3fb6e2 --- /dev/null +++ b/ios/Gemfile.lock @@ -0,0 +1,366 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1281.0) + aws-sdk-core (3.254.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.229.0) + aws-sdk-core (~> 3, >= 3.254.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.6) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + erb (6.0.7) + excon (1.6.0) + logger + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-follow_redirects (0.5.0) + faraday (>= 1, < 3) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.4) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) + fastimage (2.4.1) + fastlane (2.238.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 2.0.0) + faraday (~> 2.7) + faraday-cookie_jar (~> 0.0.8) + faraday-follow_redirects (~> 0.3) + faraday-multipart (~> 1.0) + faraday-retry (~> 2.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + irb (>= 1.8) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 4) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.106.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (1.2.5) + addressable (~> 2.9) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) + multi_json (~> 1.11) + representable (~> 3.0) + retriable (>= 3.1, < 5.0) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.66.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.0) + googleauth (1.17.3) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + io-console (0.9.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jmespath (1.6.2) + json (2.21.2) + jwt (3.2.0) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + net-http (0.9.1) + uri (>= 0.11.1) + nkf (0.3.0) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + pstore (0.2.1) + public_suffix (7.0.5) + rake (13.4.2) + rbs (4.1.3) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.7.0) + io-console (~> 0.5) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (4.2.0) + rexml (3.4.4) + rouge (3.28.0) + rubyzip (2.4.1) + security (0.1.5) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + trailblazer-option (0.1.2) + tsort (0.2.0) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + word_wrap (1.0.0) + xcodeproj (1.28.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + base64 + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + nkf + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + aarch64-linux + ruby + x86_64-linux + +DEPENDENCIES + fastlane (~> 2.225) + +CHECKSUMS + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 + atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1281.0) sha256=97fcc7d6a068ab790a1efcd8c6587c08fb108b4bed7ff1409cb47e0d642782ec + aws-sdk-core (3.254.1) sha256=518089e32134c3478cd4ec63d07fb966546a45e9e4cbf5f80d2cf16d5699d29b + aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749 + aws-sdk-s3 (1.229.0) sha256=7dd11a111875b3505d2ec0a0a383a6aab6c1badb3d4e94e7fbb958a24451f596 + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac + claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e + colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c + colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a + commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 + dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 + emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b + erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 + excon (1.6.0) sha256=ebe2ab83c055ef8e117c62c91a3535b966a59a64868188219d49ab90cd83b65f + faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb + faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe + fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 + fastlane (2.238.0) sha256=78e9252df2224c7e427012638e41051edfa29b133a40fda883fd2f1c13a77b98 + fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 + gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 + google-apis-androidpublisher_v3 (0.106.0) sha256=b90b5312b70f8f731def88f24a2b8fb791fe1b979a7c2c14a905cb6cae19cf3f + google-apis-core (1.2.5) sha256=e21562b5627a0fc6a0c3165e429c3afed0f6a628eaa514e83f7204dda1adff69 + google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 + google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254 + google-apis-storage_v1 (0.66.0) sha256=fdf6d65d3fc77e7046b3494333a818ece9e2afd763bb161e1a7a9e70cf198351 + google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 + google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7 + google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3 + google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330 + google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b + googleauth (1.17.3) sha256=9a655455885c0c17a8b70537acd2a992a7c7842c43c06c6612dfb0d93e755d58 + highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 + http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 + io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08 + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 + naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895 + optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbs (4.1.3) sha256=0c4474a9751cdc14364bfad0b3e53678323bbdc2c31683b0445932867dbab8c4 + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d + representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + retriable (4.2.0) sha256=90c3b257472a1c02f2a3dfa64dd35a420f7a624cef1a5981e20fdac5730f8cdc + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 + simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b + terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 + xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc + xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 + xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93 + +BUNDLED WITH + 4.0.18 diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 78e96a4..52f7527 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -82,6 +82,10 @@ platform :ios do # ------------------------------------------------------------------ desc "Sync code signing certificates and profiles via match" private_lane :sync_signing do + # CI runners have no unlocked login keychain, so match cannot install the + # certificate without a UI prompt. setup_ci creates a temporary one. + setup_ci if is_ci + load_asc_api_key match( From 722f36a42bceaf7ac5bcf58ad6c30f7e927957cd Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Aug 2026 13:52:59 +0545 Subject: [PATCH 3/5] Wire google-services.json and an iOS build-number override into CI Without ANDROID_GOOGLE_SERVICES_JSON the Gradle FCM plugin is skipped and the release ships with push silently disabled, so decode it from a secret when present and warn when it is not. TestFlight rejects a re-upload at the same build number, so allow one to be passed at dispatch time instead of editing pubspec for every retry. --- .github/workflows/android-release.yml | 17 ++++++++++ .github/workflows/ios-release.yml | 11 ++++++- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 43 ++++++++++++++++++++++++++ macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Podfile | 42 +++++++++++++++++++++++++ 8 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 ios/Podfile create mode 100644 macos/Podfile diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 0b627b6..56e15b7 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -100,6 +100,23 @@ jobs: "$ANDROID_KEYSTORE_PASSWORD" "$ANDROID_KEY_PASSWORD" "$ANDROID_KEY_ALIAS" \ "$KS" > android/key.properties + # Optional: android/app/build.gradle.kts only applies the FCM plugin when + # this file is present, so without the secret the build succeeds but + # ships without push notifications. + - name: Decode google-services.json + env: + GOOGLE_SERVICES_JSON: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON }} + run: | + if [ -z "$GOOGLE_SERVICES_JSON" ]; then + echo "::warning::ANDROID_GOOGLE_SERVICES_JSON is not set — building without FCM push." + exit 0 + fi + printf '%s' "$GOOGLE_SERVICES_JSON" | tr -d '\r\n' | base64 -d > android/app/google-services.json + grep -q '"project_info"' android/app/google-services.json || { + echo "Decoded google-services.json does not look like a Firebase config." >&2 + exit 1 + } + - name: Install Flutter dependencies run: flutter pub get diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml index 77061b7..90620fb 100644 --- a/.github/workflows/ios-release.yml +++ b/.github/workflows/ios-release.yml @@ -35,6 +35,10 @@ on: - beta # TestFlight - release # App Store review - build # signed IPA only, no upload + build_number: + description: "Override the build number (TestFlight rejects a re-upload at the same one). Blank uses pubspec.yaml." + required: false + type: string jobs: publish: @@ -86,7 +90,12 @@ jobs: # Generates Runner.xcworkspace and installs the pods that fastlane's # build_app then archives with the match-provisioned signing identity. - name: Build Flutter iOS - run: flutter build ios --release --no-codesign + run: | + if [ -n "${{ inputs.build_number }}" ]; then + flutter build ios --release --no-codesign --build-number="${{ inputs.build_number }}" + else + flutter build ios --release --no-codesign + fi - name: Run Fastlane ${{ inputs.release_type }} working-directory: ios diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end From a9ce8b297e41db0aa8784dd8265490c1b0d5c7a3 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 19 Aug 2026 13:55:59 +0545 Subject: [PATCH 4/5] Add the Play listing icon and feature graphic Google Play requires a 512x512 icon and a 1024x500 feature graphic before a listing can be published, and upload_screenshots had no images/ directory to read. Both are generated from the refetch mark and wordmark used by the web app. Screenshots still have to be captured by hand. --- .../android/en-US/images/featureGraphic.png | Bin 0 -> 24030 bytes .../metadata/android/en-US/images/icon.png | Bin 0 -> 7122 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 android/fastlane/metadata/android/en-US/images/featureGraphic.png create mode 100644 android/fastlane/metadata/android/en-US/images/icon.png diff --git a/android/fastlane/metadata/android/en-US/images/featureGraphic.png b/android/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 0000000000000000000000000000000000000000..2852c5cdd00202d54a426781c45f783496b0b673 GIT binary patch literal 24030 zcmeFZXHZjL)INF;6%+v*DqTgo6s30s0qLF4L6H`kAfYz}MNvTMy@wV;XrVV7y+ep} zq(kVv=fC~E_s-le_w${(^U90|;bfn)_g;IgXFcm#e$rG|q`XXb8G;~6rRUGIA?Q5# zmSl(g9JoAu*6Ir`7hkFKr^SJH(|0C(D3r*TtUA^s zUf;hH+4V}KEXk|!tlug%!C5$I_OMg(w|j!`zgMuJbT6X-S4cB}{NE+;{ndXLh~nOV zms_&`T^X^LR=XhcA`RyvY`i1?ndeN;50<~@s z)Z2Rv3alv|epSYOUppT?SQwR90UOv;w#$frCjXmT+^on}EbHdqNGVz2BrwQW0f!zK zBvjWZJ<^wEcCm@g%5fkSAzo#8L2S!=c02JDO9BO641u6K;9at=2obXPNC#^ojCR03 zh8cxK$|YWTw9<02>ia#hd%Nd2SQIl-{-hR0XtJhZzT~h9d}@tD+^0umPk5fOb7Xv~ z3Da%42OofavI|xJ$|G84$EBUs*KdHH$8QfbtH44!AQuiBsYN zRdA)1{UbyBI28)4O&GW_O}Z~t$Tbe{0i1aHi@Ojc^f6S&Tahv;d(DOei)F|OPulj< zGs@%ny!+F~NI)@Uzn$=?j0u9Sx|2gsa1O%q&G|ETDdW?NSa2M2!b!tIr?Orp?wGy^ zX8S%e@wzxN5Eh!u&qL{#yEzby8TVi)yn+1hB-5_8L*J1~Gm0F$zGLw!+zK3mo`{0y zx40^C9gpEh=v##auJy8YT$ZW&pg+M^1eQ%vGk0&MZh zWh!9O_dY=os!Sq!NPmV&3sZJ8?xnlX+bI`&;zhqR8=h0u-(J?RpbeWcPVuKMoT1Kz z%{(y^PfObLWLQUQ)tROA*LO)E_@|V>7l``qAusBJQ>ctU&Y&3lxOS=F_FzG!wPaQu zsrBvPVyiOLfF^<-sb3#6q@Ity`L|&0fHHqqj}}U&B8Q5LehH>{#Wjy*v>KGDTzVJ( zEbK6><_g}}!*1Z*9<>6`A4d3usb7nI(|I9B=82$b2x?S-0_A}jZ8K>TKhK6fs*O%B z86M2cWvkCj=&}1QXniruS~w}O9JfIbvUsPR7}COJ00oZRhnid++_8%pgtL5MKc2ou zyCJ1GncV34#KUA|imsxI_a6R0f)(q#p}cPE&RHD4L#1r@BKz;#uJ#GqML0xp7iu!P zZ?Y_=ebczxscTAiF}9;{B~zrocL-lNUK5mMnfMF3-Lgq!?XmEUi!c!S@Tfn8Ud&E=!d= zjBJWzP!Gy_bolFB^Hs=#ru+GBttRYD?~+<4@9ziQ9F*~Sd7f&uiiV^A9?tSeO{5`% z$+EME6w)+*$bXLRYw|ZbxA$x(Z8h~o<#6&{X~i3<71IJ&bvYmF-$={Yr-p`p+<*jq zg=@W2(A&(yqHTq5U7qFj9Bz3F?o!dbN;0KXwJHNEzuRcw9x{v!&JAXR`xJcB;!S2qv;EP2FX44~fZz6l?u+YZiutXd~mqqNsG?*Msg9MSG&eVYX0CnbR>^?fZ( zkD|q|JgyDDdMhP;Ga~=k_Cug?U$K&Y+6`(bRvvotbM|9z(GKSAEYzf`a1;cE7?p;b`ojzWDl6wd$Dh?{p1$qDT+^3E!5-YV~qS z0puo6p}?cx43Hp4TY~tu`fp^ssiNnyw9zS_a4q?HbKT_4lt-7}fX#L^yx9xfNR{33l69|Bc=SLi_ytjVSncz$P_I&ZuI%j&{Ctq7b8_j?L?FvnU%Yi zG6ieS(uQ$A2oCpPAKVxCsBi`?MW9_oSlutZF~3R&gi z)UCNS+~0ZJVSPQ_T&Tanb?aO2CmOm$#i|5H0%`$d2Vt2e!z*WjuaTo4trda z=b{2e#^OrUuJ}sN+2nJ;LiU$>sa6e9KYe2AxxYGRWh>CaO>mgFFWnbybbdW3P?L$d zrR(tppJj?VmsaH<{QCjp={4Jag@p0Js`r`GxT-mq&$)~v8Kw8F)(78qi5D9hjz-ri zUKrl;A~n&FDweo8X^@YO9x>{w21p;6H;o zPis18|B!-6rm9_Y(JV)eLN%U{FsQrlYn}a~oANt3WIQe(R`C;Y2B zmUnnQe!(F%^d9j{Y)B@U*`T(UaE!%X8L2XpQ*8m(W!#VTX;W|>$s;>DM#e(zn!$wZ z7WUJZ9WrSe$P41-a)zVj(-i1L+!;VlR*JGj+cvTwDNx7wV76tCw+dtFvGwpp zk~^`(di6bh=^Zh{xGC0`k%RY@c)S+}DTFp@(FrK#T>IfGGZF%S975Fv5B&ApXiVii zKPnXr96GuSIbzOkqlohGzJXp^@O#bAOz=KAzn1*M=k)XTXS(PU?cZL_6raCbnjPh6 z`&;ORohFA_>QU5NmP!UYcQO8=>2 z;yHXSkQO(dk%70hJ@|r-Q0dFRz5IDWZlAg??Y<=rk=-nlIX@n>H7{{LdPv=JO?UlG zbTm_OPlkYUeUCo=8r_@RhNR5j%*(BBKhz!Mu*lMy3a%LK);{;K|qQNNI zFD$S6xOrk8Sgn8UL1|USo5lt>2&Z^wG!P@G)&**##h=?|xSuOt^j~7o=GZn_%+^9n>uFSsqo#*9YdYXA;nw}6a zw2V0y@abxFbWzzlZYgT{$RI~qIO)1JKqfWegCfNK&IMgJ(GA$6CTsjsl-t?*xw8Ao z0@^%2OG8B4J(Z)KHaaGObAR7Mfg7=33!dJqK8R>RvobHqcr3Mk&%(?y_KtdVEZ~;# z9khmyGrhfawKd_r`_4(GA@|%vMpAOT-zP&+Uua%=l&h58~7VC;5reVs%lDy8uLZCh#VkFca=S$X-4P zL5v_1T~R}R0A}}M4E!G5K7GpKe0jqzYZ|e33Ln+Ln2e|UA6d9OBn{mdDlB_kn_&72 z<0*CIQZo^7V6fh(EuSxaRzKfR5c)WZ3q@%Y?Hx|pSE7e2lW<#pUvrvbv3q9PC%!FG z48qXawl z`7E8AsEO*a_(@4#yzjw~ne%-~jx3@pM|y#Gbbw-$bo|kS=_=~ZCG71FTB;UX2PYx5J`^42 zb~ogPV$Fki8cKM*v~Q#d+3>mBMDw0y9VMHO!?h3|?bV3~UoxkIJTIkB2o{?A7st06 zAeb4ADmNvac)ZPt4V%Awh>g0*x%*80{Tj<953{ao*8c>{H|SC4lz6?+$qL|~ zE_tIb3>i6iz>)%I4uZqEbs&QnV3GnD->{ET{V+U z63uik*8lV-O|HsW-gj6cQJ77zZ>|TmFUA&k?J9WTX})D!cazFt9Py>;NsD*N{9sXI zcTH}pv^rI;!dP~yC1J+siRLxvYS9W6g2~-MPIJI(%t%^J$7$1khABh`jT*;txs|Un z3rpO2V6&VUaNhnY__5{MU~7`x+`PGV4O@MId+vL~i?5$8)`bY}jA(k%>m(8qZ^!fv zuZS-qZ7ovKHg#7$1U_e~r zPg6c52HYr##Ap3xURtI^B&sXl`u5+DRs&N}v5x~v$H}>y z5f=%w&4TT{dVIlS4Ds+kE8=sKDF_c+^t+gTSy=tm=F@8Oxb6nnX>&-gK@Rw~T0@Fp z<+0~~J8^>oK;RVV{06?u=k;4pen4=LqAf9Y@ycq?6;2L&uRm>EJC(>1+_Dby`PJFi zgMC?+k4WRM>S9RCd>Ti_eKiM*J7`gjH!ze+Qgb|7?wkznQdUzNC1kS8y71K%u?QaR z@gIDJ<&WEmA5VOx&s_-Zdmlxx42jjRS8I(5C#P(@=fD(pPVYW+*90r27iOGj8a(Sl zXo=Vz$xduJRdJOk059=(<{mz#&gtyb*#~La zg<-%Jh$Ib^BCO~9eJK_47{Jy;$i!&-;8tDMt(@%4|xvb4CxQzW>DwSXgP z{2t(0EYEEUv)QeTkJLR!UUKAd!w&LMI$LJC2-sNEgSE=-G;r;}V_tRes(&U@rvt1*l2CRoUI88HGH~d0*1&Q>V zV-omSBoz1>aTu)k#B`)sLm)pIq zV+x{)jazJ*Z#o~CCyHH0>y@2*T~It=RptHV(L~*1ajYtSXop z1?$vk8crV@N||vhdDpnK9dk$FusXwhG~92=T*6h;XNbcmx70%^Qb2i$5#$5?BNwKw zy~LE^T#EI5we}yM&TYfjy&cW+kZE=;kHK3Wy)6-oo_Ol0q1RKgENZYBM(_sK;~kC* zpF{A`@{<7k1-7nl-mzuT#eOLlI9l<#%K4zIX~AXdMuoGJPHP=+GSjoVi3a1OTe^Mr zJ5hf(Htgq{dCLB*(0OCO9}h`mN%fq^L8rJg2%L6)>0IDct36-rs7-8uD}NK3x_rD6)@nmo^nhy)^4=ls<74 zx*B_Z;G9@QMQPM{MWcDZ6-g)biLCR4{7tmUmS?M@Q~2*@5fywml{mr?96`PM75eXtrbKBNKM!;OeC({>f1y$EGZyw`u6)=*?+014$9iD z>6z({53)koKO;N{5M32~Xq6KG;c64-iJ~ru-7 zuoF!v$CBo5FfwQLlumU$*UNbs*#X#0oYZTKZuJClP~vp?RFQpg#Rq&)!J8P9=z*ui zpa!mPS4up0G{uvQy=bw9HBwOeue-W0O^WDA*=E!IR@~WhMl}qqMC_5i!(K9C+?7=M z^g)4P)f9tgI~+YG8rr=*dlR?$h+V4ZO6TCA1-)pAx(5apfP@wG=e!phV*`G`BJs>! zE9IyP*n|Iv&H1y9!*awYvj$n?TZg0BnYwAx$bBYV`GieE^S|VJb3X8s=efY*=zhts zwG&m`(PhR1>>ad7|J%tsXQ&q7C3en!woe#ti<$P(d6jkhrQc!+@b9wLQMj$p)z#^ z(gsqo3JvDJ6$NMPuq#%tzWC3?0w&c}gvC*KSG=Q@PFWfQD9#k_Fq56ur+!D#HQ|Df zeI%sy7(;C50>VrxCm!VFJ2JXAwhyBNCKvGytdYKEt0o!y%tHta7h7BFoP+x=1B$li z{HBP3R#DDGh95oLSsOV(7?0XjCATDV0`!6t!6p1)T^`QK3 z>$=!?%?$Nx0OqFA`c#|+uvFrNwI<0NTb}}_#LPGh2oq}b4$B`kWp6P4ap=ZTZtL2P zY8Vky^t<}IY4jqdMH7CpEsOqXmG>wb6v5CF)g6uUXaqnM#5&!fMTG}pR|JCal#zo& z@QJSHKM|9*eaFr0O)CaPfH-gZgmqf{qMdusNT}lgNqE|FuNi;F*NIE2JRUv9%x=T` zj)6PNau)II+atZ1RQQ7-s0+1_-$jdc9GgK?KVQFu z=WF5;OJ0`jv>2sZHOc?YOy`_hX1qNicsBa@dBB%I3l`|^!wb&TdvX$jr!G8cUMo~i zbKb=DF_he>y%W>l{ybyBwQu5KC;8fT8FlmHk>z4P?5IG!BS&LslDD^=$R*P;)pZaa z^rc+9u4gKx)qJFU>KFNP?iXH!Degfr;H|}fw@!tgJakMp$Dgl7lze|`zr^^ZXEdLz zn*+P(^x8&T<9&nI_*CA$AALEZT=<9!!v!T_%g|h?&{^(gkRtwyWtXN@_^J0kP+{)#q zoZOfW4?1$FloRPiej%_`l-1^rU6r39RJ4{4*en)q7O`YyN17S|vt+G{>*B}0BYROG zz{L=Ai382+loe6w+OE%h@PKb-ab)bSF=OPv0AhGPlXzB01!1+; zs_a#R?O zjwJEkf4AH0ZSfCkO~Z@_cb7?S(ptVI(G2)7diZmrMh`K`ChV!5NYAi1$BPH(u>2m( z>yxWHtuxb=W8pgteETZWX`Afo8P$8dnk@f3$?2He=9{F{#q)qcUy!*uWiahxziOgZ zjq|i={loD$Durvu*)8Tap)8kFp!qIa+Z4mvHuF$|2X1#+IBC;w?VRXt-I~cGWkEst zt^QoWvl8g0h3A-&b{S+M^*r&;cvOcDQ{Y>@jQQU1rou35Pe54bfSOqfK&P{6;`?XN z{+XW{`q*e-4Lwl0#VcSt_rN37ug7x_<#P%=mW9u1H#pRQv)mTwB&oQak8{DC9Zb;^>d( zpJjgR=yCII;roR&Q3Esn*vLFISgLyNjhQoUn^X1bE=);9ku7D7$*Q_QRTz@!YZjzJ zHk7n=ebtz`c+|6`?Or|AmBP+H<(HuY)_?15m(g}Xltt)YKK7u={_MaPxZ#Ov0H=n= z;=*3rHJw-U)`KlWzYbZ?eOQp>GTq{y5<9@~WZn_V;VNU|(S;dRi`^N?O?f5WI#j z)zv1wz&fTGih@CEjrUvB38ukIzMWQAzGND$(NjYI=%r#^+hxb@TT+7a-A$PNMtJN* z%>YJ<%%qRb9o_9W_2)i&nDO%RZJbN1%RZX4iaL;w;oOs_?oHkL`ZuG6I)b_uD4K*{ zeU)`8=`g>%`P~!xaqb^}mFeh2**M}xFog2ty-Qq*XLYB>0qdvKHL`JMOTUoqqdNmJ z0eYkUE&9YOQ;*Y`D$S4Y#kVd{yqe2t(01K^PfEfH4)C|6 z5Zk(^$JUO?d>Hnxa)FXgQF1V;H?TWbJeg0_QH__~g#5R?uoAB-Nc0)s9np)BSqoTL zj?LXWJ>O*NRGh&g%h^Rgr4K<@{@L8zN^Fn)ykOT)A3%|~isFdZ#g;cLLG)WS`IqI0 zH~}=mueBg$fLJ$=?8?~*uURpy_p21<5j4YLVrUGxp_`2QhTp!eTD)<$8;y9%!68}Q z0jQn$Y$K6}oKudHoIfjN7}Qy9u0zms;IiYzi^VLs8#)$m@f(irq)jg85(WI_x)*RY z&O6OKBlR`&L4Yaq4&x12%dRJ;VV821FnR0$+}XnwJe4E$t{C)IFWUY!GM~+n)Kpm% z`0HzO)sMskdXa*6#cC;q-4p;%5Ey&zuEXYe=*3d&)0kN@2aM*=j^#NUyAdfazCOFS zA@7G@@mVx7m& znZyl7P)Q$?QeM|2eCQItkdgQy^R98oKJD<<@S5hR4h^mS$(oXa4TAPXv12zcI??LH z{Ze2P;1?b;iseO~JH*o#8JfCB04N0<{TJOv3j0U5S{DY6oOiqZ#EYxvgHpbMrJi9% z|JJ;m#AC6P7b$}fez4{W3McAv_T&7>>S^slA(H|+6Wq?XV42>&{C>tFV>HFIi^8^O|3+r>M__tjowQ6_bOmgQ#D(u_QuCT>& zm79NC)I2bX^z%zN`xCkj`hPJV@!eA4qBa?jO1M81SzWd^B%(i<=n?cm<)z9h<=m5q zM+!~}u#uyziieN=mjd6!+2fVHq_7kD;$k`p4k!KJ z_00+yI|g5*f2#mwen3fs=Y+*5@6Ry7LXa^_(ljTLl0W>%8|2DLzyO&*`lbT#^IZ(U zIt7&He!I$}>U6L1dW}b_2`5Q9$yd$tMuDHRx+=7pvW4iFMK}nzv0;oS8;bTwqxA=_ zzQ!n`Z0tv*{CCf|@y9a67Xm)S$Z!UJ=@n65M{_ohYV%KPaWCof@dh(D3``%@tn|Pq z#-AIf!JNmg8<9NepJ$$R!HYw`a`On}t0pgA+u;>xYZj^D1Ni608t-$)=ysr~FOM4n z<6F?CC|xbBO+eeKE#)s|<^TH-}*KSC97UEAi6J(9`QX=D`{(&uqZwp zV&&rDzXJjFLt(AZ+|USC`8m#TzxA)6tGtpT)x|9l;lLVTO(u?}nJX#qvB&&2Ds~ys zx8923+B~IeM4hW6d0e>$93C&=CRV z1QKc`HITBo{vIsC^^|(p0(j&Yl$)u`-3W?0xSAKAQ|bIdtR-~_m5(w$HOs(H96Ke_ zO}S5{st!K3e-1nOnp2^ldvBz+Xo!+~)ePxJKj>2Yt-?Lz!z8#%Xk9HpgeLq8E+zoh zk*dH8vs=pjLL`x;=m@-T5;JR0R4Dtrk<^0%J^)ai%cr%OLbLD`pRL_9LR3%gstQ3+ zLtIslYKV)w2qC8!4657G3wWoXkBjG1-ZUER7VXaYR~(YjyrOqzlI*a~D^DLngSv}L zlJ}RQacRH4OLWA%&EcogY5JXozjNy!m?IU~2g9p%mKd)%Sx;{-h3tPJ>WbeN{A-uu zpJM(9PttDjiRnju- zV9$($xF70;D)rCx*!Oee)MDI4agNbWUMe<|I=MVO)K0kO^)L~#t5<+?uiEl1Ka;5L z_##8Vh$vDmPl|Bb8+R!Jwqag9<@ib7&Kawtsl#hgA5u-N0x`^TK_lLqt2jGY*Yr>X zyMn|Jk@fa-E-=4%ZX?HI`xBOB!9sOoW#?q(8p}e8Qjqk+WtyIkg0pB<&~O$9YFzF` zqIOdeU-Xh1YF?L$UlE=-jw`o&vC0PBT%b*3vMl4Q590MvzpV;bqpiJ;g&2o^qIPnv z3G>NX_U_7LHZpCov-XOaDd4(-Fq->)Mu+cXdvv>h7N7zzexq4Nv?6*#oGGXBZDCDmD!#U>vDfOD6JO^Ozf0{|(K>dX4*xsXt8 z5CxXL*K#-XZV)Kf(&%VW0!}(TB_pEdpJfKa=eT$zj~*2dM}GxGe*U^A;!bvU4j?8$ zT9=dmK(`XUcepdtLiZ-5e}vXj;mGQCAH2>j3p*sKX>4)KV$W1i72}>eAB|}JQQ3~dLNw%h+2Yo5Z9mU1B3p7 z!hD5m7>i&P7N%V; zy2;lQ&#%0x^+>tZaHD@R=jXBDF{_$yb8Fd6CE+A(Bin;rI{*3yb1Tv{iUzN~MD~;F z&mOIRS<%Iw)dtdN8k1GpPUwc!utLlaYUMuJDXUUEGi;pNUasZzmpVDG*%MD1xKZJt zaHmj{cJz7NsUfYYhWL+B8uT|2yD=6v%s$bceu2=$R`!{HbULE^g&9F=9J+C;~4DFqL-rkv;)H6F4n4AS~uoDnPQmW7FC`mI5@J-$3w?LWkV*ywSZ^F@#)rwsRyg%nihOc)s%(T z!}QU>C{EUu$S}r=E~(biu-b3;c9(KBu<+Pn`2*l6ttA~_iKQQvth_;=V7|q};_c_< z0)azYpcyDKt1rFZ<;nI8mUHe2^u)6RkPPebrojO~Y1X9^(|1^9-1r|4S^#DxH2W1noBe!7=6beyhdMKT6YY6t`gIU&_ zb!xN=F~(P#CxPkWPt_@GIMl7C&MAx$fKMw54MXLyIdj%z2ej)`B>dKrASe_r2*`ZxIpt^gw zy(|h;PC$}Yv!j79t$FbL@|*Hk-kx=+T^1P?o%Jgjd4^_%OO5!g9=W{Hg@_*v^V$^6|uM4b82aYht3!7KI0*pY)@QyqVxfskcBVOneZ2|&x_bG`0$EazvaROVn_AmWTq#acIr$%pJNMIAXQ z--*EdsW&3l4Pm+-y~}-IKCp%x3%F<F^`CCzVs54DUbyF=yI)%olH3M7g~)R(-S-wXt?e zbl8X2yH#7)dpg)mdTVOs;L3Kv?>1+kwBdz;Sb6Wm%V?r-lTA?!)||jMQ@%6cJx-UI zsk%McuM%e`Awm_%59BVST4tH+IUF{XK=~yWVJuSzX3$l{GCQ}{7ZS19HR{H3fWiPh znslZ{TL0%b_(JH#{$q@rXWE3%6KA46S><3$`nMk@etYcTUG@quwUZ{dGbO3=^Mukic&Hm8gVv&Im8C5JsdOF&>Tc!i%XuRmK8Sq3YgQM!-` zFQMY9PS@kMuJgp_C>||mDI#orifg1<4-;kdqR1ABshVc354W#B0cJUmkaACy_EWC* zd54I3=iqnjEH$FROq5<6pB|I)K6I`3%pVG{F*4%r#4YqJd*m(>@b9WL9X79ZdC5oY z;%zkOho8{~e!Abu3z6SR)1a8tis)Mw?_bgWp#$zYQ@kqif>QfMxU`SaXPN${Ps;(H z63b%+xS-tKs+dZ1<6BD|jdu6~Z^j3fp+6W-CvO;?WE_PLd__e^^MazGI>CjTn6B@i zk1-!8(%X%5kaFTVR!zE7h zQk81X!yAT%ig*fZym!Y8{qO*TxMQ3&N&O2Jc=14Gjn$r1)52d{_N%B#dEpx&8DUJ{ zFc8XDzDB>wZ>gl5rg35T=u8Go#AsJPT*G2>1lKyu&=GHO* za)N^Ou1a2HJL5O~h|GX{pRvHMV)S$Kv}&exXrl5KgSiqt$GF<{`d-Yf=nn}4=q&Wc zZ8mUh?kR(My>rlak!pYMez97?bv0lgi|4Vcxx7DP`s9_ClvQ;cSmc}a`+n(U{A2dB zPac9w6XkZfT?PO%Ixo)hc0#os&z}CbZ5`J#qUQl&38cxB29G`|KbNTCJHS+TkcutZ z@55IOv#J4Kuapv!|MRYuyPioXosf>3EUw3RY>umY;9C$^XJ0A;s9^~WxNoFSy> z1r8F%8t?I6m|c5h@PuQf9=c;ut7=VjKYtFb^#SNuDsoeI__)u;2-W_+fGyp{Uiaz_kc9sL5FW+f zGH66{p6;K(b^VRoslBqYNNqTO$%d&~wH}atFF*Q7JMHw-d(M;6O&`A# zA8#UP>;a4>WmF6L{W`qOt+{;gF7d1MVOGLu+&Li9Re+uZ>3v0m8V4x3;II8q-r5b& zu-$c_;1ME)AYiwxz;4Hnv3~i$!(?w=@A7^++3@1*F%W&GO9LrslcRaTK6C%ydhp?w zzGghWie+Z65k}B# zq^^Gh#e?oHR9+`k?Grt$w;9$|)O_V%aBrBXHW!olpF2P=1I6ZHc|caFM}ZaJZ)B^r zKyAABY<{l@hA_Y$qqfcrLH1>)Y_U)<_Rk8??%_{Caj@Mw%#i=L*U{Gc{|L_idsrAd zRLqnX&d}SuPLqRm9%9=h(Z`mbiVH+y5K{0iAvaSL@I`^MfRC5QBlB`rXfk zfi*qvZ7T5MYp!KLTnlBqz4xTJMyDH)G5@2)FP~;f6p=WkZxG2Q1=QYi2SAb!4P=Xvfj=Du0ev98=OW6Uepl&M1v z^V$7B+6EY`%7EB3%Lx>M;A8zOa5$#!8PGj{ZnWKn6>tdE(t$3C0&=LyC0PqlkXg>K z{k~A8D-j6U@wxVq95KvduExI|`q%kj%K|}quNCqM`B49Q_yR!l z3f;5cob_2B)U@{nfHF3mT;TKO|7$BpU?UL!4ro$V>GJq+qaFRs>&rnQA57$E>Bg$z zPo4nK+IJN^JZ14btDsipH-MoWkGMcp{?hkEZWFD7A0}3gAdkZyajzO<-Z6M_y>sOO zAGvlOYPzc!OlJj@!$4~{X&k_LuC+NE|{02^b zBzP?2pX@@%kur5M5!78pvj|_avPNOh$uo5o3T%4wjN`HTw-Q(R4jMFh+!==XjAH6#z34X6oYI#uabTZmShqH3K>Omm&cO?6ffw6nKyDMd{jZ$>nu7g!k)#8G zKP0mM{)Ybl-2?frbt;giIuqxgp6H?TGVzs;ODe#a9}c;?1!_wz0I8-#uQXCNw4(h$ z34{$=)DMO^M*iBH!9cWiec%C5ntMrE!*1N|yA-OVE1$nbec{x|JtPqknSGixn@@f; z2Z4?8o&ySYAs}A4Z>e#T8+yHq7-4;C%2Y`kbHTh!z2;QPt9|#0N14jE2c`!;SP3=2 z(e)4=+&RhL(3-ln97pbiWQe==uE5W|YC4~Sh!FO};EO1Zu$j|t_?F0>S#rh+`kfAv zyFk?6LW8z9(++tW30%}(S~ON|kDS$}K%QFuxBag$_=Fm;2$m4Lp4dIJLVAr3>MEXRtF^nzYwg5CZn4 zJK*^#pVskP(2d}C5^N~cPLnk|&nEL;qg(e`_I=CFLXwTizka~^J&3u*Rm#h`38yPUY`K_c> zViq*KE%m`Nr(cz!>Yk-u1_Yv-xz|_=rA)B|IH8dObFaCGU-z7K{fAtLbhJJz4L)w~ z!0WazNUc${ExNk#u4gcgl;tdAkXx-g;qXCK8|2Ra>e$jXP;bLxq z1g$cCa*sZVMZr-A{p&552x>gdRt!8=-&ZSNxMFjm?eJs*_a?nG|&8 z~>Z#YZ$U3^4}S+U8{;T>2noDn)}sqPu{{T{Nk(F zLU<%_wTGuPT4^A$071xCkCd2H`@qkP;2?(a_<*D+($|r=l)bKDWQ<+QT6`8m1IHKK zXcSlKelGtEOwK#E&dvw4RDmLvnV-X=Z?Fo$dM^`0sq9BJ^crbm%u88h(Z}Fjx5~=> zmP$nK-2=k{6@SP+{J4L{dQRWJbr(fl@!+3}YN>B4x+8W!yuLoc6qk*j9eCR{QkhXx zi7IVP)3}~;*CWAUub{CyhASXB;FRYK+0NWndf!VbR7Ia44N?oVN;u=yzuMbt%H7dj zIe;FiYzXanpu7ak;%#`I#U@5`68SrGWMfJ0GQgX}XHK_n(YaAP)x}H&t_s+_|Pe>0Q9D z7)6%qxv@*sCP*h97n~jAvoI&~EXax>V~>Ku=Qq*-_N(BNqJtcSDQjeN&2zJNENwQt zK|lpM5V=uS@D6JUE<0%q>ZgAdmAt*nfxN`8q~kSERMAyRx_?ME1*WgxC0@V2+!yr| zH=`O>EmuUeV#g@HF~=3&R(I%kjPCY%-j>i});5vflZHzaQzTZddjeL|*Gy`N_y@O2 zB_*!$t)yKhkNpFa{qZ(I$Ej}w!tmqHU#g*>6@Hris ztxKoBTd~9d(QTwm!Fb>d)o8skv$VH~xz|RF__6e=DGG+^qY2Pzxuo<*!TE7Bllzh7sN_z5VPVKpGZ>7D}x24%SW&Mpg?;HufGQT{g5pxupcQF3h% z#OU+y{{?vE!Qdz5QJFKAb4nj^=IpI_x<|~Ok@*t?WQ~7CYzf!vUFZ5>Y|>e4Swe&h zpdz4>raeH_LN!sM>OAh{aMQfQtVQiy!|AvsnTDrPq;(^J$Y2>BNJ;1$j)vEkk7tB5 zXq$ury;(#6Cd$jRDD%e7)?BCt?stHuZk>LQG##`;McMdNu1MRYy<|p;#z+E|CWWp$ zZ859c7hn|{XmpMg(4JvF{oOK=JC{Z{4Y7|8jP5t7tV3p7gKUjhH*Wyh_Q?SjyVI#9 z{9)@5o;bMgoOY)?5z!BxvCTxREd;0*-du(=z4@dGz?-Aqnw~!5AO6MNQ>ksZxZhmTTx^5D2%tu!v7v)Lixd+Km?mKq)QJ9b@i%P)(iJ zk^nNTZW;i1lKg@Cp0;wS*PSQHWu^FS+|s+22T5Cdc7%x1QCtm%u5wU_&u%_#w~G*Y z7B{p4AK14|Gt;)lXNjP1^bc0el{nddW(xgsO59r$Y$#ZsARe$^N(c<*3^ufWrC`Nz zpUWsX6JKCUC=8&H|D6Fhb|xT=Fh*$i@_i%4ygbp0Ky8%c`_OltL62{IxSJLD6}lAs zVE_B@{49g|^E4Y_H@MmruikO2*aWKD)atsdFB2=OaAnD`qP; zUoDh6i>TxjzY!R&c9Z(7FI9c2m)a1H-L|AkGK>3WY19I`#AfI2rvR%CE@c(lKAOls zC8?R`E>~QC7~~<0F@;Ye*wuFo9*r^jcM7havD2-DZcxp9Hk?= z&vbFTsaL|_=w4dD#jj=|DgjSjFVn~JyWcc-I{#d4;ob3KfZZ-Tv)vn+nvX_Hb;tJe z8dMeRtZ#6b{K+q9l$ZN6x6CGYzdM@`d1@Z5kg0l#{_#Ak7hHX=bggInCeK&f;S?=C zo!oo5<~LTgA)~ZkH5a}RU3!bvm8YlE28i0;lrufl`U97)_H9da`lft}IPWJh99F#z zcqHT`Yai5GDsg*)OvzU?*2AAxP$5d^U@hSB23>c3h}FQK@?^%eGNFjEy>bE&gRCCQ zo1=bKW|E}OPMtRrJU9a~MFY|6zip0WB;RY#R4)5-A*?P6%ov1Vor zrF2TC!&<`g)A?j?T4Vjq>6E9`2K8)XySJBxGxslSlB5%c3lHKqWvIOGsY|BNr?GH{ zFv-QA-U{&ISiB}aS8K85?Jo%iUhE2z zknFCih5W07XKzk;Nsh=@_|lp7Nn)ZB$c=FDCyXn4HU{!sCg|<1yslRH) zk;IXz9$jD#4_l-4GN!9U`kCviMuwwDSvKwS0ReNv=6ADW z)tr23Qt_EiYPIwc4lE3JfIR%7O3ucXR3C%%lXW0ffg^aKHjVRX7c&uech=+Il@f8{g-*M z6^&9`vb$CBqy9_1Mb0}OEF>1cTlxYNpDjDao^c=a+%koPAAa&0toLFaFn{rLI{sWP z9ek26?ixhl5vr0si~oAixz$Mq=(cIoS7*(0fZUb|+2-fBS?ovd!0NgOTRq$z#{ z-&n`{)1d=Kv8+`!Y>d+Z^?8A}*FO&3v+U5b5Z2e{U3}u&D}0xa(o0=i)!k6D6*EI|txi8`4ki6eRr#e)>6Qhv zU(?S7FbD*`I6Am{n7oH$4pAq4*Pa%)MrA~N;zi3W`SXVds`0f_%U6;o;=Ju7d$(bg zqa_2U)DMn{a40m%A|M%I3zz_sX;5lFOGL#jNdf!Wt8`#r8v7B15dth zwXc%%ONN$*+iIRZ#EUg1*^RfU=E5cYYpF!4CvY~N8j@sD{yT6no0li40%lfQH#)<{ z8@%0Bc_g0o)MZ{j?VVTu67Z5Pf1|sTs+lVre@VM#XLHjhHtvF2+W%?i%HN^zzW<;| zmaqO`()2p2AS*$W66By{a<{4 z`22X!bMJHRbMLw5yk4*8JU3BUW@6Q6Tv7Baff1)snF`amjHcFpM(bmmxHD?YW|s3h zLT2)L3^?@-@L%_F(kkwM9PH_hx> z7x})vQr9L%Ou=}n!$+AT=MD*}%`1cqdS!{DA!qJ@r1bZ69Un(@42l{LlUC?7cYm_G zCG+3~Ins^u8m@n|Eznn*I}(KEuh`3Im+WY-*gX}`IpKR_qX|GFS*KahJ0hTz())nnCIGvy>#K-!U*}R2 z8C$~9;r?UsUd1WHlJQ242$nXG|68mZ(o$3^Uw``JQ}uessDp}WcJY=sCBG{Dt`UPg z1LVHn{S?YIv58cES9>5T9?wkUIgddp)Rm=ZVaOzq8q+O#493Z2UM=0!ebU%wBwm2u zF#vHV=3JjXz0gTLwe}ziR=>r$m0sB&vH8qCFr{J-UkhrLkVGa0`)KWZx~lqdVDZ7?{6 z-6BOz`}EVdc>YMFmGYd4Qu^GTdihJGxA%EEjq}10lxwmQq)Zkg>3((9JqPM%&g%Hx zU$~#Jv+WSM$}YEQUZvPz%}RzoLXtE<;UW9ANN$t;lZ)a)xkeSebgXcuWG$YAMIP@pWFX7L97@Hp_Z+NTg7j} zR3w9VsB2VXls;QEK3u(bdo)bwA>(-rx#lfe zenTrGINa{Se6JriDS|>Hm6LX&qn`lyPIU|V$n7iFUS2>(&IC==k+=`IinYJq(fKCf zIo7$nM?v~Imv)PvYT+6dR&v0$@fuR5sVS}2CU30|)_xg&QMos(V^^^L-VacL;y zP_gzZ*m(GhvA@@-+l?cw<)gN9oB7!%t(XDr6UCC*hSjElTG7n^yr$p(U{uT1`GpJJ z{ycPXuSGR*Av;vv+4}vbIm2JB+-)*+3?AQ!Q1#KGhqeY=ceM|>ElZw)D;Af?B9%|7lyqu%e3 ztf`Ymo_kW*$wC)~wiX~{O`7@l&cmXW8!s+8f4dG`v|uOUBO^|B*r~OCc3+Wde*LB| z74E5esJ9j9g^aouG0hi6?E67Q8}LW!(hvuK>-6v|Yc;=h?0x2r3bCU7N(iFX{IQTc zhSPDj?fqmm+xaQw8tgkzzaD(Ye!n$BmOaGClhV7G0WtHQHa+T@^KeM^%mJtbLTmee zkByF}mU;$i)-%$U7iI$QD?z=NCG*jyP(XFY9|tpX^;Va-ExaTlFC+~)IHi%(EZN%j zDc1dYT;&s4LWH)Rm>+(0yTYPPwQJ{Tr*6l?kliaeKm8mX|Ahn`2me+~u}j3>>r6qm zYB5u%!{d-owL zs|q)y_vWHyMYrcX%O?O-ucH&$wnnSgu_` zF?vcwKg84!vcVU(KY>_xJ=M`oi^J!5CcA|0v2twp(40kQJ~M5SYt=-cyDFQAD!0_G zD@~xhCf<~L^lW9~KD2ff=l!V|$?MFb2QWRpjH9wv+PXvGT28o+jN!z#C^<|63xl01 z1#!0{#S}GDT-{;jfm+=S5A&SfEuDu@n&SA2w-X%ht#_sg70>!9lg7 z9!pphIFj^h?DKv6)R^MymcPh(P_MaTA#zKJ8QW4#bZn9L@dKCN$H*LCoafPNL z3Yf^iR?6|uMC>Q$&t-%TJI4qQlyPowfAGyEL=etjNh%IFLVj2kASSLd_exIAP+ham zIvdp;+85^kukGSPrw^*%}FY8>g5P$WK#oJO2(bXhR`#liZ&EvrB{Maq;I@X zwNu5qPi)WsJpbJFh(^#N@W(Few`{&4*{a`cT4(C*plh^oCg6flkMawq7HMoLO4)#} zb`I2I9HG&GH{!>JdQ{!S9ZLFMum$m$)g%!se*9H{=Y_zGpn`U)6!%S9SV&r?$!H$M zy&BU@Wrmk@@0Nfa(=9ngGh;voGh$ZcyPb^~0JX)i?%G143S(x1I2sQxp$?)!>9^^{ zR|Z8Iycic>#p}-J2xLu;&8vE_Q<&9Q7M|-x7Y_Kj(koO(b7LK?10AnyA|3UAlq=H- z7->;|ggI(i6gV-~>vJ&Ik-6p3?~@e_8de)`72vwt9nm&o7b9q zr{u%yTq>vDRkC#yq$+n1km9Aa=M$s~^ALBkVLK}O7{$bb6AVW=d5kN+tia^L>t zL$TAvMES!uvUK(+@UtQpe7$qLqj!Q8AAwqBA30~z0v`R6>%Ef`D8eE;B+{%t@i z^OJeC)}Q?cLXA6O_QAi>p|W&~b?IO0^(7<>uTdlbp}vU*sV)H*5>3ZZcvI+_fy1#? zis{RJx~yOhzEfP@T{3CWWf{MJ?C&F-P20=MDY`J4E8upRwUKV;H(m*f_RAv|$Xc>- zZa*C=$``NGdOCj)y3ARACSbbJQzqSa?&-}hjlYo#zx^%(0;%>3xn6Ypb{8PhZ1S*dkPj^O{%dg(u>RU?+Z6 zJh&6a*oEQBCRh00xQbR{%ac~EI$Ogj4?5`qP4j8 z$3EtV3%cOv|MHpkQDud*F_3!;*R~eF2Pi`c0jo9Z-)82Y9|t=%3qkPJ)4LSz9~^!o zOhk8=Q7N5oX-&X?V01u(Q%v8&1p9aCjo&FR_WEpN0+BHL^$iv{AqNIO*}YR&_5=^a za}PIz>4hCn&gK6=%q2Jm#b)l|jVW~5fKY_)g&(dCCz`%=mPko48o+SeW$*yY9^i!) zHD3<3g=;bhAN0b$l4z79g{9aA?doNUaH8t%gG3bCf^>Ovz1q!itYNV+bJh=WDRUO< zQ57PBvE5sKgeTdZ=i^ouhA!mS+}+mF(sh=(B*Ri|Bl-(DD6c8Z_t4U!JZJb#_|`(t zI)`K?t8tLCeP?qBMN1nBbNi9E1(=`uy5ZL>tJcA4g%I2GEBiF08kPEA+ra_LoCg z#CaO#_x3@cvaP}B!In$D@7!Z}mgRbZDHL7XhY^tk-_y)czQ&1{% znKTZ5&0pnR>4 zcd=v*HW7ibgvwt#?d7O)kt5a?<07jp>VCOnvjcyEl?XK+SNz?@mHzsd~FC`a%hO!@+Qw7~b*uB?uu6oTwae>AaqkB*FOS4!U5!w^eBE&>j8M+)Gku1WM zQm7N)q_RWEC!ffY=WoZd4mgVQ10vWewI0dc6`D^F8D7Z1W$C zR)U$D7kAgLi0WmesQ8kzYb%ir$!>E5cwo^mb4o7c6%}mc1b+3p;p&^L literal 0 HcmV?d00001 diff --git a/android/fastlane/metadata/android/en-US/images/icon.png b/android/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0d8a9e41b3510d633a3d96c8ed0dba5c54162c33 GIT binary patch literal 7122 zcmeG>X*kr~+h@iUiHwSB9=lR$q3~EDQUF(|KpqU>B{eRq#mfsJ%w&ju9wmhwaMe5#^{gF*2IIyQN?OwWzLk(1jcgpL@< z6rs1?pI;O8h}pe+a^QRL@Z@S8S1|FZzws3Fqs}$cv8{VHfYRI>bOZq2XaJP&06;QF z0T3$(fb(_;%@hD&rvU(s`=HQm;pT>@>XTH!kkURNlOmO@R7Vcs`Pc0Qe_gPkll>Pp`7O zz5r*20amggCVioTxCaB)Vt~w*Ixp@iRgk9-(0hj}c~A&6oF-Dt!hN04Sb!GEADXw6 z9@v5U4rw)(sOTP>?y&qQh~cGQ#^EZ+X~b)NA$RB}!|C?MMg=74r=Tm#HUpsyNoBkn z;3T<$BFHu4_d0z`0U*~(T`bY(bLK~2%c=%>-(zJ!Oya(EQN+_w>lft zQ3(WgH)`&b$dKgyCO>fWJRgk@e3~Y|otN|b;fk9aE`Cb47eSyRK;z4z8O*uogEduY znL#OLXH%re(Ey1BBNhvl4Ovlf0W%K&1)M%uLClu_VwIT#0c(a}#4xs^A*-dmoHg`c z0JaWdIHXpkf!}{2u)Skd><40YZ@QUi(y#zHDb~tRa{m~1)&4tPpwR~fMyI}>VY|Nf zVVU5sm(e$oz^^epI49aQ%ii;5C3YyZg9$CEfyC|{t7Yr9)0zt(o~}vb695S8$XMtP z*ADGCR?;c;S_s;Rz?Npr3sUm!RLz#2zPKMpo&-oefR?GyMPEjN6Bk*0qBwvSfXi?c z;JC2(UP1*`;Bp5F5dLBD9XSa`{(?&-_|+MH)dM4ZaEU_!$CvCIkH)CjQR5<~iL0lb zlEfiH-)JBc?#0DQ;#1?RjoR(<>x%j=o|?Lnlw`$zbt&Rgr8i6Cv6Y`WRhqH3S-r&8 z;Cj1LRk_uLzppMuzE|uYW9ssY>Vrgu3gYv4En9WA!TK$YM3MqZ*%3XgmKblMal4&0 z>vCJrMUk`S*_kHPk*>G7a~y_^@5@7|CZW_ zV2&#aFyEPlMk+n-ZnB+jtF)(xS_@-p(quRSV7g^7IJ-#Sg<>(fmFBN+p%)LdliEtj{oJmwI4vDL z7*&z}3(axl=$M_Ri0e=ym6?^>scX?{fw6Sij9Zr2AyLpxQqk>L`1}kXe1|S87qJdo z+upDH>b@9$Et%66G9=U2O0 zN0%MAJ=+l!fCkdP0z=B3f{rVj3Oln}`?C6(922M(9id&lNjm7 zcWkDmLe-?vQt4=PA>)X%4z$3B}~d2-~03g;I-rqmb3LpR=zXt$wEj<_*` zF{}euzm4PLW#%3zC464}5q_ph`GJqn1dq;$p~mD*jd_=W=n{`k202Wp#;LN#hH*)R@BS)i^SR8uKArp}RZ?=`De_M|&=A+>>*e%1 z8dT3T=$q(X(}^Db4>n~OV!zD1FI+PzRO~Q{-{R!Q(=i6sFe5w>Z`OTPnEHsQ}J~S8)-bwm4-Py#Hs2^K5m;uuV^h*bNbeUJLJg#8B zR|C3eQcM9Q{KUX~gchU6Ai|L*#b28y57u4naI8V$v$a!q{$Ls`>+viY13T;XWI;hY zJ&x^Yek|Rc?_fJ2zOS&Bu8C~o<%7(0kk>i~nl<0%hvg-7&6~~ZE)?<_hQ^%kCMXG~ zW)3d->EIeQkiX+Wv)+W-D4*#_fzothdC5kKW%L{9M{UBE#$y~om(2Ma7;Op@?LJ!I zl*teFRqQ?c~*{oG!$tc9FG1Ir1^Du~MP{b(0~T`i0-aInn2^b^^&$f}Yr;Ki)-Ow4E6@leG`btR?m6#lKbr!!R z7it6vsPhN}R&A`yNoGJ?3=L?H!B+ovQRGaed@rXT$|;Wo(xs3h!*!an$d!ir=z|fl zeqGg2u07i|f5MwQ86yOt&BM~#7a@;yd8$v2U0I+G?jg((Q3Q4s*4A%ajlm;l+U=lM z{;AqHB1$A0Z7caBp2}z1Eon&&u!Xf!B$veF-TAh#F7gLyTQ8@4YgpD+VOd%C z-0+M8RYsY3&yQh;9+!xv zPu1)_sdaNyt`sMMaCX&z{yj&W)qRzc{CRk>EjZ2A(X5#3v(v0yQqW>=D6uc!#G`ArG!Nn1*nyH6Y;jd>0Gv2;zpDL%fo@~t6j;p zm{@tqK4ATj{*8bi$0Vg(58np`-rhuv6|iV!tCnJG17Rg-(PZEq#3+o0>A!@JosTpL zBlO<9jR{TuyJTrW23ER#QCum}(s-8pZ|%Td*m`&Ep0slyG;G}j<76haz}4jw@@CX04?0)u;LHJuTGx_!L_339ifg={(q%jU zuv0MjlV@ECK~@z8BKA2S^(;6ihqiXYpnsO{h3%<_iTpyvrz_jVUgTbKl9O9VMGskk zY5c(W%&vlH^4Ffu?e+i3zwQ^Xl>YFSNKAdbO1~fK0MW%ZtF4WfR&o;@iS{rVzNfAd zIERmfNxm_X1$h^sC>pr)gRoj|3!jD47>~j1aaxRoSorLnVtL zugu*j{*_e2d}FyI;hK)Voy|cjM>DbF0G(*7s&jhrUGFpli3$iC38wt-Vmg?PXJW17Vo6ujglbs)MVgy;Q^UUYqtJCV-U40Z_V}K`utkf~p&X2zA8pp?!g|T>{FRTmC3*ssY zZtn~f2kqz6|MQ6G1rz*K*b&Ip?3UzKR| zleRRq@B~({WToNGpF@g=uj%0K+@ZOjb~@8wmi@%~mAR5o*9A&d+WsF~Q}#{aqk8{h z-=6ZhMJwrykvPQaWlG4N+8waM)?(lx--)~Gfo9SrO3kzOaxZolwyDH}5$nP&7*$)A z#1w33ySLV?QC<#mxII6+TpHMWDjIYJK-kxe;?;K54${?O*6;{EzU&}gFO!vbDT1zv z-UW{wZXGtkrnZ&xaDvX#=X20GLLO!*JHAw%|+OU`_7&M!}x${MbLD4Qpz(^RZpbucAjq>vz8 z%_ozfLS?(r{Fif2=sCnvymYS*S zlGo!Q&Qap4mh`4YT(~H>^}@X^r0%?Trb5m2SA?Ks9)(5S0PcH5C4U|Mv4~(RJ)^$h zK%El~7&U79gTUooP~x@=lo zv>W4ptJmp>TeR$#fg`$UVDC^)x#FX|$&I|DG`yPVN#Us-a)*(i3afL(e!ak-?jGL` z$4>Ijb}sCR%hpQ2Eg0X6t(8`**sWp{)=H=bcI!t$v68~6@0X}g46kxuS}}@ImE5YT zyy?ikO0LEyj5_VAQa)~7&)H2nVPgH#?qiZ~*E+OJ8Z?|3i&pM_RGppKPYMZJU0Dfj zrwRyK4SlT_tnBQYTKkp4mka-F$&Pr<%ZBO%_pwBdGzkblJq)0^q3A-HK^-%k|7EtGiZ`R^0?QoJE0M-n2!` zZ#%D{xRJdaHG7K4H67H^9SE#~C-ns16;FyDPr(%2YqsdY8RW?pkMhviO76HUz9JnG zo`S>B6_4`TYp#WDvOJFCwW<@|G`&WEBnfrrJb+W=3YO>KY7Tz?7N7LnLXG=nE~U1# z2hi{O4RRS;h#Lb zWMRg+!4PqU!Wyn{OlaG>FT6ULz;;{_ffo=_ekfAxpSgp)b5ac6$mZug&t8siEM)Z1 z>ucE@IXche)yZuoJx_5C6vR9tr+*A=1=xgHg|7BkT%f;)e2GsQ*JHA_E&Hl-h~>j%ELa zp}QBgcO?Fe1bQ|~U8N70!zDMvlV2UGnMH;#dz4a;@B+fQZTd1sYTOU|O?)so?RdzZ zyjK1&ec=3MI-56q3@>v(WWUGEn}jPrS;&Uo=2}2K6(<4 zE1=R J`K0sB{{dFq0b>9F literal 0 HcmV?d00001 From 43624c26e152c0e76e31195b9ccc52e069f2adff Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 20 Aug 2026 06:59:37 +0545 Subject: [PATCH 5/5] Ignore Google Play service account keys The publishing service account JSON is a private key; fastlane reads it through PLAY_STORE_JSON_KEY_PATH or PLAY_STORE_JSON_KEY_DATA, so it never needs to be in the tree. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 6252cab..363b7b2 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,11 @@ app.*.map.json # Firebase config files contain project keys — each developer/CI supplies their own. **/google-services.json **/GoogleService-Info.plist +# Google Play service account keys — point fastlane at one with +# PLAY_STORE_JSON_KEY_PATH, or hand it to CI as PLAY_STORE_JSON_KEY_DATA. +**/*service_account*.json +**/*service-account*.json +**/play-store-*.json .env *.env