Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/actions/resolve-flutter-build-metadata/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
name: Resolve Flutter package metadata
description: Resolve Flutter build-name/build-number values, release tag metadata, and artifact naming.

inputs:
platform:
description: Platform name used for platform-specific release tags.
required: true
require-tag-build-number:
description: Whether platform release tags must include +BUILD_NUMBER.
required: false
default: 'true'

outputs:
build_name:
description: Flutter build-name value.
value: ${{ steps.metadata.outputs.build_name }}
build_number:
description: Flutter build-number value.
value: ${{ steps.metadata.outputs.build_number }}
artifact_ref_name:
description: Sanitized ref name for artifact names.
value: ${{ steps.metadata.outputs.artifact_ref_name }}
is_tag_build:
description: Whether the current build resolved to a release tag.
value: ${{ steps.metadata.outputs.is_tag_build }}

runs:
using: composite
steps:
- name: Resolve metadata
id: metadata
shell: bash
env:
PLATFORM: ${{ inputs.platform }}
REQUIRE_TAG_BUILD_NUMBER: ${{ inputs.require-tag-build-number }}
run: |
set -euo pipefail

if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then
echo "::error::This package workflow only supports release tags."
exit 1
fi

tag_name="${GITHUB_REF_NAME}"
if [[ "${tag_name}" =~ ^${PLATFORM}-v([0-9]+)\.([0-9]+)\.([0-9]+)\+([0-9]+)$ ]]; then
build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
build_number="${BASH_REMATCH[4]}"
else
if [[ "${REQUIRE_TAG_BUILD_NUMBER}" == "true" ]]; then
echo "::error::Invalid ${PLATFORM} release tag '${tag_name}'. Expected ${PLATFORM}-vX.Y.Z+B, for example ${PLATFORM}-v0.1.0+5."
exit 1
fi

