From 54a1fd0ffbd9b663bd9aa502009ea54faae3b304 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Thu, 11 Jun 2026 10:42:39 +0200
Subject: [PATCH] build: store-release pipeline (App Store + Play Store), Android release signing
---
.gitignore | 4
tools/release.sh | 59 +++++++++++
android/app/build.gradle.kts | 21 +++-
tools/build-android-aab.sh | 22 ++++
tools/build-appstore.sh | 82 ++++++++++++++++
tools/privacy.html | 95 +++++++++++++++++++
6 files changed, 277 insertions(+), 6 deletions(-)
diff --git a/.gitignore b/.gitignore
index 3820a95..36777cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,7 @@
/android/app/debug
/android/app/profile
/android/app/release
+
+# Release signing — never commit the keystore.
+android/release.keystore
+android/key.properties
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index b7d0fa7..d714eb0 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -20,21 +20,30 @@
}
defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.tekmidian.pailot"
- // You can update the following values to match your application needs.
- // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
+ // Release signing — keystore lives at android/release.keystore (gitignored).
+ // Back this file up: losing it means losing the ability to ship updates to
+ // the Play Store under this package name.
+ signingConfigs {
+ create("release") {
+ storeFile = file("../release.keystore")
+ storePassword = "pailot-release-key-2026"
+ keyAlias = "pailot"
+ keyPassword = "pailot-release-key-2026"
+ }
+ }
+
buildTypes {
release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig = signingConfigs.getByName("debug")
+ signingConfig = signingConfigs.getByName("release")
+ isMinifyEnabled = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
}
}
diff --git a/tools/build-android-aab.sh b/tools/build-android-aab.sh
new file mode 100755
index 0000000..7c61328
--- /dev/null
+++ b/tools/build-android-aab.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# PAILot Android AAB Build Script
+# Produces a Play Store-ready signed App Bundle.
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$APP_DIR"
+
+# Pin JDK 21 — system openjdk 25 is not yet parseable by the Kotlin compiler
+# bundled with AGP. JDK 17 also works if you prefer; just point JAVA_HOME at it.
+JDK="${JAVA_HOME_RELEASE:-/opt/homebrew/Cellar/openjdk@21/21.0.11/libexec/openjdk.jdk/Contents/Home}"
+
+echo "=== Building Android App Bundle (AAB) ==="
+JAVA_HOME="$JDK" flutter build appbundle --release --no-tree-shake-icons
+
+echo ""
+echo "=== Done ==="
+AAB=build/app/outputs/bundle/release/app-release.aab
+ls -la "$AAB"
+echo ""
+echo "Upload $AAB to the Play Console (Production → Create new release)."
diff --git a/tools/build-appstore.sh b/tools/build-appstore.sh
new file mode 100755
index 0000000..e2d4dfb
--- /dev/null
+++ b/tools/build-appstore.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+# PAILot App Store Build Script
+# Builds an iOS IPA and uploads it to App Store Connect.
+# Adapted from Glidr's pipeline — same Apple Dev Team (7KU642K5ZL).
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$APP_DIR"
+
+# App Store Connect API credentials (issuer is shared across keys for this team).
+ASC_KEY_ID="${ASC_KEY_ID:-4H68LK2M3J}"
+ASC_ISSUER_ID="${ASC_ISSUER_ID:-69a6de75-2050-47e3-e053-5b8c7c11a4d1}"
+ASC_KEY_PATH="${ASC_KEY_PATH:-$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8}"
+TEAM_ID="7KU642K5ZL"
+
+if [ ! -f "$ASC_KEY_PATH" ]; then
+ echo "ERROR: App Store Connect API key not found at $ASC_KEY_PATH"
+ echo " Download from https://appstoreconnect.apple.com/access/api"
+ exit 1
+fi
+
+# Homebrew rsync breaks Xcode's export step — temporarily hide it.
+BREW_RSYNC="$(brew --prefix 2>/dev/null)/bin/rsync"
+RSYNC_MOVED=false
+if [ -f "$BREW_RSYNC" ]; then
+ mv "$BREW_RSYNC" "${BREW_RSYNC}.bak"
+ RSYNC_MOVED=true
+ echo " Temporarily hid Homebrew rsync"
+fi
+cleanup() {
+ if [ "$RSYNC_MOVED" = true ] && [ -f "${BREW_RSYNC}.bak" ]; then
+ mv "${BREW_RSYNC}.bak" "$BREW_RSYNC"
+ echo " Restored Homebrew rsync"
+ fi
+}
+trap cleanup EXIT
+
+echo "=== Building IPA for App Store ==="
+flutter build ipa --release --export-method development --no-tree-shake-icons
+
+echo ""
+echo "=== Fixing archive name ==="
+ARCHIVE="build/ios/archive/Runner.xcarchive/Info.plist"
+if [ -f "$ARCHIVE" ]; then
+ plutil -replace Name -string "PAILot" "$ARCHIVE"
+ plutil -replace SchemeName -string "PAILot" "$ARCHIVE"
+ echo " Archive name set to 'PAILot'"
+fi
+
+echo ""
+echo "=== Exporting and uploading to App Store Connect ==="
+cat > /tmp/PAILotAppStoreExport.plist <<PLIST
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>method</key>
+ <string>app-store-connect</string>
+ <key>destination</key>
+ <string>upload</string>
+ <key>signingStyle</key>
+ <string>automatic</string>
+ <key>teamID</key>
+ <string>${TEAM_ID}</string>
+</dict>
+</plist>
+PLIST
+
+rm -rf /tmp/pailot-appstore-export 2>/dev/null
+xcodebuild -exportArchive \
+ -archivePath build/ios/archive/Runner.xcarchive \
+ -exportOptionsPlist /tmp/PAILotAppStoreExport.plist \
+ -exportPath /tmp/pailot-appstore-export \
+ -allowProvisioningUpdates \
+ -authenticationKeyPath "$ASC_KEY_PATH" \
+ -authenticationKeyID "$ASC_KEY_ID" \
+ -authenticationKeyIssuerID "$ASC_ISSUER_ID"
+
+echo ""
+echo "=== Done ==="
+echo "Uploaded to App Store Connect. Next: complete the build's metadata and submit for review."
diff --git a/tools/privacy.html b/tools/privacy.html
new file mode 100644
index 0000000..46cc4a5
--- /dev/null
+++ b/tools/privacy.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="Privacy Policy for the PAILot voice-first AI communicator app">
+ <title>Privacy Policy - PAILot</title>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f9f9f9; }
+ .container { max-width: 900px; margin: 0 auto; padding: 20px; background-color: #ffffff; }
+ header { border-bottom: 2px solid #007AFF; padding-bottom: 20px; margin-bottom: 30px; }
+ h1 { font-size: 2.5rem; color: #007AFF; margin-bottom: 10px; }
+ .subtitle { color: #666; font-size: 1rem; }
+ .last-updated { color: #999; font-size: 0.9rem; margin-top: 10px; }
+ h2 { font-size: 1.5rem; color: #007AFF; margin-top: 35px; margin-bottom: 15px; }
+ h3 { font-size: 1.1rem; color: #333; margin-top: 20px; margin-bottom: 10px; }
+ p { margin-bottom: 15px; text-align: justify; }
+ ul { margin-left: 30px; margin-bottom: 15px; }
+ li { margin-bottom: 8px; }
+ .highlight { background-color: #f0f8ff; border-left: 4px solid #007AFF; padding: 15px; margin: 20px 0; }
+ footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 0.9rem; text-align: center; }
+ @media (max-width: 600px) { .container { padding: 15px; } h1 { font-size: 2rem; } h2 { font-size: 1.3rem; } }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <header>
+ <h1>Privacy Policy</h1>
+ <p class="subtitle">PAILot — voice-first AI communicator</p>
+ <p class="last-updated">Last updated: June 2026</p>
+ </header>
+
+ <h2>Introduction</h2>
+ <p>PAILot is a client application that connects to a personal AI infrastructure server (AIBroker) that you operate. PAILot is intentionally designed so that we, TEKmidian, do not host, store, or process your data on our infrastructure. The app's purpose is to relay your voice and text messages between you and the Claude Code sessions running on your own machine.</p>
+
+ <div class="highlight">
+ <strong>Plain English:</strong> PAILot talks to a server you run yourself. We don't see your messages, your voice, or your conversations. There is no PAILot cloud.
+ </div>
+
+ <h2>What PAILot Does Not Do</h2>
+ <ul>
+ <li>We do not operate a backend. The app connects only to the AIBroker daemon you have configured.</li>
+ <li>We do not collect or store any of your messages, voice recordings, transcripts, or session data.</li>
+ <li>We do not use third-party analytics SDKs (no Firebase Analytics, no Crashlytics by default, no advertising IDs).</li>
+ <li>We do not track you across apps or websites.</li>
+ <li>We do not sell or share any data with third parties.</li>
+ </ul>
+
+ <h2>What the App Processes Locally and Sends to Your Server</h2>
+
+ <h3>Voice recordings</h3>
+ <p>When you record a voice message, the audio is captured locally on your device and sent over an encrypted MQTT connection to <em>your own</em> AIBroker daemon. Transcription happens on that machine. Neither the audio nor the transcript leaves your infrastructure.</p>
+
+ <h3>Text messages and screenshots</h3>
+ <p>Text messages and screenshots you send are transmitted directly to your AIBroker daemon. They are not routed through any TEKmidian server.</p>
+
+ <h3>Push notification tokens</h3>
+ <p>To deliver notifications when the app is in the background, your device's APNs token (iOS) or FCM token (Android) is sent to your AIBroker daemon, which uses it to push notifications via Apple or Google. The token never reaches TEKmidian.</p>
+
+ <h3>Local storage on your device</h3>
+ <p>The app stores conversation history, session names, unread counts, and configuration locally on the device using the operating system's standard private app sandbox. This data is removed when you uninstall the app.</p>
+
+ <h2>Permissions the App Requests</h2>
+ <ul>
+ <li><strong>Microphone</strong> — required to record voice messages. Used only when you actively initiate a recording.</li>
+ <li><strong>Notifications</strong> — required to alert you to incoming messages.</li>
+ <li><strong>Camera (optional)</strong> — used only when you explicitly attach a photo.</li>
+ <li><strong>Network</strong> — required to communicate with your AIBroker server.</li>
+ </ul>
+
+ <h2>Third-Party Services Embedded in the App</h2>
+ <p>The app relies on the operating system's push notification systems (Apple Push Notification service and Firebase Cloud Messaging) <em>solely</em> as a delivery channel. These platforms have their own privacy policies governing notification delivery, which apply when your AIBroker server sends a push through them.</p>
+
+ <h2>Data Retention</h2>
+ <p>We retain none of your data because we never receive any. Anything stored on your device is governed by your device's privacy and backup settings. Data on your AIBroker server is governed by your own decisions about how you run that server.</p>
+
+ <h2>Children's Privacy</h2>
+ <p>PAILot is not directed at children under 13. We do not knowingly collect information from children.</p>
+
+ <h2>Changes to This Privacy Policy</h2>
+ <p>If this policy materially changes, the updated version will be available at the same URL and announced in the app's release notes. The "Last updated" date above will reflect the most recent change.</p>
+
+ <h2>Your Rights</h2>
+ <p>Because we do not process your personal data, GDPR/CCPA "access, correction, deletion" requests do not produce anything on our side. To remove all locally stored data, uninstall the app. To remove data held on your AIBroker server, manage it directly on the machine where that server runs.</p>
+
+ <h2>Contact Us</h2>
+ <p>If you have questions about this policy, contact us at <a href="mailto:support@tekmidian.com">support@tekmidian.com</a>.</p>
+
+ <footer>
+ <p>© 2026 TEKmidian. All rights reserved.</p>
+ </footer>
+ </div>
+</body>
+</html>
diff --git a/tools/release.sh b/tools/release.sh
new file mode 100755
index 0000000..3c7125d
--- /dev/null
+++ b/tools/release.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+# PAILot full-store release pipeline.
+# Builds iOS IPA, uploads to App Store Connect, then builds Android AAB.
+#
+# Prereqs:
+# - Apple Dev Team 7KU642K5ZL provisioning set up (same as Glidr).
+# - App Store Connect API key at $HOME/.appstoreconnect/private_keys/AuthKey_*.p8
+# - android/release.keystore present (generated once; back this up!).
+#
+# Usage:
+# ./tools/release.sh # iOS + Android
+# ./tools/release.sh --ios-only # skip Android
+# ./tools/release.sh --android-only # skip iOS
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+cd "$APP_DIR"
+
+IOS=true
+ANDROID=true
+for arg in "$@"; do
+ case "$arg" in
+ --ios-only) ANDROID=false ;;
+ --android-only) IOS=false ;;
+ esac
+done
+
+# Kill any competing flutter/dart processes (other sessions hold locks).
+pkill -f "dartvm.*flutter_tools" 2>/dev/null || true
+sleep 1
+
+APP_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //')
+echo "========================================="
+echo " PAILot store release v${APP_VERSION}"
+echo "========================================="
+echo ""
+
+if $IOS; then
+ echo "[iOS] Building IPA and uploading to App Store Connect..."
+ bash tools/build-appstore.sh
+ echo ""
+fi
+
+if $ANDROID; then
+ echo "[Android] Building AAB..."
+ bash tools/build-android-aab.sh
+ echo ""
+fi
+
+echo "========================================="
+echo " RELEASE COMPLETE"
+echo "========================================="
+echo ""
+$IOS && echo "iOS: Uploaded to App Store Connect."
+$ANDROID && echo "Android: build/app/outputs/bundle/release/app-release.aab"
+$ANDROID && echo " Upload to Google Play Console manually."
+echo ""
+echo "Next: bump the build number on each store and submit for review."
--
Gitblit v1.3.1