···11+---
22+title: Release Audit - Apple App Store + Google Play
33+updated: 2026-04-15
44+scope: Repository audit for likely store submission blockers or high-risk policy issues.
55+---
66+77+## Sources
88+99+- Apple App Review Guidelines: <https://developer.apple.com/app-store/review/guidelines/>
1010+ - 1.2 User-Generated Content requirements
1111+ - 5.1.1 Data Collection and Storage (Privacy Policies)
1212+ - 4.8 Login Services (third-party login exception for service clients)
1313+- Google Play User Data policy: <https://support.google.com/googleplay/android-developer/answer/10144311?hl=en>
1414+ - Privacy Policy requirement
1515+ - Account deletion requirement (if account creation is supported)
1616+- Google Play Developer Programme Policy (UGC section): <https://support.google.com/googleplay/android-developer/answer/16070163>
1717+ - UGC terms acceptance + moderation expectations
1818+1919+## Findings
2020+2121+### No in-app Privacy Policy link/text
2222+2323+- Policy mapping:
2424+ - Apple 5.1.1(i): privacy policy must be linked in App Store Connect and in-app in an easily accessible manner.
2525+ - Google Play User Data: privacy policy link/text must exist in Play Console and in-app.
2626+- Evidence in app:
2727+ - Login has no privacy/terms surface: `lib/features/auth/presentation/login_screen.dart` (see UI around lines 54-205).
2828+ - Settings has no legal/privacy entry: `lib/features/settings/presentation/settings_screen.dart` (lines 69-151).
2929+ - About page has external links and email only, no privacy policy link: `lib/features/settings/presentation/about_screen.dart` (lines 8-97).
3030+ - Existing backlog confirms missing policy: `docs/TODO.md` (lines 62-65).
3131+- Impact: High probability of rejection by both stores until fixed.
3232+3333+### High Risk (Google Play UGC): No explicit Terms/User Policy acceptance before posting UGC
3434+3535+- Policy mapping:
3636+ - Google Play UGC policy requires robust moderation, including requiring acceptance of Terms of Use and/or user policy before users create/upload UGC.
3737+- Evidence in app:
3838+ - 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).
3939+ - No in-app terms/user policy screen found in `lib/`.
4040+- Impact: Elevated Play policy risk for social/UGC apps.
4141+4242+### Moderate Risk (Apple UGC 1.2): Posting-side objectionable-content controls are not explicit
4343+4444+- Policy mapping:
4545+ - Apple 1.2 says UGC/social apps should include a method for filtering objectionable material from being posted.
4646+- Evidence:
4747+ - Reporting/blocking exists (good):
4848+ - `lib/features/profile/presentation/widgets/profile_action_buttons.dart` (Report/Block UI around lines 85-140).
4949+ - `lib/features/profile/presentation/widgets/report_dialog.dart` (report flow lines 10-220).
5050+ - Moderation controls exist for viewed content (good): `lib/features/settings/presentation/settings_screen.dart` (Moderation section lines 75-77).
5151+ - No explicit compose-time objectionable-content filter is visible in compose flow.
5252+- Impact: Could pass if platform-side moderation is accepted by review, but still a non-trivial risk without clear reviewer notes.
5353+5454+### Release Engineering Blocker
5555+5656+- Android release is configured to use debug signing:
5757+ - `android/app/build.gradle.kts` lines 33-38.
5858+- Android application ID is still placeholder:
5959+ - `android/app/build.gradle.kts` lines 23-25 (`com.example.lazurite`).
6060+- iOS bundle identifiers are still placeholder:
6161+ - `ios/Runner.xcodeproj/project.pbxproj` lines 498, 681, 704 (`com.example.lazurite`).
6262+- iOS release config currently shows developer signing identity:
6363+ - `ios/Runner.xcodeproj/project.pbxproj` line 642 (`iPhone Developer`).
6464+- Clarification:
6565+ - `org.stormlightlabs.lazurite.auth` appears in `Info.plist` URL type name (`CFBundleURLName`) and is not itself a placeholder bundle ID.
6666+- Impact: Submission can fail operationally or be blocked in release pipeline.
6767+6868+### Reviewer-access risk for App Store
6969+7070+- Apple "Before You Submit" requires full reviewer access (demo account or demo mode for account-based features).
7171+- App is account-based and login-gated; no repo evidence of dedicated reviewer/demo path.
7272+- Impact: Common review delay/rejection if review notes do not include working credentials.
7373+7474+## OK
7575+7676+- In-app report mechanism exists for posts/accounts:
7777+ - `lib/features/profile/presentation/widgets/report_dialog.dart`.
7878+- Block/mute actions exist:
7979+ - `lib/features/profile/presentation/widgets/profile_action_buttons.dart`.
8080+- User-reachable contact info exists:
8181+ - Email link in About: `lib/features/settings/presentation/about_screen.dart` line 11 + UI lines 90-93.
8282+- Sign in with Apple requirement appears likely exempt:
8383+ - App behaves as a client for a specific third-party service (Bluesky), matching Apple 4.8 exception language.
8484+8585+## Fixes (In Priority Order)
8686+8787+- [ ] Add a dedicated Legal screen and surface:
8888+ - [ ] Privacy Policy (in-app link + readable text summary)
8989+ - [ ] Terms of Use / User Policy
9090+ - [ ] Reachable from login and settings/about
9191+- [ ] Add UGC policy acceptance flow before first create/upload action (compose, media upload, messages if applicable).
9292+- [ ] Document moderation operations in policy/reviewer notes:
9393+ - [ ] How reports are handled and SLA
9494+ - [ ] What objectionable content rules apply
9595+- [ ] Replace placeholder identifiers and release signing setup:
9696+ - [ ] Android `applicationId`
9797+ - [ ] Android release signing config (non-debug)
9898+ - [ ] iOS bundle IDs + distribution signing
9999+- [ ] Prepare App Store review notes with working reviewer credentials/demo path.
100100+- [ ] Verify account deletion obligations:
101101+ - [ ] If any account creation is enabled in-app, add in-app deletion entry point per Apple/Google rules.
···11+import 'package:flutter/material.dart';
22+import 'package:flutter_svg/flutter_svg.dart';
33+import 'package:lazurite/features/settings/presentation/widgets/contact_section.dart';
44+import 'package:url_launcher/url_launcher.dart';
55+66+class PrivacyPolicyScreen extends StatelessWidget {
77+ const PrivacyPolicyScreen({super.key});
88+99+ static const _effectiveDate = 'April 15, 2026';
1010+ static const _websiteUrl = 'https://stormlightlabs.org';
1111+ static const _emailUrl = 'mailto:info@stormlightlabs.org';
1212+1313+ Future<void> _launch(String url) async {
1414+ await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
1515+ }
1616+1717+ @override
1818+ Widget build(BuildContext context) {
1919+ final theme = Theme.of(context);
2020+ final colorScheme = theme.colorScheme;
2121+2222+ return Scaffold(
2323+ appBar: AppBar(title: const Text('Lazurite\'s Privacy Policy')),
2424+ body: ListView(
2525+ padding: const EdgeInsets.all(24),
2626+ children: [
2727+ Center(
2828+ child: SvgPicture.asset(
2929+ 'assets/logo.svg',
3030+ width: 64,
3131+ height: 64,
3232+ colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn),
3333+ ),
3434+ ),
3535+ const SizedBox(height: 16),
3636+ Text(
3737+ 'Privacy Policy',
3838+ textAlign: TextAlign.center,
3939+ style: theme.textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w700),
4040+ ),
4141+ const SizedBox(height: 8),
4242+ Text(
4343+ 'Effective $_effectiveDate',
4444+ textAlign: TextAlign.center,
4545+ style: theme.textTheme.bodySmall?.copyWith(color: colorScheme.onSurfaceVariant),
4646+ ),
4747+ const SizedBox(height: 24),
4848+ Text(
4949+ 'Lazurite is a client for Bluesky. Core app behavior runs from your device, and we do not operate a '
5050+ 'developer backend for normal use.',
5151+ style: theme.textTheme.bodyLarge,
5252+ ),
5353+ const SizedBox(height: 20),
5454+ const _PolicySection(
5555+ title: 'What the app stores on your device',
5656+ paragraphs: [
5757+ 'Lazurite stores account session data, settings, cached content, and other app data locally so the app can work quickly and reliably.',
5858+ 'This local data can include profile metadata, viewed posts, follows, lists, likes, drafts, and media caches.',
5959+ ],
6060+ ),
6161+ const _PolicySection(
6262+ title: 'How your data is used',
6363+ paragraphs: [
6464+ 'Local data is used to keep you signed in, remember your preferences, improve loading performance, and support offline-friendly behavior.',
6565+ 'Lazurite does not sell your personal information.',
6666+ ],
6767+ ),
6868+ const _PolicySection(
6969+ title: 'Network requests and third parties',
7070+ paragraphs: [
7171+ 'When you use Lazurite, requests are sent directly from your device to Bluesky and related infrastructure.',
7272+ 'Your use of Bluesky remains subject to Bluesky policies, terms, and moderation systems.',
7373+ ],
7474+ ),
7575+ const _PolicySection(
7676+ title: 'Permissions',
7777+ paragraphs: [
7878+ 'If you choose to save media, Lazurite requests photo or storage permissions required by your platform.',
7979+ 'Permissions are used only for the feature you invoke.',
8080+ ],
8181+ ),
8282+ const _PolicySection(
8383+ title: 'Diagnostics and logs',
8484+ paragraphs: [
8585+ 'Lazurite keeps local app logs to help troubleshoot issues. These logs stay on your device unless you choose to share them.',
8686+ 'Lazurite does not include ad tracking SDKs.',
8787+ ],
8888+ ),
8989+ const _PolicySection(
9090+ title: 'Data retention and control',
9191+ paragraphs: [
9292+ 'Data remains on your device until you remove it by signing out, clearing app storage, or uninstalling the app.',
9393+ 'Because Lazurite does not run a central app backend for normal use, most data-control actions happen on your device or through Bluesky account settings.',
9494+ ],
9595+ ),
9696+ const _PolicySection(
9797+ title: 'Children',
9898+ paragraphs: [
9999+ 'Lazurite is not directed to children under 13, or under the minimum age required in your jurisdiction.',
100100+ ],
101101+ ),
102102+ const _PolicySection(
103103+ title: 'Policy updates',
104104+ paragraphs: [
105105+ 'We may revise this policy from time to time. Material updates will be reflected by a new effective date and app release notes when appropriate.',
106106+ ],
107107+ ),
108108+ ContactSection(onStormlightLabsTap: () => _launch(_websiteUrl), onEmailTap: () => _launch(_emailUrl)),
109109+ const SizedBox(height: 12),
110110+ Center(child: Text('Lazurite v1.0.0', style: theme.textTheme.bodySmall)),
111111+ ],
112112+ ),
113113+ );
114114+ }
115115+}
116116+117117+class _PolicySection extends StatelessWidget {
118118+ const _PolicySection({required this.title, required this.paragraphs});
119119+120120+ final String title;
121121+ final List<String> paragraphs;
122122+123123+ @override
124124+ Widget build(BuildContext context) {
125125+ final theme = Theme.of(context);
126126+ final colorScheme = theme.colorScheme;
127127+128128+ return Padding(
129129+ padding: const EdgeInsets.only(bottom: 18),
130130+ child: Column(
131131+ crossAxisAlignment: CrossAxisAlignment.start,
132132+ children: [
133133+ Text(
134134+ title,
135135+ style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700, color: colorScheme.primary),
136136+ ),
137137+ const SizedBox(height: 6),
138138+ for (final paragraph in paragraphs)
139139+ Padding(
140140+ padding: const EdgeInsets.only(bottom: 8),
141141+ child: Text(paragraph, style: theme.textTheme.bodyMedium),
142142+ ),
143143+ ],
144144+ ),
145145+ );
146146+ }
147147+}