if [[ ! "${tag_name}" =~ ^${PLATFORM}-v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "::error::Invalid ${PLATFORM} release tag '${tag_name}'. Expected ${PLATFORM}-vX.Y.Z+B."
exit 1
fi

build_name="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
build_number="${GITHUB_RUN_NUMBER}"
fi

artifact_ref_name="$(printf '%s' "${tag_name}" | sed 's#[^A-Za-z0-9_.+-]#-#g')"

echo "build_name=${build_name}" >> "${GITHUB_OUTPUT}"
echo "build_number=${build_number}" >> "${GITHUB_OUTPUT}"
echo "artifact_ref_name=${artifact_ref_name}" >> "${GITHUB_OUTPUT}"
echo "is_tag_build=true" >> "${GITHUB_OUTPUT}"

{
echo "## Package metadata"
echo
echo "- Platform: ${PLATFORM}"
echo "- Tag: ${tag_name}"
echo "- Build name: ${build_name}"
echo "- Build number: ${build_number}"
} >> "${GITHUB_STEP_SUMMARY}"
23 changes: 23 additions & 0 deletions .github/actions/setup-flutter/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
name: Setup Flutter
description: Install a Flutter SDK from Git and add it to PATH.

inputs:
flutter-repository:
description: Flutter SDK Git repository.
required: false
default: https://github.com/flutter/flutter.git
flutter-ref:
description: Flutter SDK branch, tag, or ref.
required: false
default: stable

runs:
using: composite
steps:
- name: Install Flutter
shell: bash
run: |
set -euo pipefail
git clone --depth 1 --branch "${{ inputs.flutter-ref }}" "${{ inputs.flutter-repository }}" "${RUNNER_TEMP}/flutter"
echo "${RUNNER_TEMP}/flutter/bin" >> "${GITHUB_PATH}"
135 changes: 135 additions & 0 deletions .github/workflows/ios-unsigned-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: iOS Unsigned Release

on:
push:
tags:
- 'ios-v*'

permissions:
contents: read

concurrency:
group: ios-unsigned-release-${{ github.ref }}
cancel-in-progress: true

jobs:
package:
name: Package unsigned iOS IPA
runs-on: macos-latest
timeout-minutes: 60
env:
IOS_BUNDLE_IDENTIFIER: ${{ vars.IOS_BUNDLE_IDENTIFIER }}

steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Flutter stable
uses: ./.github/actions/setup-flutter

- name: Resolve package metadata
id: metadata
uses: ./.github/actions/resolve-flutter-build-metadata
with:
platform: ios
require-tag-build-number: 'true'

- name: Show Xcode environment
run: |
set -euo pipefail
xcode-select -p
xcodebuild -version
swift --version

- name: Show Flutter environment
run: flutter --version

- name: Precache iOS Flutter artifacts
run: flutter precache --ios

- name: Get Flutter packages
run: |
set -euo pipefail
for attempt in 1 2 3; do
if flutter pub get; then
exit 0
fi

echo "flutter pub get failed on attempt ${attempt}; retrying after cleaning temporary pub cache files."
rm -rf "${HOME}/.pub-cache/_temp"
sleep $((attempt * 10))
done

echo "::error::flutter pub get failed after 3 attempts."
exit 1

- name: Install iOS pods
run: |
set -euo pipefail
cd ios
pod install

- name: Apply release bundle identifier
run: |
set -euo pipefail
if [ -z "${IOS_BUNDLE_IDENTIFIER}" ]; then
echo "::error::Set repository variable IOS_BUNDLE_IDENTIFIER before creating an iOS release tag."
exit 1
fi

if ! printf '%s\n' "${IOS_BUNDLE_IDENTIFIER}" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]$'; then
echo "::error::IOS_BUNDLE_IDENTIFIER has an invalid bundle identifier format."
exit 1
fi

perl -0pi -e 's/PRODUCT_BUNDLE_IDENTIFIER = com\.example\.techpie;/PRODUCT_BUNDLE_IDENTIFIER = $ENV{IOS_BUNDLE_IDENTIFIER};/g' ios/Runner.xcodeproj/project.pbxproj

matches=$(grep -c "PRODUCT_BUNDLE_IDENTIFIER = ${IOS_BUNDLE_IDENTIFIER};" ios/Runner.xcodeproj/project.pbxproj)
if [ "${matches}" -ne 3 ]; then
echo "::error::Expected to update 3 Runner bundle identifier settings, updated ${matches}."
exit 1
fi

- name: Build unsigned iOS app
run: |
set -euo pipefail
flutter build ios --release --no-codesign \
--build-name "${{ steps.metadata.outputs.build_name }}" \
--build-number "${{ steps.metadata.outputs.build_number }}"
test -d build/ios/iphoneos/Runner.app
actual_bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' build/ios/iphoneos/Runner.app/Info.plist)
if [ "${actual_bundle_id}" != "${IOS_BUNDLE_IDENTIFIER}" ]; then
echo "::error::Expected bundle identifier ${IOS_BUNDLE_IDENTIFIER}, got ${actual_bundle_id}."
exit 1
fi

- name: Write unsigned artifact notice
run: |
set -euo pipefail
cat > build/ios/UNSIGNED_ARTIFACT_README.md <<'NOTICE'
# Unsigned iOS artifact

This artifact is unsigned. It cannot be uploaded directly to TestFlight and may not install directly on a device. It is only for verifying that the source code can produce an unsigned iOS package in a macOS/Xcode environment.
NOTICE

- name: Package unsigned IPA
run: |
set -euo pipefail
rm -rf build/ios/ipa
mkdir -p build/ios/ipa/Payload
cp -R build/ios/iphoneos/Runner.app build/ios/ipa/Payload/Runner.app
cd build/ios/ipa
ditto -c -k --sequesterRsrc --keepParent Payload TechPie-unsigned.ipa
test -f TechPie-unsigned.ipa

- name: Upload unsigned IPA artifact
uses: actions/upload-artifact@v4
with:
name: techpie-ios-unsigned-${{ steps.metadata.outputs.artifact_ref_name }}
retention-days: 30
if-no-files-found: error
path: |
build/ios/UNSIGNED_ARTIFACT_README.md
build/ios/ipa/TechPie-unsigned.ipa
9 changes: 8 additions & 1 deletion ios/Runner/NativeGlass/NativeGlassButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,29 @@ final class NativeGlassButtonPlatformView: NSObject, FlutterPlatformView {
private func applyButtonAppearance() {
let image = symbolImage()

#if compiler(>=6.2)
if #available(iOS 26.0, *) {
applyLiquidGlassAppearance(image: image)
} else if #available(iOS 15.0, *) {
return
}
#endif

if #available(iOS 15.0, *) {
applyModernFallbackAppearance(image: image)
} else {
applyLegacyFallbackAppearance(image: image)
}
}

