1234
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Firebase Setup Process Documentation</title> <link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css" /> <style> :root { --bg-primary: #181818; --bg-secondary: #202020; --bg-tertiary: #2a2a2a; --bg-controls: #0f0f0f; --text-primary: #ffffff; --text-secondary: #D1D1D1; --text-muted: #4E4E4E; --border: #2a2a2a; --accent: #1E43FF; --accent-dark: #1534c4; --code-bg: #2a2a2a; --card-row-alt: #1f1f1f; --cover-bg: #181818; --cover-text: #ffffff; --cover-subtitle: #D1D1D1; } [data-theme="light"] { --bg-primary: #ffffff; --bg-secondary: #F7F8FA; --bg-tertiary: #EFF2F9; --bg-controls: #F7F8FA; --text-primary: #181818; --text-secondary: #4E4E4E; --text-muted: #D1D1D1; --border: #E2E5EC; --accent: #1E43FF; --accent-dark: #1534c4; --code-bg: #EFF2F9; --card-row-alt: #F7F8FA; --cover-bg: #181818; --cover-text: #ffffff; --cover-subtitle: #D1D1D1; } * { box-sizing: border-box; margin: 0; padding: 0; } html, body { width: 100vw; height: 100vh; overflow: hidden; background: var(--bg-primary); color: var(--text-primary); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-weight: 400; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; transition: background 250ms ease, color 250ms ease; } .deck { position: relative; width: 100vw; height: 100vh; overflow: hidden; } .slide { position: absolute; top: 0; left: 0; width: 100vw; height: calc(100vh - 48px); padding: 40px 60px; opacity: 0; transform: translateX(100%); transition: transform 350ms ease, opacity 350ms ease, background 250ms ease, color 250ms ease; display: flex; flex-direction: column; overflow: hidden; background: var(--bg-primary); color: var(--text-primary); } .slide.active { opacity: 1; transform: translateX(0); z-index: 2; } .slide.prev { transform: translateX(-100%); opacity: 0; } .slide.cover { justify-content: center; align-items: center; text-align: center; padding: 60px; background: var(--cover-bg); color: var(--cover-text); position: absolute; overflow: hidden; } .slide.cover::after { content: ""; position: absolute; right: -10%; top: 20%; width: 55%; height: 70%; background: var(--accent); border-radius: 50%; filter: blur(140px); opacity: 0.55; z-index: 0; } .slide.cover > * { position: relative; z-index: 1; } .cover h1 { font-size: 72px; font-weight: 700; line-height: 1.05; letter-spacing: -1.5px; margin-bottom: 24px; max-width: 1300px; color: var(--cover-text); } .cover .subtitle { font-size: 28px; font-weight: 400; color: var(--cover-subtitle); max-width: 1100px; margin-bottom: 48px; line-height: 1.4; } .cover .meta { font-size: 20px; color: var(--cover-subtitle); letter-spacing: 0.5px; } .cover .meta strong { color: var(--cover-text); font-weight: 500; } .slide-eyebrow { font-size: 18px; font-weight: 500; letter-spacing: 2px; text-transform: uppercase; color: var(--accent); margin-top: 24px; margin-bottom: 12px; } .slide-title { font-size: 48px; font-weight: 700; line-height: 1.1; letter-spacing: -0.8px; margin-bottom: 28px; max-width: 1400px; color: var(--text-primary); } .slide-subtitle { font-size: 22px; color: var(--text-secondary); margin-bottom: 32px; line-height: 1.5; max-width: 1300px; } .slide.section-divider { background: var(--accent); color: #ffffff; justify-content: center; align-items: flex-start; padding: 80px 100px; } .section-divider .part-label { font-size: 22px; font-weight: 500; letter-spacing: 3px; text-transform: uppercase; color: rgba(255,255,255,0.7); margin-bottom: 20px; } .section-divider h2 { font-size: 72px; font-weight: 700; color: #ffffff; line-height: 1.05; letter-spacing: -1.5px; max-width: 1400px; } .section-divider .section-sub { font-size: 26px; color: rgba(255,255,255,0.9); margin-top: 24px; max-width: 1200px; line-height: 1.4; } .content-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; } ul.bullets { list-style: none; margin: 0; padding: 0; } ul.bullets li { display: flex; align-items: flex-start; gap: 16px; font-size: 22px; line-height: 1.45; margin-bottom: 16px; color: var(--text-primary); } ul.bullets li .ph { color: var(--accent); font-size: 26px; flex-shrink: 0; margin-top: 2px; } ul.bullets li .num { color: var(--accent); font-weight: 700; font-size: 22px; flex-shrink: 0; min-width: 26px; margin-top: 1px; } ul.bullets li strong { color: var(--text-primary); font-weight: 600; } ul.bullets li code, p code { font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; font-size: 19px; background: var(--code-bg); color: var(--accent); padding: 2px 8px; border-radius: 4px; } ul.bullets ul { list-style: none; margin: 8px 0 0 0; padding: 0; } ul.bullets ul li { font-size: 19px; color: var(--text-secondary); margin-bottom: 6px; } ul.bullets ul li::before { content: "-"; color: var(--text-muted); margin-right: 10px; } .two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; flex: 1; overflow: hidden; } .col h3 { font-size: 22px; font-weight: 600; color: var(--accent); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; } .card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } .card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 12px; padding: 24px; transition: background 250ms ease, border 250ms ease; } .card .card-icon { font-size: 36px; color: var(--accent); margin-bottom: 16px; } .card h3 { font-size: 22px; font-weight: 600; margin-bottom: 10px; color: var(--text-primary); } .card p { font-size: 17px; color: var(--text-secondary); line-height: 1.5; } .flow { display: flex; flex-direction: column; gap: 14px; counter-reset: step; } .flow .step { display: flex; align-items: flex-start; gap: 20px; background: var(--bg-secondary); border: 1px solid var(--border); border-left: 4px solid var(--accent); padding: 16px 22px; border-radius: 8px; transition: background 250ms ease, border 250ms ease; } .flow .step .step-num { counter-increment: step; color: var(--accent); font-weight: 700; font-size: 22px; min-width: 28px; } .flow .step .step-num::before { content: counter(step, decimal-leading-zero); } .flow .step .step-body { font-size: 19px; line-height: 1.45; color: var(--text-primary); } .flow .step .step-body strong { color: var(--accent); font-weight: 600; } .flow .step .step-body code { font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; font-size: 16px; background: var(--code-bg); color: var(--text-primary); padding: 2px 6px; border-radius: 4px; } table.ref-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 16px; } table.ref-table th { background: var(--accent); color: #ffffff; text-align: left; padding: 12px 16px; font-weight: 600; font-size: 16px; } table.ref-table th:first-child { border-top-left-radius: 8px; } table.ref-table th:last-child { border-top-right-radius: 8px; } table.ref-table td { padding: 10px 16px; border-bottom: 1px solid var(--border); color: var(--text-primary); vertical-align: top; font-size: 15px; } table.ref-table tr:nth-child(even) td { background: var(--card-row-alt); } table.ref-table tr:nth-child(odd) td { background: var(--bg-primary); } table.ref-table td code { font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; font-size: 14px; color: var(--text-secondary); background: transparent; padding: 0; } .key-callout { background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%); border-radius: 12px; padding: 24px 32px; margin-top: 24px; color: #ffffff; } .key-callout .callout-label { font-size: 14px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: rgba(255,255,255,0.7); margin-bottom: 8px; } .key-callout .callout-body { font-size: 22px; font-weight: 500; color: #ffffff; line-height: 1.4; } .key-callout code { font-family: 'SF Mono', 'Monaco', 'Menlo', monospace; background: rgba(0,0,0,0.25); padding: 2px 8px; border-radius: 4px; font-size: 19px; } .controls-bar { position: fixed; bottom: 0; left: 0; width: 100vw; height: 48px; background: var(--bg-controls); border-top: 1px solid var(--border); display: flex; align-items: center; padding: 0 24px; gap: 16px; z-index: 100; transition: background 250ms ease, border 250ms ease; } .progress-bar { flex: 1; height: 6px; background: var(--bg-tertiary); border-radius: 3px; overflow: hidden; } .progress-fill { height: 100%; background: var(--accent); transition: width 350ms ease; width: 0%; } .slide-counter { color: var(--text-primary); font-size: 18px; letter-spacing: 0.5px; min-width: 70px; text-align: right; } .nav-btn { background: transparent; border: none; cursor: pointer; color: var(--text-primary); font-size: 28px; padding: 4px 8px; transition: color 150ms ease; } .nav-btn:hover { color: var(--accent); } .nav-btn:disabled { color: var(--text-muted); cursor: not-allowed; } .theme-toggle { background: transparent; border: 1px solid var(--border); border-radius: 20px; cursor: pointer; color: var(--text-primary); font-size: 18px; padding: 4px 12px; display: flex; align-items: center; gap: 6px; transition: color 150ms ease, border 150ms ease, background 150ms ease; } .theme-toggle:hover { border-color: var(--accent); color: var(--accent); } .theme-toggle .ph { font-size: 20px; } .theme-toggle .theme-label { font-size: 13px; font-weight: 500; letter-spacing: 0.5px; text-transform: uppercase; } [data-theme="light"] .theme-toggle .icon-moon { display: inline-block; } [data-theme="light"] .theme-toggle .icon-sun { display: none; } [data-theme="light"] .theme-toggle .label-light { display: inline; } [data-theme="light"] .theme-toggle .label-dark { display: none; } .theme-toggle .icon-sun { display: inline-block; } .theme-toggle .icon-moon { display: none; } .theme-toggle .label-dark { display: inline; } .theme-toggle .label-light { display: none; } @page { size: A4 landscape; margin: 0; } @media print { html, body { width: 297mm; height: 210mm; overflow: visible; } .controls-bar { display: none !important; } .slide { position: relative !important; opacity: 1 !important; transform: none !important; page-break-after: always; width: 297mm; height: 210mm; overflow: hidden; } .slide.cover::after { -webkit-print-color-adjust: exact; print-color-adjust: exact; } } </style> </head> <body> <div class="deck" id="deck"> <section class="slide cover active"> <h1>Firebase Setup<br/>Process Documentation</h1> <div class="subtitle">A step-by-step guide to connecting Firebase, setting up Firebase Functions, and configuring build flavors for a Flutter mobile app.</div> <div class="meta">Mobile App Project Template &middot; Reference Document</div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Overview</div> <h2 class="slide-title">What this guide covers</h2> <div class="slide-subtitle">Seven parts that together document the full process of wiring a mobile app to Firebase with per-environment isolation.</div> <div class="card-grid" style="grid-template-columns: repeat(4, 1fr);"> <div class="card"><i class="ph ph-cloud card-icon"></i><h3>Part 1</h3><p>Create Firebase projects, register apps, download config files, enable services.</p></div> <div class="card"><i class="ph ph-flower-lotus card-icon"></i><h3>Part 2</h3><p>Flutter flavor structure: entry points, config interface, config classes, initialisation.</p></div> <div class="card"><i class="ph ph-android-logo card-icon"></i><h3>Part 3</h3><p>Android flavor setup: Gradle product flavors, Firebase BOM, google-services placement.</p></div> <div class="card"><i class="ph ph-apple-logo card-icon"></i><h3>Part 4</h3><p>iOS flavor setup: Xcode configurations, Podfile, plist copy build phase.</p></div> <div class="card"><i class="ph ph-function card-icon"></i><h3>Part 5</h3><p>Firebase Functions: CLI init, project aliases, module structure, environment variables.</p></div> <div class="card"><i class="ph ph-tree-structure card-icon"></i><h3>Part 6</h3><p>End-to-end build flow for Android, iOS, and Functions deployment.</p></div> <div class="card"><i class="ph ph-table card-icon"></i><h3>Part 7</h3><p>Cross-environment reference table for all config values and file paths.</p></div> <div class="card"><i class="ph ph-check-circle card-icon"></i><h3>Purpose</h3><p>A template-ready guide for setting up Firebase in any new mobile app project.</p></div> </div> </div> </section> <section class="slide section-divider"><div class="part-label">Part 1</div><h2>Create Firebase Projects</h2><div class="section-sub">Separate Firebase projects per environment keep data, auth, storage, and functions fully isolated between production, staging, and UAT.</div></section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 1 &middot; Firebase Console</div> <h2 class="slide-title">Project setup steps</h2> <div class="flow"> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Create three Firebase projects</strong> in the Firebase Console: <code>{project-name}</code> (production), <code>{project-name}-stg</code> (staging), <code>{project-name}-uat</code> (UAT).</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Register an Android app</strong> in each Firebase project with the environment-specific package name (e.g. <code>com.company.app</code>, <code>com.company.app.stg</code>, <code>com.company.app.uat</code>).</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Register an iOS app</strong> in each Firebase project with the environment-specific bundle ID (e.g. <code>com.company.ios.app</code>, <code>com.company.ios.app.stg</code>, <code>com.company.ios.app.uat</code>).</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Download the config files</strong> from each project: <code>google-services.json</code> for Android and <code>GoogleService-Info.plist</code> for iOS. Six files in total across the three projects.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Enable required Firebase services</strong> in each project: Authentication, Cloud Firestore, Cloud Storage, Cloud Functions (requires Blaze plan), Cloud Messaging, and Firebase Hosting if web pages are needed.</div></div> </div> </div> </section> <section class="slide section-divider"><div class="part-label">Part 2</div><h2>Flutter Project Structure for Flavors</h2><div class="section-sub">Three layers - an abstract config interface, environment-specific config classes, and flavor entry points - ensure each build variant connects to the correct Firebase project at runtime.</div></section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 2.1 &amp; 2.2 &middot; Flutter entry points and config interface</div> <h2 class="slide-title">Entry points + AppConfig interface</h2> <div class="two-col"> <div class="col"> <h3>2.1 Flavor Entry Points</h3> <p style="font-size:19px;color:var(--text-secondary);margin-bottom:18px;line-height:1.5;">Create one <code>main_*.dart</code> per environment. Each instantiates its config and passes it to a shared initialisation function.</p> <ul class="bullets"> <li><i class="ph ph-file-dart"></i><span><code>lib/main_production.dart</code> creates <code>ProductionConfig()</code> and calls <code>mainCommon()</code></span></li> <li><i class="ph ph-file-dart"></i><span><code>lib/main_staging.dart</code> creates <code>StagingConfig()</code> and calls <code>mainCommon()</code></span></li> <li><i class="ph ph-file-dart"></i><span><code>lib/main_uat.dart</code> creates <code>UatConfig()</code> and calls <code>mainCommon()</code></span></li> </ul> </div> <div class="col"> <h3>2.2 The AppConfig Interface</h3> <p style="font-size:19px;color:var(--text-secondary);margin-bottom:18px;line-height:1.5;">Create <code>lib/core/config/app_config.dart</code> as the abstract contract every flavor config must implement.</p> <ul class="bullets"> <li><i class="ph ph-dot"></i><span><code>environmentName</code> - production, staging, uat</span></li> <li><i class="ph ph-dot"></i><span><code>firebaseOptions</code> - platform-specific Firebase credentials</span></li> <li><i class="ph ph-dot"></i><span><code>projectId</code> - the Firebase project ID</span></li> <li><i class="ph ph-dot"></i><span>Environment-specific values (RevenueCat keys, LogRocket app ID, etc.)</span></li> <li><i class="ph ph-globe"></i><span>A <code>static late AppConfig instance</code> acts as a global singleton, set once at startup.</span></li> </ul> </div> </div> </div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 2.3 &amp; 2.4 &middot; Config classes and shared initialisation</div> <h2 class="slide-title">Config classes + shared startup</h2> <div class="two-col"> <div class="col"> <h3>2.3 Flavor Config Classes</h3> <p style="font-size:19px;color:var(--text-secondary);margin-bottom:18px;line-height:1.5;">Create one config class per environment under <code>lib/core/config/</code>.</p> <ul class="bullets"> <li><i class="ph ph-file-code"></i><span><code>production_config.dart</code></span></li> <li><i class="ph ph-file-code"></i><span><code>staging_config.dart</code></span></li> <li><i class="ph ph-file-code"></i><span><code>uat_config.dart</code></span></li> </ul> <p style="font-size:18px;color:var(--text-secondary);margin-top:16px;line-height:1.5;">Each class implements <code>AppConfig</code> and returns platform-specific <code>FirebaseOptions</code> using <code>defaultTargetPlatform</code> to detect Android vs. iOS.</p> <p style="font-size:18px;color:var(--text-secondary);margin-top:12px;line-height:1.5;">Values come from the <code>google-services.json</code> and <code>GoogleService-Info.plist</code> files downloaded in Part 1.</p> </div> <div class="col"> <h3>2.4 Shared Initialisation</h3> <p style="font-size:19px;color:var(--text-secondary);margin-bottom:14px;line-height:1.5;">Create <code>lib/main_common.dart</code> with the shared startup logic all entry points call. Order:</p> <ul class="bullets"> <li><span class="num">1</span><span>Set <code>AppConfig.instance</code> to the passed config</span></li> <li><span class="num">2</span><span>Initialise LogRocket for crash reporting (if configured)</span></li> <li><span class="num">3</span><span>Call <code>WidgetsFlutterBinding.ensureInitialized()</code></span></li> <li><span class="num">4</span><span>Initialise RevenueCat with platform API keys</span></li> <li><span class="num">5</span><span>Initialise EasyLocalization</span></li> <li><span class="num">6</span><span><strong>Call <code>Firebase.initializeApp(options: config.firebaseOptions)</code></strong></span></li> <li><span class="num">7</span><span>Configure system UI and launch the app</span></li> </ul> </div> </div> </div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 2.5 &amp; 2.6 &middot; pubspec.yaml and generated files</div> <h2 class="slide-title">Firebase dependencies</h2> <div class="slide-subtitle">Add the required Firebase packages to <code>pubspec.yaml</code>. Each package maps to a specific Firebase service used by the app.</div> <div class="card-grid"> <div class="card"><i class="ph ph-fire card-icon"></i><h3>firebase_core</h3><p>Core Firebase SDK - required for all other Firebase packages to work.</p></div> <div class="card"><i class="ph ph-user-circle card-icon"></i><h3>firebase_auth</h3><p>Email/Password, Facebook, Apple, and other authentication providers.</p></div> <div class="card"><i class="ph ph-cloud-arrow-up card-icon"></i><h3>firebase_storage</h3><p>File and media uploads to Cloud Storage buckets.</p></div> <div class="card"><i class="ph ph-database card-icon"></i><h3>cloud_firestore</h3><p>NoSQL database access with real-time listeners and offline support.</p></div> <div class="card"><i class="ph ph-function card-icon"></i><h3>cloud_functions</h3><p>Calling deployed Cloud Functions from the client app.</p></div> <div class="card"><i class="ph ph-bell card-icon"></i><h3>firebase_messaging</h3><p>Push notifications via FCM, with APNs support on iOS.</p></div> </div> <div class="key-callout"><div class="callout-label">Note on firebase_options.dart</div><div class="callout-body">Running <code>flutterfire configure</code> generates <code>lib/firebase_options.dart</code> with default options. In this setup it exists as a fallback reference only - it is <strong>not used at runtime</strong> because the flavor config classes override it.</div></div> </div> </section> <section class="slide section-divider"><div class="part-label">Part 3</div><h2>Android Flavor Setup</h2><div class="section-sub">Configure Gradle product flavors so each build variant gets its own application ID, app display name, and <code>google-services.json</code> file.</div></section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 3 &middot; android/app/build.gradle</div> <h2 class="slide-title">Android configuration steps</h2> <div class="flow"> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Add the Google Services plugin</strong> to the <code>plugins</code> block: <code>id "com.google.gms.google-services"</code>.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Define product flavors</strong> in the <code>android</code> block. Add <code>flavorDimensions "default"</code> and create one flavor per environment (production, staging, uat), each with its own <code>applicationId</code> and <code>app_name</code> via <code>resValue</code>.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Add Firebase dependencies via BOM</strong> - use <code>platform("com.google.firebase:firebase-bom:33.3.0")</code> to manage all Firebase library versions. Individual libraries (firebase-analytics, firebase-auth, firebase-firestore) are added without version numbers.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Place google-services.json files</strong> in flavor-specific source directories: <code>android/app/src/production/</code>, <code>android/app/src/staging/</code>, <code>android/app/src/uat/</code>. Gradle automatically picks the right one based on the active flavor.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Configure signing</strong> - create a <code>key.properties</code> file in the <code>android/</code> directory with your keystore details. This file is gitignored and set up manually per machine and CI environment.</div></div> </div> <div class="key-callout"><div class="callout-label">Build command</div><div class="callout-body"><code>flutter build apk -t lib/main_staging.dart --flavor staging</code> - <code>-t</code> selects the entry point, <code>--flavor</code> selects the Gradle product flavor.</div></div> </div> </section> <section class="slide section-divider"><div class="part-label">Part 4</div><h2>iOS Flavor Setup</h2><div class="section-sub">iOS needs Xcode build configurations per flavor, Podfile configuration mapping, and a build phase script that copies the correct <code>GoogleService-Info.plist</code> into the app bundle at build time.</div></section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 4.1 - 4.3 &middot; Xcode and Podfile</div> <h2 class="slide-title">Xcode configurations, Podfile, plist files</h2> <div class="flow"> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Create build configurations in Xcode</strong> - go to Project &gt; Info &gt; Configurations and duplicate Debug, Profile, and Release for each flavor, resulting in <code>Debug-production</code>, <code>Release-production</code>, <code>Debug-staging</code>, <code>Release-staging</code>, <code>Debug-uat</code>, <code>Release-uat</code>, etc. Set the correct bundle ID and display name per configuration.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Update the Podfile</strong> - replace the default <code>project</code> configuration map to include all flavor variants. Map each Debug variant to <code>:debug</code> and each Profile/Release variant to <code>:release</code>. Without this, <code>pod install</code> fails for the custom configuration names.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Place GoogleService-Info.plist files</strong> in environment-specific directories under <code>ios/Runner/Firebase/</code>: <code>Production/</code>, <code>Staging/</code>, <code>Uat/</code>. Each plist comes from the matching Firebase project.</div></div> </div> </div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 4.4 &amp; 4.5 &middot; Build phase script and post_install</div> <h2 class="slide-title">Copy plist script + Podfile post-install</h2> <div class="two-col"> <div class="col"> <h3>4.4 Plist Copy Build Phase</h3> <p style="font-size:19px;color:var(--text-secondary);margin-bottom:14px;line-height:1.5;">In Xcode: Runner target &gt; Build Phases &gt; "+" &gt; "New Run Script Phase". Name it "Copy GoogleService-Info.plist".</p> <p style="font-size:19px;color:var(--text-secondary);margin-bottom:14px;line-height:1.5;">The script checks the <code>CONFIGURATION</code> variable and pattern-matches:</p> <ul class="bullets"> <li><i class="ph ph-arrow-right"></i><span>Contains <code>staging</code> - copies the Staging plist</span></li> <li><i class="ph ph-arrow-right"></i><span>Contains <code>uat</code> - copies the UAT plist</span></li> <li><i class="ph ph-arrow-right"></i><span>Everything else (including production and defaults) - copies the Production plist</span></li> </ul> <p style="font-size:17px;color:var(--text-secondary);margin-top:16px;line-height:1.45;"><strong style="color:var(--text-primary);">Position:</strong> This script must run <em>after</em> the Flutter build script and <em>before</em> the Sources compile phase.</p> </div> <div class="col"> <h3>4.5 Podfile post_install Settings</h3> <ul class="bullets"> <li><i class="ph ph-gear"></i><span><code>CLANG_CXX_LANGUAGE_STANDARD = c++17</code> - required by Firebase/gRPC</span></li> <li><i class="ph ph-gear"></i><span><code>IPHONEOS_DEPLOYMENT_TARGET = 15.0</code> - must match the Podfile platform version</span></li> <li><i class="ph ph-gear"></i><span><code>EXCLUDED_ARCHS[sdk=iphonesimulator*] = i386</code> - exclude i386 for Apple Silicon simulators</span></li> <li><i class="ph ph-gear"></i><span><code>PERMISSION_*</code> preprocessor definitions - required by <code>permission_handler</code> (camera, microphone, photos, location, notifications, media library)</span></li> <li><i class="ph ph-gear"></i><span><code>DEFINES_MODULE = YES</code> and <code>SWIFT_VERSION = 5.0</code> - fixes Firebase Swift/Objective-C module conflicts</span></li> </ul> </div> </div> </div> </section> <section class="slide section-divider"><div class="part-label">Part 5</div><h2>Firebase Functions Setup</h2><div class="section-sub">Backend logic lives in Cloud Functions. The CLI manages multiple Firebase projects through aliases, and per-project environment files keep secrets isolated between environments.</div></section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 5.1 - 5.4 &middot; CLI init, aliases, directory structure</div> <h2 class="slide-title">Initialise and structure</h2> <div class="flow"> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Install Firebase CLI</strong> globally (<code>npm install -g firebase-tools</code>), then from the project root (separate from Flutter) run <code>firebase login</code> and <code>firebase init</code>. Select Firestore, Functions (JavaScript, Node.js 20), and Hosting if needed.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Configure project aliases in <code>.firebaserc</code></strong> - map human-readable aliases to each Firebase project so you can switch with one command: <code>firebase use staging</code>, <code>firebase use production</code>, <code>firebase use uat</code>.</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Configure <code>firebase.json</code></strong> - defines Firestore (index file), Functions (source directory, codebase name, ignore patterns), and Hosting (public directory, URL rewrites for reset-password, login, etc.).</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Set up the Functions directory</strong> - organise into feature-based modules (<code>feed/</code>, <code>chat/</code>, <code>socialgraph/</code>, <code>user/</code>, <code>auth/</code>, <code>notifications/</code>, <code>media/</code>, <code>community_hub/</code>, etc.) plus a <code>core/</code> directory for shared utilities and <code>triggers.js</code> for Firestore document triggers.</div></div> </div> <div class="key-callout"><div class="callout-label">Export pattern in index.js</div><div class="callout-body">Initialise Firebase Admin once at the top, then import each module and explicitly re-export every function via <code>exports.functionName = module.functionName</code>. Firebase deploys each export as a separate Cloud Function.</div></div> </div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 5.5 - 5.7 &middot; Environment vars, rules, deployment</div> <h2 class="slide-title">Environment variables, Firestore, deployment</h2> <div class="two-col"> <div class="col"> <h3>5.5 Environment Variables</h3> <p style="font-size:18px;color:var(--text-secondary);margin-bottom:12px;line-height:1.5;">Create <code>.env.{firebase-project-id}</code> files in the <code>functions/</code> directory - one per environment.</p> <ul class="bullets"> <li><i class="ph ph-file-lock"></i><span><code>.env.{prod-project-id}</code></span></li> <li><i class="ph ph-file-lock"></i><span><code>.env.{staging-project-id}</code></span></li> <li><i class="ph ph-file-lock"></i><span><code>.env.{uat-project-id}</code></span></li> </ul> <p style="font-size:17px;color:var(--text-secondary);margin-top:14px;line-height:1.45;">Same variable names across all three (SendGrid API key, CRM API key, CRM base URL, verify user key, app hosting URL) with environment-specific values.</p> <p style="font-size:17px;color:var(--text-secondary);margin-top:12px;line-height:1.45;">Firebase Functions automatically loads the <code>.env</code> file matching the active project. Files are gitignored.</p> <h3 style="margin-top:20px;">5.6 Firestore Rules &amp; Indexes</h3> <ul class="bullets"> <li><i class="ph ph-shield"></i><span><strong>Rules</strong> (<code>firestore.rules</code>) - if all access is routed through Cloud Functions, deny direct access. Otherwise, use authenticated rules.</span></li> <li><i class="ph ph-graph"></i><span><strong>Indexes</strong> (<code>firestore.indexes.json</code>) - compound indexes for multi-field queries with ordering.</span></li> </ul> </div> <div class="col"> <h3>5.7 Deployment Commands</h3> <div class="flow" style="margin-top:4px;"> <div class="step"><div class="step-body" style="font-size:18px;"><code>firebase use staging</code><br/><span style="color:#D1D1D1;font-size:15px;">Switch to the target environment</span></div></div> <div class="step"><div class="step-body" style="font-size:18px;"><code>firebase deploy --only functions</code><br/><span style="color:#D1D1D1;font-size:15px;">Deploy all functions</span></div></div> <div class="step"><div class="step-body" style="font-size:18px;"><code>firebase deploy --only firestore:rules</code><br/><span style="color:#D1D1D1;font-size:15px;">Deploy Firestore security rules</span></div></div> <div class="step"><div class="step-body" style="font-size:18px;"><code>firebase deploy --only firestore:indexes</code><br/><span style="color:#D1D1D1;font-size:15px;">Deploy Firestore compound indexes</span></div></div> <div class="step"><div class="step-body" style="font-size:18px;"><code>firebase deploy --only hosting</code><br/><span style="color:#D1D1D1;font-size:15px;">Deploy web pages (if using Hosting)</span></div></div> </div> </div> </div> </div> </section> <section class="slide section-divider"><div class="part-label">Part 6</div><h2>How It All Connects</h2><div class="section-sub">End-to-end build flows showing how each flavor selection triggers the chain that ultimately connects the app to the correct Firebase project.</div></section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 6 &middot; Android build flow</div> <h2 class="slide-title">Android: flavor to Firebase</h2> <div class="flow"> <div class="step"><div class="step-num"></div><div class="step-body">Developer runs <code>flutter build apk -t lib/main_staging.dart --flavor staging</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body">Flutter compiles <code>main_staging.dart</code> as the entry point, which creates <code>StagingConfig()</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body">Gradle selects <code>android/app/src/staging/google-services.json</code> based on the flavor name</div></div> <div class="step"><div class="step-num"></div><div class="step-body">The Google Services plugin processes the JSON into Android resources at build time</div></div> <div class="step"><div class="step-num"></div><div class="step-body">At runtime, <code>mainCommon()</code> calls <code>Firebase.initializeApp(options: config.firebaseOptions)</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body">The <code>StagingConfig.firebaseOptions</code> getter detects the platform and returns the staging Android <code>FirebaseOptions</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Firebase connects to the staging project</strong> - the app is now talking to the correct backend</div></div> </div> </div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 6 &middot; iOS build flow</div> <h2 class="slide-title">iOS: scheme to Firebase</h2> <div class="flow"> <div class="step"><div class="step-num"></div><div class="step-body">Developer builds with the staging Xcode scheme (e.g. <code>Release-staging</code> configuration)</div></div> <div class="step"><div class="step-num"></div><div class="step-body">Flutter compiles <code>main_staging.dart</code> as the entry point, which creates <code>StagingConfig()</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body">The "Copy GoogleService-Info.plist" build phase detects <code>staging</code> in the configuration name</div></div> <div class="step"><div class="step-num"></div><div class="step-body">It copies <code>ios/Runner/Firebase/Staging/GoogleService-Info.plist</code> into the app bundle</div></div> <div class="step"><div class="step-num"></div><div class="step-body">At runtime, <code>mainCommon()</code> calls <code>Firebase.initializeApp(options: config.firebaseOptions)</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body">The <code>StagingConfig.firebaseOptions</code> getter detects iOS and returns the staging iOS <code>FirebaseOptions</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>Firebase connects to the staging project</strong> - the iOS app is now talking to the correct backend</div></div> </div> </div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 6 &middot; Functions deployment flow</div> <h2 class="slide-title">Functions: deploy to Firebase</h2> <div class="flow"> <div class="step"><div class="step-num"></div><div class="step-body"><code>firebase use staging</code> selects the staging project via the alias in <code>.firebaserc</code></div></div> <div class="step"><div class="step-num"></div><div class="step-body"><code>firebase deploy --only functions</code> deploys all functions to the staging Firebase project</div></div> <div class="step"><div class="step-num"></div><div class="step-body">Firebase automatically loads <code>.env.{staging-project-id}</code> for environment variables</div></div> <div class="step"><div class="step-num"></div><div class="step-body">Each export from <code>index.js</code> is deployed as a separate Cloud Function</div></div> <div class="step"><div class="step-num"></div><div class="step-body"><strong>The Flutter app (built with staging flavor) calls these functions</strong> via the <code>cloud_functions</code> package, targeting the same staging project</div></div> </div> <div class="key-callout"><div class="callout-label">The core principle</div><div class="callout-body">Every layer - Dart config, Gradle flavor, Xcode configuration, Firebase CLI alias - is keyed on the same environment name. Select once, and the entire chain resolves correctly.</div></div> </div> </section> <section class="slide section-divider"><div class="part-label">Part 7</div><h2>Cross-Environment Reference</h2><div class="section-sub">Quick-lookup table showing every config value and file path across the three environments. Use this when setting up a new mobile app template.</div></section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Part 7 &middot; Reference</div> <h2 class="slide-title" style="font-size:40px;">Cross-environment config lookup</h2> <div style="overflow:auto;flex:1;"> <table class="ref-table"> <thead><tr><th>Aspect</th><th>Production</th><th>Staging</th><th>UAT</th></tr></thead> <tbody> <tr><td>Firebase Project ID</td><td><code>{project-name}</code></td><td><code>{project-name}-stg</code></td><td><code>{project-name}-uat</code></td></tr> <tr><td>Android Package</td><td><code>com.company.app</code></td><td><code>com.company.app.stg</code></td><td><code>com.company.app.uat</code></td></tr> <tr><td>iOS Bundle ID</td><td><code>com.company.ios.app</code></td><td><code>com.company.ios.app.stg</code></td><td><code>com.company.ios.app.uat</code></td></tr> <tr><td>App Display Name</td><td>App</td><td>App Staging</td><td>App UAT</td></tr> <tr><td>Storage Bucket</td><td><code>{project-name}.firebasestorage.app</code></td><td><code>{project-name}-stg.firebasestorage.app</code></td><td><code>{project-name}-uat.firebasestorage.app</code></td></tr> <tr><td>Flutter Entry Point</td><td><code>main_production.dart</code></td><td><code>main_staging.dart</code></td><td><code>main_uat.dart</code></td></tr> <tr><td>Config Class</td><td><code>ProductionConfig</code></td><td><code>StagingConfig</code></td><td><code>UatConfig</code></td></tr> <tr><td>google-services.json</td><td><code>src/production/</code></td><td><code>src/staging/</code></td><td><code>src/uat/</code></td></tr> <tr><td>GoogleService-Info.plist</td><td><code>Firebase/Production/</code></td><td><code>Firebase/Staging/</code></td><td><code>Firebase/Uat/</code></td></tr> <tr><td>Functions .env file</td><td><code>.env.{prod-project-id}</code></td><td><code>.env.{staging-project-id}</code></td><td><code>.env.{uat-project-id}</code></td></tr> <tr><td>Firebase CLI Alias</td><td><code>firebase use production</code></td><td><code>firebase use staging</code></td><td><code>firebase use uat</code></td></tr> </tbody> </table> </div> </div> </section> <section class="slide"> <div class="content-area"> <div class="slide-eyebrow">Summary</div> <h2 class="slide-title">What this guide gives you</h2> <div class="card-grid"> <div class="card"><i class="ph ph-stack card-icon"></i><h3>Three isolated environments</h3><p>Production, staging, and UAT with separate Firebase projects, auth users, databases, storage, and functions.</p></div> <div class="card"><i class="ph ph-swap card-icon"></i><h3>One-command switching</h3><p>Swap between environments using flavor arguments on build commands and <code>firebase use</code> for deployments.</p></div> <div class="card"><i class="ph ph-lock card-icon"></i><h3>Isolated secrets</h3><p>Per-project <code>.env</code> files keep API keys and credentials separate between environments.</p></div> <div class="card"><i class="ph ph-devices card-icon"></i><h3>Android and iOS parity</h3><p>Gradle flavors on Android and Xcode configurations with a plist copy script on iOS produce matching builds.</p></div> <div class="card"><i class="ph ph-gear-six card-icon"></i><h3>Modular Cloud Functions</h3><p>Feature-based directory structure with a central <code>index.js</code> that re-exports every function.</p></div> <div class="card"><i class="ph ph-book-open card-icon"></i><h3>Template-ready</h3><p>Drop this documentation into any new mobile app project and follow the steps end-to-end.</p></div> </div> </div> </section> <section class="slide cover"> <h1 style="font-size:64px;">End of Documentation</h1> <div class="subtitle">Firebase setup process complete - ready to be used as a template for any future mobile app project.</div> <div class="meta">Use arrow keys or click anywhere to navigate.</div> </section> </div> <div class="controls-bar"> <div class="progress-bar"><div class="progress-fill" id="progress-fill"></div></div> <button class="theme-toggle" id="theme-toggle" aria-label="Toggle light/dark mode"><i class="ph ph-sun icon-sun"></i><i class="ph ph-moon icon-moon"></i><span class="theme-label label-dark">Light</span><span class="theme-label label-light">Dark</span></button> <button class="nav-btn" id="prev-btn" aria-label="Previous slide"><i class="ph ph-caret-left"></i></button> <span class="slide-counter" id="slide-counter">1 / 24</span> <button class="nav-btn" id="next-btn" aria-label="Next slide"><i class="ph ph-caret-right"></i></button> </div> <script> (function() { const slides = Array.from(document.querySelectorAll('.slide')); const progressFill = document.getElementById('progress-fill'); const slideCounter = document.getElementById('slide-counter'); const prevBtn = document.getElementById('prev-btn'); const nextBtn = document.getElementById('next-btn'); let current = 0; function render() { slides.forEach((s, i) => { s.classList.remove('active', 'prev'); if (i === current) s.classList.add('active'); else if (i < current) s.classList.add('prev'); }); const pct = ((current + 1) / slides.length) * 100; progressFill.style.width = pct + '%'; slideCounter.textContent = (current + 1) + ' / ' + slides.length; prevBtn.disabled = current === 0; nextBtn.disabled = current === slides.length - 1; } function next() { if (current < slides.length - 1) { current++; render(); } } function prev() { if (current > 0) { current--; render(); } } document.addEventListener('keydown', e => { if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); next(); } else if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); } else if (e.key === 'p' || e.key === 'P') { window.print(); } else if (e.key === 'Home') { current = 0; render(); } else if (e.key === 'End') { current = slides.length - 1; render(); } }); nextBtn.addEventListener('click', next); prevBtn.addEventListener('click', prev); document.addEventListener('click', e => { if (e.target.closest('.controls-bar')) return; if (e.target.closest('a')) return; next(); }); const themeToggle = document.getElementById('theme-toggle'); const savedTheme = localStorage.getItem('fb-docs-theme'); const prefersLight = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; const initialTheme = savedTheme || (prefersLight ? 'light' : 'dark'); if (initialTheme === 'light') document.documentElement.setAttribute('data-theme', 'light'); themeToggle.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme'); if (current === 'light') { document.documentElement.removeAttribute('data-theme'); localStorage.setItem('fb-docs-theme', 'dark'); } else { document.documentElement.setAttribute('data-theme', 'light'); localStorage.setItem('fb-docs-theme', 'light'); } }); document.addEventListener('keydown', e => { if (e.key === 't' || e.key === 'T') themeToggle.click(); }); render(); })(); </script> </body> </html>