mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Firebase push notifications and update related configurations

+537 -78
+76
.env.example
··· 1 + # ------------------------------------------------------------------------------ 2 + # Build metadata 3 + # ------------------------------------------------------------------------------ 4 + FLUTTER_BUILD_NAME=1.0.0 5 + FLUTTER_BUILD_NUMBER=1 6 + 7 + # App identifiers 8 + ANDROID_APPLICATION_ID=org.stormlightlabs.lazurite 9 + IOS_BUNDLE_ID=org.stormlightlabs.lazurite 10 + 11 + # ------------------------------------------------------------------------------ 12 + # Android signing (Play + side channels) 13 + # ------------------------------------------------------------------------------ 14 + ANDROID_KEYSTORE_PATH=/absolute/path/upload-keystore.jks 15 + ANDROID_KEYSTORE_PASSWORD= 16 + ANDROID_KEY_ALIAS=upload 17 + ANDROID_KEY_PASSWORD= 18 + 19 + # Optional: if using android/key.properties templating 20 + ANDROID_KEY_PROPERTIES_PATH=android/key.properties 21 + 22 + # ------------------------------------------------------------------------------ 23 + # Google Play 24 + # ------------------------------------------------------------------------------ 25 + PLAY_PACKAGE_NAME=org.stormlightlabs.lazurite 26 + PLAY_TRACK=internal 27 + PLAY_CHANGES_NOT_SENT_FOR_REVIEW=true 28 + 29 + # ------------------------------------------------------------------------------ 30 + # App Store Connect / TestFlight 31 + # ------------------------------------------------------------------------------ 32 + ASC_APPLE_ID= 33 + ASC_TEAM_ID= 34 + ASC_BUNDLE_ID=org.stormlightlabs.lazurite 35 + ASC_KEY_ID= 36 + ASC_ISSUER_ID= 37 + ASC_API_PRIVATE_KEY_PATH=/absolute/path/AuthKey_ABC123XYZ.p8 38 + 39 + # ------------------------------------------------------------------------------ 40 + # AltStore PAL / Classic 41 + # ------------------------------------------------------------------------------ 42 + ALTSTORE_DEVELOPER_ID= 43 + ALTSTORE_CONTACT_EMAIL= 44 + ALTSTORE_MARKETPLACE_TOKEN= 45 + ALTSTORE_APPLE_MARKETPLACE_ID= 46 + ALTSTORE_SOURCE_JSON_URL=https://example.com/altstore/source.json 47 + ALTSTORE_ADP_HOST_ROOT=https://example.com/lazurite/adp/ 48 + ALTSTORE_CLASSIC_IPA_URL=https://example.com/lazurite/lazurite.ipa 49 + 50 + # ------------------------------------------------------------------------------ 51 + # Firebase / FCM 52 + # ------------------------------------------------------------------------------ 53 + FIREBASE_PROJECT_ID= 54 + FIREBASE_ANDROID_APP_ID= 55 + FIREBASE_IOS_APP_ID= 56 + FIREBASE_ANDROID_PACKAGE_NAME=org.stormlightlabs.lazurite 57 + FIREBASE_IOS_BUNDLE_ID=org.stormlightlabs.lazurite 58 + 59 + # File locations in this repo 60 + FIREBASE_ANDROID_CONFIG_PATH=android/app/google-services.json 61 + FIREBASE_IOS_CONFIG_PATH=ios/Runner/GoogleService-Info.plist 62 + 63 + # APNs auth for iOS push via Firebase Cloud Messaging 64 + FIREBASE_APNS_KEY_ID= 65 + FIREBASE_APNS_TEAM_ID= 66 + FIREBASE_APNS_P8_PATH=/absolute/path/AuthKey_APNS.p8 67 + 68 + # ------------------------------------------------------------------------------ 69 + # GitHub release automation 70 + # ------------------------------------------------------------------------------ 71 + GITHUB_REPOSITORY=owner/repo 72 + GITHUB_RELEASE_TAG=v${FLUTTER_BUILD_NAME} 73 + GITHUB_RELEASE_NOTES_MODE=generate 74 + 75 + # Optional: API token for release automation jobs 76 + GITHUB_TOKEN=
+6
android/app/build.gradle.kts
··· 3 3 id("kotlin-android") 4 4 // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 5 id("dev.flutter.flutter-gradle-plugin") 6 + id("com.google.gms.google-services") 6 7 } 7 8 8 9 android { ··· 13 14 compileOptions { 14 15 sourceCompatibility = JavaVersion.VERSION_17 15 16 targetCompatibility = JavaVersion.VERSION_17 17 + isCoreLibraryDesugaringEnabled = true 16 18 } 17 19 18 20 kotlinOptions { ··· 41 43 flutter { 42 44 source = "../.." 43 45 } 46 + 47 + dependencies { 48 + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") 49 + }
+29
android/app/google-services.json
··· 1 + { 2 + "project_info": { 3 + "project_number": "251668382991", 4 + "project_id": "lazurite-push", 5 + "storage_bucket": "lazurite-push.firebasestorage.app" 6 + }, 7 + "client": [ 8 + { 9 + "client_info": { 10 + "mobilesdk_app_id": "1:251668382991:android:621a1c3c3e6afc1f66ea7a", 11 + "android_client_info": { 12 + "package_name": "org.stormlightlabs.lazurite" 13 + } 14 + }, 15 + "oauth_client": [], 16 + "api_key": [ 17 + { 18 + "current_key": "AIzaSyD0AcVccAcTyBAFqhfahqiPS8CvUHKI7Kg" 19 + } 20 + ], 21 + "services": { 22 + "appinvite_service": { 23 + "other_platform_oauth_client": [] 24 + } 25 + } 26 + } 27 + ], 28 + "configuration_version": "1" 29 + }
+1
android/settings.gradle.kts
··· 21 21 id("dev.flutter.flutter-plugin-loader") version "1.0.0" 22 22 id("com.android.application") version "8.11.1" apply false 23 23 id("org.jetbrains.kotlin.android") version "2.2.20" apply false 24 + id("com.google.gms.google-services") version "4.4.3" apply false 24 25 } 25 26 26 27 include(":app")
+255 -72
docs/release.md
··· 1 1 --- 2 - title: Release Audit - Apple App Store + Google Play 3 - updated: 2026-04-15 4 - scope: Repository audit for likely store submission blockers or high-risk policy issues. 2 + title: Release and Distribution Guide 3 + updated: 2026-05-02 5 4 --- 6 5 7 - ## Sources 6 + ## Shared Release Baseline 7 + 8 + 1. Pin and verify toolchain. 9 + - `flutter --version` 10 + - `dart --version` 11 + 2. Set version in `pubspec.yaml` and/or pass build flags. 12 + - `--build-name` => human version (for example `1.4.0`) 13 + - `--build-number` => monotonically increasing integer 14 + 3. Run release gates. 15 + - `flutter pub get` 16 + - `flutter analyze` 17 + - `gtimeout 1200s flutter test --reporter=failures-only` 18 + 4. Tag immutable source. 19 + - `git tag vX.Y.Z` 20 + 5. Build store artifacts from that exact tag/commit. 21 + 22 + ## Environment Variables 23 + 24 + Use the root `.env.example` as the canonical variable list. Keep real values in untracked secrets (`.env.local`, CI secrets manager, etc.). 25 + 26 + ## Google Play (Android) 27 + 28 + ### Build 29 + 30 + 1. Configure real release signing (upload key), not debug signing. 31 + 2. Build AAB (preferred by Google Play): 32 + 33 + ```bash 34 + flutter build appbundle --release \ 35 + --build-name "$FLUTTER_BUILD_NAME" \ 36 + --build-number "$FLUTTER_BUILD_NUMBER" 37 + ``` 38 + 39 + 3. Artifact: `build/app/outputs/bundle/release/app-release.aab` 40 + 41 + ### Deploy 42 + 43 + 1. Enroll in Play App Signing. 44 + 2. Upload `app-release.aab` to Internal testing first. 45 + 3. Promote to closed/open/production after validation. 46 + 47 + ### Notes 48 + 49 + - If you need the same signing key across multiple stores, provide your own app signing key when configuring Play App Signing. 50 + - For non-Play Android channels, ship signed APKs (Play consumes AAB; side channels consume APK). 51 + 52 + ## Apple App Store (iOS) 53 + 54 + ### Build 55 + 56 + 1. Use an explicit App ID + matching bundle ID. 57 + 2. Build signed IPA: 58 + 59 + ```bash 60 + flutter build ipa --release \ 61 + --build-name "$FLUTTER_BUILD_NAME" \ 62 + --build-number "$FLUTTER_BUILD_NUMBER" 63 + ``` 64 + 65 + 3. Artifact: `build/ios/ipa/*.ipa` 66 + 67 + ### Deploy 68 + 69 + 1. Upload using Xcode or Transporter to App Store Connect. 70 + 2. Wait for processing. 71 + 3. Ship through TestFlight (internal/external) first. 72 + 4. Submit selected build for App Review. 73 + 74 + ### Notes 75 + 76 + - App Store Connect associates build using bundle ID + version + build string. 77 + - As of 2026, Apple requires Xcode 14+ for uploads. 78 + 79 + ## AltStore.io 80 + 81 + AltStore distribution has two distinct paths. 82 + 83 + ### AltStore PAL (EU marketplace path) 84 + 85 + #### Build/Package 86 + 87 + 1. Build iOS release (`flutter build ipa --release`). 88 + 2. Submit via App Store Connect with Notarization (or App Store approval, which also results in notarization). 89 + 3. Download the Alternative Distribution Package (ADP). 90 + 4. Host ADP exactly as-delivered; preserve directory hierarchy and do not modify `manifest.json`. 91 + 92 + #### Deploy 93 + 94 + 1. Accept Apple Alternative EU Terms Addendum. 95 + 2. Register Developer ID with AltStore PAL API. 96 + 3. Add returned marketplace token in App Store Connect Integrations. 97 + 4. Publish a Source JSON with required app/version metadata. 98 + 99 + ### AltStore Classic (sideloaded IPA path) 100 + 101 + #### Build 102 + 103 + 1. Build/sign IPA (`flutter build ipa --release`). 104 + 2. Host IPA at stable HTTPS URL. 105 + 106 + #### Deploy 107 + 108 + 1. Publish/update Source JSON. 109 + 2. Keep newest entry first in `versions` array. 110 + 3. Ensure each release updates `version` (`CFBundleShortVersionString`) and/or `buildVersion` (`CFBundleVersion`). 111 + 4. Include accurate `downloadURL`, `size`, and optional `minOSVersion` / `maxOSVersion`. 112 + 113 + ### Notes 114 + 115 + - AltStore determines latest release by `versions` ordering, not dates. 116 + - AltStore checks declared app permissions/entitlements against downloaded app package. 117 + 118 + ## F-Droid 119 + 120 + ### Build Strategy 121 + 122 + 1. Decide target: 123 + - Main F-Droid repo, or 124 + - Your own F-Droid-compatible repo. 125 + 2. For main F-Droid repo, create an `fdroid` flavor without proprietary SDKs (Firebase/Crashlytics/Play Services tracking dependencies are not allowed in main repo). 126 + 3. Build signed deterministic APK for that flavor. 127 + 128 + Recommended flavor command pattern: 129 + 130 + ```bash 131 + flutter build apk --release \ 132 + --flavor fdroid \ 133 + --target lib/main_fdroid.dart \ 134 + --build-name "$FLUTTER_BUILD_NAME" \ 135 + --build-number "$FLUTTER_BUILD_NUMBER" 136 + ``` 137 + 138 + ### Deploy 139 + 140 + 1. Prepare fdroiddata metadata and submit inclusion proposal (prefer metadata merge request path). 141 + 2. Keep upstream releases tagged. 142 + 3. Aim for reproducible builds from day one (strongly recommended by F-Droid, harder to retrofit later). 143 + 144 + ### Notes 145 + 146 + - If Firebase push is required in Play/App Store builds, maintain a clean non-proprietary F-Droid flavor where push is disabled or replaced. 147 + 148 + ## Obtainium (Android direct update channel) 149 + 150 + ### Build 151 + 152 + 1. Produce signed APK artifacts for direct install. 153 + 2. Prefer a stable, machine-discoverable release URL source (typically GitHub Releases). 154 + 155 + ### Deploy 156 + 157 + 1. Publish release where source exposes: 158 + - version identifier 159 + - at least one APK download URL 160 + 2. If multiple APK variants exist, keep filenames explicit (`arm64-v8a`, `universal`, etc.) so users can filter reliably. 161 + 162 + ### Notes 163 + 164 + - Obtainium supports GitHub, GitLab, F-Droid repos, direct APK links, and HTML fallback. 165 + - Cross-store signing matters: updating from F-Droid-signed to non-F-Droid-signed builds may fail due to signature mismatch. 166 + 167 + ## GitHub Releases 168 + 169 + ### Build 170 + 171 + 1. Build release artifacts from tagged commit (`vX.Y.Z`). 172 + 2. Generate checksums for all distributables. 8 173 9 - - Apple App Review Guidelines: <https://developer.apple.com/app-store/review/guidelines/> 10 - - 1.2 User-Generated Content requirements 11 - - 5.1.1 Data Collection and Storage (Privacy Policies) 12 - - 4.8 Login Services (third-party login exception for service clients) 13 - - Google Play User Data policy: <https://support.google.com/googleplay/android-developer/answer/10144311?hl=en> 14 - - Privacy Policy requirement 15 - - Account deletion requirement (if account creation is supported) 16 - - Google Play Developer Programme Policy (UGC section): <https://support.google.com/googleplay/android-developer/answer/16070163> 17 - - UGC terms acceptance + moderation expectations 174 + Example: 18 175 19 - ## Findings 176 + ```bash 177 + shasum -a 256 build/app/outputs/flutter-apk/*.apk build/ios/ipa/*.ipa > checksums.txt 178 + ``` 20 179 21 - ### No in-app Privacy Policy link/text 180 + ### Deploy 22 181 23 - - Policy mapping: 24 - - Apple 5.1.1(i): privacy policy must be linked in App Store Connect and in-app in an easily accessible manner. 25 - - Google Play User Data: privacy policy link/text must exist in Play Console and in-app. 26 - - Evidence in app: 27 - - Login has no privacy/terms surface: `lib/features/auth/presentation/login_screen.dart` (see UI around lines 54-205). 28 - - Settings has no legal/privacy entry: `lib/features/settings/presentation/settings_screen.dart` (lines 69-151). 29 - - About page has external links and email only, no privacy policy link: `lib/features/settings/presentation/about_screen.dart` (lines 8-97). 30 - - Existing backlog confirms missing policy: `docs/TODO.md` (lines 62-65). 31 - - Impact: High probability of rejection by both stores until fixed. 182 + 1. Create release from tag. 183 + 2. Attach binaries (`.aab`, `.apk`, `.ipa`, `checksums.txt`, optional symbols/maps). 184 + 3. Use generated release notes, then curate manually. 32 185 33 - ### High Risk (Google Play UGC): No explicit Terms/User Policy acceptance before posting UGC 186 + CLI example: 34 187 35 - - Policy mapping: 36 - - Google Play UGC policy requires robust moderation, including requiring acceptance of Terms of Use and/or user policy before users create/upload UGC. 37 - - Evidence in app: 38 - - Compose allows direct posting with no terms acceptance gate: `lib/features/compose/presentation/compose_screen.dart` (lines 567-588, especially Post action at 584-587). 39 - - No in-app terms/user policy screen found in `lib/`. 40 - - Impact: Elevated Play policy risk for social/UGC apps. 188 + ```bash 189 + gh release create "v${FLUTTER_BUILD_NAME}" \ 190 + --generate-notes \ 191 + build/app/outputs/bundle/release/app-release.aab \ 192 + build/app/outputs/flutter-apk/*.apk \ 193 + build/ios/ipa/*.ipa \ 194 + checksums.txt 195 + ``` 196 + 197 + ### Hardening (Recommended) 198 + 199 + - Add artifact attestations in GitHub Actions for build provenance. 200 + - Keep each release asset < 2 GiB. 201 + 202 + ## Firebase Push Notifications (iOS + Android) 203 + 204 + ### 1) Firebase Project and App Registration 205 + 206 + 1. Install CLI tooling. 207 + - `firebase login` 208 + - `dart pub global activate flutterfire_cli` 209 + 2. Run: 210 + 211 + ```bash 212 + flutterfire configure 213 + ``` 214 + 215 + 3. Commit generated `lib/firebase_options.dart`. 216 + 217 + ### 2) Platform Config Files 218 + 219 + 1. Android: place `google-services.json` at `android/app/google-services.json`. 220 + 2. iOS: place `GoogleService-Info.plist` at `ios/Runner/GoogleService-Info.plist` and include it in Runner target. 221 + 222 + ### 3) Android Gradle Wiring 223 + 224 + 1. Add Google services Gradle plugin in project/plugin management. 225 + 2. Apply `com.google.gms.google-services` in app module. 226 + 227 + ### 4) Apple Push Prerequisites 228 + 229 + 1. In Xcode, enable `Push Notifications` capability. 230 + 2. In Xcode Background Modes, enable: 231 + - `Background fetch` 232 + - `Remote notifications` 233 + 3. Upload APNs auth key (`.p8`, Key ID, Team ID) in Firebase Console > Project Settings > Cloud Messaging. 41 234 42 - ### Moderate Risk (Apple UGC 1.2): Posting-side objectionable-content controls are not explicit 235 + ### 5) App Initialization and Runtime 43 236 44 - - Policy mapping: 45 - - Apple 1.2 says UGC/social apps should include a method for filtering objectionable material from being posted. 46 - - Evidence: 47 - - Reporting/blocking exists (good): 48 - - `lib/features/profile/presentation/widgets/profile_action_buttons.dart` (Report/Block UI around lines 85-140). 49 - - `lib/features/profile/presentation/widgets/report_dialog.dart` (report flow lines 10-220). 50 - - Moderation controls exist for viewed content (good): `lib/features/settings/presentation/settings_screen.dart` (Moderation section lines 75-77). 51 - - No explicit compose-time objectionable-content filter is visible in compose flow. 52 - - Impact: Could pass if platform-side moderation is accepted by review, but still a non-trivial risk without clear reviewer notes. 237 + 1. Initialize with generated options: 53 238 54 - ### Reviewer-access risk for App Store 239 + ```dart 240 + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 241 + ``` 55 242 56 - - Apple "Before You Submit" requires full reviewer access (demo account or demo mode for account-based features). 57 - - App is account-based and login-gated; no repo evidence of dedicated reviewer/demo path. 58 - - Impact: Common review delay/rejection if review notes do not include working credentials. 59 - - Create a real Bluesky account for reviewers, populate it with sample content, and 60 - provide the credentials in App Store Connect / Play Console. 243 + 2. Request user notification permission (iOS) before expecting token delivery. 244 + 3. Register and sync FCM token with backend; rotate on refresh. 61 245 62 - ## OK 246 + ### 6) Channel-Specific Caveat 63 247 64 - - In-app report mechanism exists for posts/accounts: 65 - - `lib/features/profile/presentation/widgets/report_dialog.dart`. 66 - - Block/mute actions exist: 67 - - `lib/features/profile/presentation/widgets/profile_action_buttons.dart`. 68 - - User-reachable contact info exists: 69 - - Email link in About: `lib/features/settings/presentation/about_screen.dart` line 11 + UI lines 90-93. 70 - - Sign in with Apple requirement appears likely exempt: 71 - - App behaves as a client for a specific third-party service (Bluesky), matching Apple 4.8 exception language. 248 + - F-Droid mainline build should not include proprietary Firebase dependencies. 249 + Keep push-enabled binaries for Play/App Store/AltStore/Obtainium, and maintain a non-Firebase F-Droid flavor. 72 250 73 - ## Fixes (In Priority Order) 251 + ## Primary References 74 252 75 - - [x] Add a dedicated Legal screen and surface: 76 - - [x] Privacy Policy (in-app link + readable text summary) 77 - - [x] Terms of Use / User Policy 78 - - [x] Reachable from login and settings/about 79 - - [ ] Add UGC policy acceptance flow before first create/upload action (compose, media upload, messages if applicable). 80 - - [ ] Document moderation operations in policy/reviewer notes: 81 - - [ ] How reports are handled and SLA 82 - - [ ] What objectionable content rules apply 83 - - [ ] Replace placeholder identifiers and release signing setup: 84 - - [ ] Android `applicationId` 85 - - [ ] Android release signing config (non-debug) 86 - - [ ] iOS bundle IDs + distribution signing 87 - - [ ] Prepare App Store review notes with working reviewer credentials/demo path. 88 - - [ ] Verify account deletion obligations: 89 - - [ ] If any account creation is enabled in-app, add in-app deletion entry point per Apple/Google rules. 253 + - Flutter Android release: <https://docs.flutter.dev/deployment/android> 254 + - Flutter iOS release: <https://docs.flutter.dev/deployment/ios> 255 + - Android signing + Play App Signing: <https://developer.android.com/studio/publish/app-signing> 256 + - Play App Signing help: <https://support.google.com/googleplay/android-developer/answer/9842756> 257 + - App Store Connect uploads: <https://developer.apple.com/help/app-store-connect/manage-builds/upload-builds/> 258 + - Apple explicit App ID / bundle ID requirements: <https://developer.apple.com/help/glossary/app-id/> and <https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier> 259 + - AltStore PAL distribution: <https://faq.altstore.io/developers/distribute-with-altstore-pal> 260 + - AltStore source format: <https://faq.altstore.io/developers/make-a-source> 261 + - AltStore updates/version ordering: <https://faq.altstore.io/developers/updating-apps> 262 + - F-Droid inclusion policy: <https://f-droid.org/en/docs/Inclusion_Policy/> 263 + - F-Droid inclusion workflow: <https://f-droid.org/en/docs/Inclusion_How-To/> 264 + - F-Droid reproducible builds: <https://f-droid.org/docs/Reproducible_Builds/> 265 + - Obtainium tracking/source behavior: <https://wiki.obtainium.imranr.dev/app_tracking/> and <https://wiki.obtainium.imranr.dev/sources/> 266 + - GitHub releases: <https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository> 267 + - GitHub release notes automation: <https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes> 268 + - GitHub build provenance (artifact attestations): <https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations> 269 + - GitHub release asset limits: <https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases> 270 + - Firebase Flutter setup: <https://firebase.google.com/docs/flutter/setup> 271 + - Firebase FCM Flutter setup: <https://firebase.google.com/docs/cloud-messaging/flutter/get-started> 272 + - Firebase Android config (`google-services.json` + plugin): <https://firebase.google.com/docs/android/setup>
+5
ios/Runner.xcodeproj/project.pbxproj
··· 62 62 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 63 63 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 64 64 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 65 + 9D72E1A09E8B4F5D8D8B46A1 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; 65 66 AA7C2CE85592DCD41249030A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; 66 67 D8BAC14F436233DB60FD42F5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 67 68 E85E7FAF00CB12EDA75D52D5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; ··· 134 135 97C146FD1CF9000F007C117D /* Assets.xcassets */, 135 136 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 136 137 97C147021CF9000F007C117D /* Info.plist */, 138 + 9D72E1A09E8B4F5D8D8B46A1 /* Runner.entitlements */, 137 139 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 138 140 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 139 141 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, ··· 487 489 buildSettings = { 488 490 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 489 491 CLANG_ENABLE_MODULES = YES; 492 + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 490 493 CODE_SIGN_IDENTITY = "Apple Development"; 491 494 CODE_SIGN_STYLE = Automatic; 492 495 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ··· 674 677 buildSettings = { 675 678 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 676 679 CLANG_ENABLE_MODULES = YES; 680 + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 677 681 CODE_SIGN_IDENTITY = "Apple Development"; 678 682 CODE_SIGN_STYLE = Automatic; 679 683 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ··· 701 705 buildSettings = { 702 706 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 703 707 CLANG_ENABLE_MODULES = YES; 708 + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; 704 709 CODE_SIGN_IDENTITY = "Apple Development"; 705 710 CODE_SIGN_STYLE = Automatic; 706 711 CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+30
ios/Runner/GoogleService-Info.plist
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>API_KEY</key> 6 + <string>AIzaSyA3x2u6DzV_OqMpPPoDnq2ZmF7z0fjl4jk</string> 7 + <key>GCM_SENDER_ID</key> 8 + <string>251668382991</string> 9 + <key>PLIST_VERSION</key> 10 + <string>1</string> 11 + <key>BUNDLE_ID</key> 12 + <string>org.stormlightlabs.lazurite</string> 13 + <key>PROJECT_ID</key> 14 + <string>lazurite-push</string> 15 + <key>STORAGE_BUCKET</key> 16 + <string>lazurite-push.firebasestorage.app</string> 17 + <key>IS_ADS_ENABLED</key> 18 + <false></false> 19 + <key>IS_ANALYTICS_ENABLED</key> 20 + <false></false> 21 + <key>IS_APPINVITE_ENABLED</key> 22 + <true></true> 23 + <key>IS_GCM_ENABLED</key> 24 + <true></true> 25 + <key>IS_SIGNIN_ENABLED</key> 26 + <true></true> 27 + <key>GOOGLE_APP_ID</key> 28 + <string>1:251668382991:ios:327b0e0f48ac3e7366ea7a</string> 29 + </dict> 30 + </plist>
+1
ios/Runner/Info.plist
··· 71 71 <array> 72 72 <string>processing</string> 73 73 <string>fetch</string> 74 + <string>remote-notification</string> 74 75 </array> 75 76 <key>UILaunchStoryboardName</key> 76 77 <string>LaunchScreen</string>
+8
ios/Runner/Runner.entitlements
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>aps-environment</key> 6 + <string>development</string> 7 + </dict> 8 + </plist>
+1 -1
lib/core/scheduler/post_scheduler.dart
··· 253 253 254 254 /// Initialises WorkManager. Call once from main() before runApp(). 255 255 static Future<void> initialize() async { 256 - await Workmanager().initialize(callbackDispatcher, isInDebugMode: false); 256 + await Workmanager().initialize(callbackDispatcher); 257 257 } 258 258 259 259 /// Registers a one-off background task to fire at [scheduledAt].
+1 -1
lib/features/notifications/background/notification_background_worker.dart
··· 72 72 notificationReconcileUniqueName, 73 73 notificationReconcileTaskName, 74 74 frequency: const Duration(minutes: 15), 75 - existingWorkPolicy: ExistingWorkPolicy.keep, 75 + existingWorkPolicy: ExistingPeriodicWorkPolicy.keep, 76 76 constraints: Constraints(networkType: NetworkType.connected), 77 77 ); 78 78 return;
+8
lib/features/notifications/data/firebase_push_token_provider.dart
··· 30 30 _messaging ??= FirebaseMessaging.instance; 31 31 final messaging = _messaging!; 32 32 33 + final notificationSettings = await messaging.requestPermission( 34 + alert: true, 35 + badge: true, 36 + sound: true, 37 + provisional: false, 38 + ); 39 + log.i('Notification permission status: ${notificationSettings.authorizationStatus.name}'); 40 + 33 41 await messaging.setAutoInitEnabled(true); 34 42 35 43 _refreshSubscription = messaging.onTokenRefresh.listen(
+15 -1
lib/shared/presentation/widgets/global_tap_outside_unfocus.dart
··· 1 + import 'dart:ui' as ui; 2 + 1 3 import 'package:flutter/material.dart'; 2 4 3 5 /// Ensures tapping outside a focused [EditableText] dismisses keyboard focus ··· 12 14 actions: <Type, Action<Intent>>{ 13 15 EditableTextTapOutsideIntent: CallbackAction<EditableTextTapOutsideIntent>( 14 16 onInvoke: (intent) { 15 - intent.focusNode.unfocus(); 17 + // Preserve Flutter's default down-event behavior on touch so overlay 18 + // interactions (like typeahead suggestion taps) are not interrupted. 19 + if (intent.pointerDownEvent.kind != ui.PointerDeviceKind.touch) { 20 + intent.focusNode.unfocus(); 21 + } 22 + return null; 23 + }, 24 + ), 25 + EditableTextTapUpOutsideIntent: CallbackAction<EditableTextTapUpOutsideIntent>( 26 + onInvoke: (intent) { 27 + if (intent.pointerUpEvent.kind == ui.PointerDeviceKind.touch) { 28 + intent.focusNode.unfocus(); 29 + } 16 30 return null; 17 31 }, 18 32 ),
+26 -2
pubspec.lock
··· 1809 1809 dependency: "direct main" 1810 1810 description: 1811 1811 name: workmanager 1812 - sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00 1812 + sha256: "065673b2a465865183093806925419d311a9a5e0995aa74ccf8920fd695e2d10" 1813 1813 url: "https://pub.dev" 1814 1814 source: hosted 1815 - version: "0.5.2" 1815 + version: "0.9.0+3" 1816 + workmanager_android: 1817 + dependency: transitive 1818 + description: 1819 + name: workmanager_android 1820 + sha256: "9ae744db4ef891f5fcd2fb8671fccc712f4f96489a487a1411e0c8675e5e8cb7" 1821 + url: "https://pub.dev" 1822 + source: hosted 1823 + version: "0.9.0+2" 1824 + workmanager_apple: 1825 + dependency: transitive 1826 + description: 1827 + name: workmanager_apple 1828 + sha256: "1cc12ae3cbf5535e72f7ba4fde0c12dd11b757caf493a28e22d684052701f2ca" 1829 + url: "https://pub.dev" 1830 + source: hosted 1831 + version: "0.9.1+2" 1832 + workmanager_platform_interface: 1833 + dependency: transitive 1834 + description: 1835 + name: workmanager_platform_interface 1836 + sha256: f40422f10b970c67abb84230b44da22b075147637532ac501729256fcea10a47 1837 + url: "https://pub.dev" 1838 + source: hosted 1839 + version: "0.9.1+1" 1816 1840 xdg_directories: 1817 1841 dependency: transitive 1818 1842 description:
+1 -1
pubspec.yaml
··· 36 36 image_picker: ^1.1.2 37 37 cross_file: ^0.3.4+2 38 38 characters: ^1.4.0 39 - workmanager: ^0.5.2 39 + workmanager: ^0.9.0+3 40 40 plugin_platform_interface: ^2.1.8 41 41 url_launcher_platform_interface: ^2.3.2 42 42 connectivity_plus: ^7.0.0
+41
test/features/typeahead/presentation/typeahead_text_field_test.dart
··· 5 5 import 'package:lazurite/features/typeahead/data/typeahead_repository.dart'; 6 6 import 'package:lazurite/features/typeahead/data/typeahead_result.dart'; 7 7 import 'package:lazurite/features/typeahead/presentation/typeahead_text_field.dart'; 8 + import 'package:lazurite/shared/presentation/widgets/global_tap_outside_unfocus.dart'; 8 9 9 10 void main() { 10 11 group('TypeaheadTextField', () { ··· 53 54 expect(selected, isNotNull); 54 55 expect(selected!.did, 'did:plc:alice'); 55 56 expect(find.text('Alice'), findsNothing); 57 + }); 58 + 59 + testWidgets('tap result still selects when wrapped in GlobalTapOutsideUnfocus', (tester) async { 60 + final controller = TextEditingController(); 61 + TypeaheadResult? selected; 62 + 63 + final repository = _FakeTypeaheadRepository( 64 + searchHandler: ({required String query, int limit = 10}) async { 65 + return const [TypeaheadResult(did: 'did:plc:alice', handle: 'alice.bsky.social', displayName: 'Alice')]; 66 + }, 67 + ); 68 + 69 + await tester.pumpWidget( 70 + MaterialApp( 71 + home: GlobalTapOutsideUnfocus( 72 + child: Scaffold( 73 + body: Padding( 74 + padding: const EdgeInsets.all(16), 75 + child: TypeaheadTextField( 76 + controller: controller, 77 + repository: repository, 78 + onSelected: (result) => selected = result, 79 + debounceMs: 1, 80 + minChars: 2, 81 + ), 82 + ), 83 + ), 84 + ), 85 + ), 86 + ); 87 + 88 + await tester.enterText(find.byType(TextFormField), 'alice'); 89 + await tester.pump(const Duration(milliseconds: 20)); 90 + await tester.pumpAndSettle(); 91 + 92 + await tester.tap(find.text('Alice')); 93 + await tester.pumpAndSettle(); 94 + 95 + expect(selected, isNotNull); 96 + expect(selected!.did, 'did:plc:alice'); 56 97 }); 57 98 58 99 testWidgets('tap outside dismisses overlay', (tester) async {
+33
test/shared/presentation/widgets/global_tap_outside_unfocus_test.dart
··· 31 31 await tester.pumpAndSettle(); 32 32 expect(focusNode.hasFocus, isFalse); 33 33 }); 34 + 35 + testWidgets('touch down outside does not unfocus until touch up', (tester) async { 36 + final focusNode = FocusNode(debugLabel: 'global-focus-test-up'); 37 + addTearDown(focusNode.dispose); 38 + 39 + await tester.pumpWidget( 40 + MaterialApp( 41 + home: GlobalTapOutsideUnfocus( 42 + child: Scaffold( 43 + body: Column( 44 + children: [ 45 + TextField(focusNode: focusNode), 46 + const SizedBox(height: 200), 47 + const SizedBox(width: 120, height: 40, child: ColoredBox(color: Colors.red)), 48 + ], 49 + ), 50 + ), 51 + ), 52 + ), 53 + ); 54 + 55 + await tester.tap(find.byType(TextField)); 56 + await tester.pumpAndSettle(); 57 + expect(focusNode.hasFocus, isTrue); 58 + 59 + final gesture = await tester.startGesture(const Offset(20, 260)); 60 + await tester.pump(); 61 + expect(focusNode.hasFocus, isTrue); 62 + 63 + await gesture.up(); 64 + await tester.pumpAndSettle(); 65 + expect(focusNode.hasFocus, isFalse); 66 + }); 34 67 }