#if compiler(>=6.2)
@available(iOS 26.0, *)
private func applyLiquidGlassAppearance(image: UIImage?) {
var configuration = UIButton.Configuration.prominentGlass()
configuration.image = image

button.configuration = configuration
}
#endif

@available(iOS 15.0, *)
private func applyModernFallbackAppearance(image: UIImage?) {
Expand Down
9 changes: 9 additions & 0 deletions ios/Runner/NativeGlass/NativeGlassConfirmationButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,12 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform
}

private func applyButtonAppearance() {
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
applyLiquidGlassAppearance()
return
}
#endif

let image = symbolImage(named: sfSymbol)

Expand All @@ -139,6 +141,7 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform
}
}

#if compiler(>=6.2)
@available(iOS 26.0, *)
private func applyLiquidGlassAppearance() {
let image = symbolImage(named: sfSymbol)
Expand All @@ -155,6 +158,7 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform

button.configuration = configuration
}
#endif

private func symbolImage(named systemName: String) -> UIImage? {
return UIImage(systemName: systemName)?
Expand Down Expand Up @@ -184,12 +188,17 @@ final class NativeGlassConfirmationButtonPlatformView: NSObject, FlutterPlatform
actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))

if let popover = actionSheet.popoverPresentationController {
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
popover.sourceItem = button
} else {
popover.sourceView = rootView
popover.sourceRect = rootView.bounds
}
#else
popover.sourceView = rootView
popover.sourceRect = rootView.bounds
#endif
}

controller.present(actionSheet, animated: true)
Expand Down
2 changes: 2 additions & 0 deletions ios/Runner/NativeGlass/NativeGlassDropdownMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,15 @@ final class NativeGlassDropdownMenuPlatformView: NSObject, FlutterPlatformView {
private func applyButtonAppearance() {
let image = symbolImage(named: sfSymbol)

#if compiler(>=6.2)
if #available(iOS 26.0, *) {
var configuration = UIButton.Configuration.glass()
configuration.image = image
configuration.title = label
button.configuration = configuration
return
}
#endif

if #available(iOS 15.0, *) {
var configuration = UIButton.Configuration.plain()
Expand Down
7 changes: 7 additions & 0 deletions ios/Runner/NativeGlass/NativeGlassSelect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,15 @@ final class NativeGlassSelectPlatformView: NSObject, FlutterPlatformView {
private func applyButtonAppearance() {
let image = symbolImage(named: sfSymbol)

#if compiler(>=6.2)
if #available(iOS 26.0, *) {
var configuration = UIButton.Configuration.glass()
configuration.image = image
configuration.title = selectedLabel
button.configuration = configuration
return
}
#endif

if #available(iOS 15.0, *) {
var configuration = UIButton.Configuration.plain()
Expand Down Expand Up @@ -246,12 +248,17 @@ final class NativeGlassSelectPlatformView: NSObject, FlutterPlatformView {
sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))

if let popover = sheet.popoverPresentationController {
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
popover.sourceItem = button
} else {
popover.sourceView = rootView
popover.sourceRect = rootView.bounds
}
#else
popover.sourceView = rootView
popover.sourceRect = rootView.bounds
#endif
}

controller.present(sheet, animated: true)
Expand Down
4 changes: 4 additions & 0 deletions ios/Runner/NativeGlass/NativeNavigationBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,12 @@ final class NativeNavigationBarPlatformView: NSObject, FlutterPlatformView {

navigationItem.title = configuration.title
navigationItem.largeTitleDisplayMode = configuration.largeTitleMode ? .always : .never
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
navigationItem.subtitle = configuration.largeTitleMode ? nil : configuration.subtitle
navigationItem.largeSubtitle = configuration.largeTitleMode ? configuration.subtitle : nil
}
#endif

if #available(iOS 16.0, *) {
let leadingItems = configuration.leadingItems.map(makeBarButtonItem)
Expand Down Expand Up @@ -220,9 +222,11 @@ final class NativeNavigationBarPlatformView: NSObject, FlutterPlatformView {
if #available(iOS 16.0, *) {
barButtonItem.isHidden = item.hidden
}
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
barButtonItem.identifier = item.id
}
#endif
return barButtonItem
}

Expand Down
Loading