diff --git a/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/.posthog-wizard b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/.posthog-wizard new file mode 100644 index 000000000..e69de29bb diff --git a/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/SKILL.md b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/SKILL.md new file mode 100644 index 000000000..869d818d5 --- /dev/null +++ b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/SKILL.md @@ -0,0 +1,53 @@ +--- +name: integration-android +description: PostHog integration for Android applications +metadata: + author: PostHog + version: dev +--- + +# PostHog integration for Android + +This skill helps you add PostHog analytics to Android applications. + +## Workflow + +Follow these steps in order to complete the integration: + +1. `references/1-begin.md` - PostHog Setup - Begin ← **Start here** +2. `references/2-edit.md` - PostHog Setup - Edit +3. `references/3-revise.md` - PostHog Setup - Revise +4. `references/4-conclude.md` - PostHog Setup - Conclusion + +## Reference files + +- `references/EXAMPLE.md` - Android example project code +- `references/1-begin.md` - Start the event tracking setup process by analyzing the project and creating an event tracking plan +- `references/2-edit.md` - Implement PostHog event tracking in the identified files, following best practices and the example project +- `references/3-revise.md` - Review and fix any errors in the PostHog integration implementation +- `references/4-conclude.md` - Review and fix any errors in the PostHog integration implementation +- `references/android.md` - Android - docs +- `references/identify-users.md` - Identify users - docs + +The example project shows the target implementation pattern. Consult the documentation for API details. + +## Key principles + +- **Environment variables**: Always use environment variables for PostHog keys. Never hardcode them. +- **Minimal changes**: Add PostHog code alongside existing integrations. Don't replace or restructure existing code. +- **Match the example**: Your implementation should follow the example project's patterns as closely as possible. + +## Framework guidelines + +- Adapt dependency configuration to the appropriate build.gradle(.kts) file according to the project gradle version +- Call `PostHogAndroid.setup()` only once in the Application class's `onCreate()` method, so it's initialized as early as possible and only once. +- Initialize PostHog in the Application class's `onCreate()` method +- Ensure every activity has a `android:label` to accurately track screen views. + +## Identifying users + +Identify users during login and signup events. Refer to the example code and documentation for the correct identify pattern for this framework. If both frontend and backend code exist, pass the client-side session and distinct ID using `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` headers to maintain correlation. + +## Error tracking + +Add PostHog error tracking to relevant files, particularly around critical user flows and API boundaries. diff --git a/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/1-begin.md b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/1-begin.md new file mode 100644 index 000000000..55f0a8326 --- /dev/null +++ b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/1-begin.md @@ -0,0 +1,56 @@ +--- +title: PostHog Setup - Begin +description: Start the event tracking setup process by analyzing the project and creating an event tracking plan +--- + +We're making an event tracking plan for this project. + +This is the first of several phases — plan the events, implement them, revise and validate changes, then conclude by creating a dashboard and writing a setup report. + +## Task list + +As soon as you've read this description and have a rough sense of the work, make a single **call `TaskCreate` immediately** before reading any reference file or beginning analysis. The user is watching the task pane and shouldn't see it sit empty. + +It's fine if your first list is incomplete or imprecise. Seed it with whatever high-level items you can infer from the overview above, then call `TaskCreate` again (or `TaskUpdate` to refine existing items) every time your understanding sharpens: after a phase reveals work you didn't anticipate, after planning surfaces concrete sub-items, after you hit something new. Use `TaskUpdate` to mark items `in_progress` when you start them and `completed` when you finish. Keeping the list current matters more than getting it right on the first call. + +Keep task titles broad and job-oriented. Describe the purpose or area of work with wording like "Planning event tracking", "Identifying users", "Installing PostHog", "Capturing events", or "Creating dashboards", not the specific files, paths, or symbols involved. Adjust the task names according to the user's project and context. + +Before proceeding, find any existing `posthog.capture()` code. Make note of event name formatting. + +From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events. Also look for additional files related to login that could be used for identifying users, along with error handling. Read the files. If a file is already well-covered by PostHog events, replace it with another option. Do not spawn subagents. + +Look for opportunities to track client-side events. + +**IMPORTANT: Server-side events are REQUIRED** if the project includes any instrumentable server-side code. If the project has API routes (e.g., `app/api/**/route.ts`) or Server Actions, you MUST include server-side events for critical business operations like: + + - Payment/checkout completion + - Webhook handlers + - Authentication endpoints + +Do not skip server-side events - they capture actions that cannot be tracked client-side. + +Create a new file with a JSON array at the root of the project: .posthog-events.json. It should include one object for each event we want to add with these exact field names: `event_name` (the event name), `event_description` (one sentence), and `file` (the file path the event goes in). The wizard reads this file to surface the plan in the UI. If events already exist, don't duplicate them; supplement them. + +Track actions only, not pageviews. These can be captured automatically. Exceptions can be made for "viewed"-type events that correspond to the top of a conversion funnel. + +As you review files, make an internal note of opportunities to identify users and catch errors. We'll need them for the next step. + +## Status + +Before beginning a phase of the setup, you will send a status message with the exact prefix '[STATUS]', as in: + +[STATUS] Checking project structure. + +Status to report in this phase: + +- Checking project structure +- Verifying PostHog dependencies +- Generating events based on project + +## Abort statuses + +If and only if the instructions have `[ABORT]` states specified, and you clearly match the conditions for an abort, emit the abort message. Do NOT attempt to exit or halt yourself — the wizard's middleware catches `[ABORT]` and terminates the run for you. + +--- + +**Upon completion, continue with:** [2-edit.md](2-edit.md) \ No newline at end of file diff --git a/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/2-edit.md b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/2-edit.md new file mode 100644 index 000000000..e5f7ffd16 --- /dev/null +++ b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/2-edit.md @@ -0,0 +1,36 @@ +--- +title: PostHog Setup - Edit +description: Implement PostHog event tracking in the identified files, following best practices and the example project +--- + +For each of the files and events noted in .posthog-events.json, make edits to capture events using PostHog. Make sure to set up any helper files needed. Carefully examine the included example project code: your implementation should match it as closely as possible. Do not spawn subagents. + +Use environment variables for PostHog keys. Do not hardcode PostHog keys. + +If a file already has existing integration code for other tools or services, don't overwrite or remove that code. Place PostHog code below it. + +For each event, add useful properties, and use your access to the PostHog source code to ensure correctness. You also have access to documentation about creating new events with PostHog. Consider this documentation carefully and follow it closely before adding events. Your integration should be based on documented best practices. Carefully consider how the user project's framework version may impact the correct PostHog integration approach. + +Remember that you can find the source code for any dependency in the node_modules directory. This may be necessary to properly populate property names. There are also example project code files available via the PostHog MCP; use these for reference. + +Where possible, add calls for PostHog's identify() function on the client side upon events like logins and signups. Use the contents of login and signup forms to identify users on submit. If there is server-side code, pass the client-side session and distinct ID to the server-side code to identify the user. On the server side, make sure events have a matching distinct ID where relevant. + +It's essential to do this in both client code and server code, so that user behavior from both domains is easy to correlate. + +You should also add PostHog exception capture error tracking to these files where relevant. + +Remember: Do not alter the fundamental architecture of existing files. Make your additions minimal and targeted. + +Remember the documentation and example project resources you were provided at the beginning. Read them now. + +## Status + +Status to report in this phase: + +- Inserting PostHog capture code +- A status message for each file whose edits you are planning, including a high level summary of changes +- A status message for each file you have edited + +--- + +**Upon completion, continue with:** [3-revise.md](3-revise.md) \ No newline at end of file diff --git a/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/3-revise.md b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/3-revise.md new file mode 100644 index 000000000..3b07f5069 --- /dev/null +++ b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/3-revise.md @@ -0,0 +1,22 @@ +--- +title: PostHog Setup - Revise +description: Review and fix any errors in the PostHog integration implementation +--- + +Check the project for errors. Read the package.json file for any type checking or build scripts that may provide input about what to fix. Remember that you can find the source code for any dependency in the node_modules directory. Do not spawn subagents. + +Ensure that any components created were actually used. + +Once all other tasks are complete, run any linter or prettier-like scripts found in the package.json, but ONLY on the files you have edited or created during this session. Do not run formatting or linting across the entire project's codebase. + +## Status + +Status to report in this phase: + +- Finding and correcting errors +- Report details of any errors you fix +- Linting, building and prettying + +--- + +**Upon completion, continue with:** [4-conclude.md](4-conclude.md) \ No newline at end of file diff --git a/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/4-conclude.md b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/4-conclude.md new file mode 100644 index 000000000..d876d4353 --- /dev/null +++ b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/4-conclude.md @@ -0,0 +1,57 @@ +--- +title: PostHog Setup - Conclusion +description: Review and fix any errors in the PostHog integration implementation +--- + +Use the PostHog MCP to create a new dashboard named "Analytics basics (wizard)" based on the events created here. Keep the `(wizard)` tag with that exact casing so anyone browsing PostHog can see the wizard created this dashboard, and so a quick search for `(wizard)` surfaces every wizard-created artifact in one go. Make sure to use the exact same event names as implemented in the code. Populate it with up to five insights, with special emphasis on things like conversion funnels, churn events, and other business critical insights. + +Once the dashboard exists, emit its URL on its own line in your assistant message using this exact marker: `[DASHBOARD_URL] `. The wizard parses this marker from your visible message and surfaces the link in the success summary. Mentioning the URL only in thinking or in prose without the marker means the link is dropped. + +Search for a file called `.posthog-events.json` and read it for available events. + +Do not spawn subagents. + +Create the file posthog-setup-report.md. It should include a summary of the integration edits, a table with the event names, event descriptions, and files where events were added, a list of links for the dashboard and insights created, and a "Verify before merging" checklist (see below). Follow this format: + + +# PostHog post-wizard report + +The wizard has completed a deep integration of your project. [Detailed summary of changes] + +[table of events/descriptions/files] + +## Next steps + +We've built some insights and a dashboard for you to keep an eye on user behavior, based on the events we just instrumented: + +[links] + +## Verify before merging + +[checklist] + +### Agent skill + +We've left an agent skill folder in your project. You can use this context for further agent development when using Claude Code. This will help ensure the model provides the most up-to-date approaches for integrating PostHog. + + + +For the "Verify before merging" checklist, write GitHub-style checkboxes (`- [ ] ...`) covering what the developer (or their coding agent) still needs to do to take this from "wizard finished" to "merged". Include ONLY the items that actually apply to the integration you just performed — judge each against the code you changed in this run, and drop any that don't fit. Phrase each item as a concrete, checkable action. Candidate items, with the condition for including each: + +- Always: "Run a full production build (the wizard only verified the files it touched) and fix any lint or type errors introduced by the generated code." +- Always: "Run the test suite — call sites that were rewritten or instrumented may need updated mocks or fixtures." +- If you added environment variables: "Add the exact PostHog env var names you added to `.env.example` and any monorepo/bootstrap scripts so collaborators know what to set." +- If this integration ships a minified production browser bundle (most SPA/SSR web frameworks — e.g. Next.js, Nuxt, SvelteKit, Astro, Vite-based apps): "Wire source-map upload (`posthog-cli sourcemap` or your bundler's upload step) into CI so production stack traces de-minify." +- If LLM analytics was set up in this run: "Trigger the LLM call path(s) you instrumented and confirm `$ai_generation` events appear in PostHog AI Observability." +- If the app has user auth and an `identify` call was added: "Confirm the returning-visitor path also calls `identify` — a handler that only identifies on fresh login can leave returning sessions on anonymous distinct IDs." + +Do not invent items beyond what applies. If only the two "Always" items apply, the checklist is just those two. + +Upon completion, remove .posthog-events.json. + +## Status + +Status to report in this phase: + +- Configured dashboard: [insert PostHog dashboard URL] +- Created setup report: [insert full local file path] \ No newline at end of file diff --git a/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/EXAMPLE.md b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/EXAMPLE.md new file mode 100644 index 000000000..32e7823dd --- /dev/null +++ b/apps/basic-integration/android/Jetchat/.claude/skills/integration-android/references/EXAMPLE.md @@ -0,0 +1,2122 @@ +# PostHog Android Example Project + +Repository: https://github.com/PostHog/context-mill +Path: example-apps/android + +--- + +## README.md + +# PostHog Android example + +This is an Android example demonstrating PostHog integration with product analytics, session replay, and error tracking using Kotlin and Jetpack Compose. + +This example uses the PostHog Android SDK (`posthog-android`) to provide automatic PostHog integration with built-in error tracking, session replay, and simplified configuration. + +## Features + +- **Product Analytics**: Track user events and behaviors +- **Session Replay**: Record and replay user sessions +- **Error Tracking**: Automatic error capture and crash reporting +- **User Authentication**: Demo login system with PostHog user identification +- **Event Tracking**: Examples of custom event tracking throughout the app + +## Getting Started + +### 1. Prerequisites + +- Android Studio (latest stable version) +- Android SDK (API level 24 or higher) +- JDK 11 or higher +- Gradle 8.0 or higher +- A [PostHog account](https://app.posthog.com/signup) + +### 2. Configure Environment Variables + +The PostHog configuration is stored in `local.properties` (this file is gitignored): + +```properties +# PostHog configuration +posthog.apiKey=your_posthog_project_token +posthog.host=https://us.i.posthog.com +``` + +Alternatively, you can configure PostHog in your `build.gradle` file: + +```gradle +android { + defaultConfig { + buildConfigField "String", "POSTHOG_PROJECT_TOKEN", "\"your_posthog_project_token\"" + buildConfigField "String", "POSTHOG_HOST", "\"https://us.i.posthog.com\"" + } +} +``` + +Get your PostHog project token from your [PostHog project settings](https://app.posthog.com/project/settings). + +### 3. Build and Run + +1. Open the project in Android Studio +2. Sync Gradle files +3. Run the app on an emulator or physical device + +## Project Structure + +``` +├── app/ +│ ├── src/ +│ │ ├── main/ +│ │ │ ├── java/com/example/posthog/ +│ │ │ │ ├── BurritoApplication.kt # Application class with PostHog initialization +│ │ │ │ ├── MainActivity.kt # Main activity +│ │ │ │ ├── ui/ +│ │ │ │ │ ├── screens/ +│ │ │ │ │ │ ├── LoginScreen.kt # Login screen with user identification +│ │ │ │ │ │ ├── BurritoScreen.kt # Demo feature screen with event tracking +│ │ │ │ │ │ └── ProfileScreen.kt # User profile with error tracking demo +│ │ │ │ │ └── components/ # Reusable UI components +│ │ │ │ └── utils/ +│ │ │ │ └── PostHogHelper.kt # PostHog utility functions +│ │ │ ├── res/ # Resources (layouts, strings, etc.) +│ │ │ └── AndroidManifest.xml # App manifest +│ │ └── test/ # Unit tests +│ └── build.gradle # App-level Gradle configuration +├── build.gradle # Project-level Gradle configuration +├── settings.gradle # Gradle settings +└── local.properties # Local configuration (gitignored) +``` + +## Key Integration Points + +### Application Initialization (BurritoApplication.kt) + +PostHog is initialized in the `Application` class to ensure it's available throughout the app lifecycle: + +```kotlin +class BurritoApplication : Application() { + override fun onCreate() { + super.onCreate() + + val posthogConfig = PostHogConfig( + apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN, + host = BuildConfig.POSTHOG_HOST + ).apply { + // Enable session replay + sessionReplay = true + + // Enable automatic exception capture + captureApplicationLifecycleEvents = true + captureDeepLinks = true + captureScreenViews = true + } + + PostHog.setup(this, posthogConfig) + } +} +``` + +**Key Points:** +- PostHog is initialized in `onCreate()` to ensure it's initialized as early as possible +- Configuration is loaded from `BuildConfig` (set in `build.gradle`) +- Session replay, lifecycle events, and screen views are enabled +- The Application class must be registered in `AndroidManifest.xml` + +### User Identification (LoginScreen.kt) + +Users are identified when they log in: + +```kotlin +val posthog = PostHog.getInstance() + +fun handleLogin(username: String, password: String) { + // Authenticate user + val success = authenticateUser(username, password) + + if (success) { + // Identify the user once on login/sign up + posthog.identify( + distinctId = username, + properties = mapOf( + "username" to username, + "login_method" to "password" + ) + ) + + // Capture login event + posthog.capture("user_logged_in", mapOf( + "username" to username + )) + } +} +``` + +**Key Points:** +- `identify()` is called once when the user logs in or signs up +- User properties can be set during identification +- Events are captured using `capture()` with event names and properties +- The `distinctId` should be a unique identifier for the user + +### Event Tracking (BurritoScreen.kt) + +Custom events are tracked throughout the app: + +```kotlin +val posthog = PostHog.getInstance() + +fun handleBurritoConsideration() { + // Track custom event + posthog.capture("burrito_considered", mapOf( + "total_considerations" to considerationCount, + "username" to currentUser.username, + "timestamp" to System.currentTimeMillis() + )) + + // Update user properties + posthog.setUserProperties(mapOf( + "last_burrito_consideration" to System.currentTimeMillis(), + "total_burrito_considerations" to considerationCount + )) +} +``` + +**Key Points:** +- Events are captured with `capture()` method +- Event properties provide context about the event +- User properties can be updated with `setUserProperties()` +- Properties can be strings, numbers, booleans, or dates + +### Error Tracking + +Errors are captured automatically and can also be tracked manually: + +**Automatic Error Capture:** +PostHog automatically captures uncaught exceptions when configured: + +```kotlin +val posthogConfig = PostHogConfig( + apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN, + host = BuildConfig.POSTHOG_HOST +).apply { + // Automatic exception capture is enabled by default + captureApplicationLifecycleEvents = true +} +``` + +**Manual Error Capture:** +```kotlin +val posthog = PostHog.getInstance() + +try { + // Risky operation + performRiskyOperation() +} catch (e: Exception) { + // Capture exception manually + posthog.captureException(e, mapOf( + "context" to "burrito_consideration", + "user_id" to currentUser.id + )) +} +``` + +### Screen View Tracking + +Screen views are automatically tracked when `captureScreenViews` is enabled. You can also manually track screen views: + +```kotlin +val posthog = PostHog.getInstance() + +// Manual screen view tracking +posthog.screen("BurritoScreen", mapOf( + "screen_category" to "features", + "user_type" to "premium" +)) +``` + +### Session Replay + +Session replay is enabled in the PostHog configuration: + +```kotlin +val posthogConfig = PostHogConfig( + apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN, + host = BuildConfig.POSTHOG_HOST +).apply { + sessionReplay = true + sessionReplayConfig = SessionReplayConfig( + maskAllInputs = false, // Set to true to mask all input fields + maskAllText = false // Set to true to mask all text + ) +} +``` + +### Accessing PostHog in Components + +PostHog is accessed via the singleton instance: + +```kotlin +val posthog = PostHog.getInstance() +posthog.capture("event_name", mapOf("property" to "value")) +``` + +The instance is available throughout your application after initialization. + +## Gradle Configuration + +### App-level build.gradle + +```gradle +android { + defaultConfig { + // PostHog configuration + buildConfigField "String", "POSTHOG_PROJECT_TOKEN", "\"${project.findProperty("posthog.apiKey") ?: ""}\"" + buildConfigField "String", "POSTHOG_HOST", "\"${project.findProperty("posthog.host") ?: "https://us.i.posthog.com"}\"" + } +} + +dependencies { + // PostHog Android SDK + implementation 'com.posthog:posthog-android:3.+' + + // Other dependencies... +} +``` + +### Reading from local.properties + +The `local.properties` file is automatically read by Gradle: + +```gradle +def localProperties = new Properties() +localProperties.load(new FileInputStream(rootProject.file("local.properties"))) + +android { + defaultConfig { + buildConfigField "String", "POSTHOG_PROJECT_TOKEN", "\"${localProperties.getProperty("posthog.apiKey", "")}\"" + buildConfigField "String", "POSTHOG_HOST", "\"${localProperties.getProperty("posthog.host", "https://us.i.posthog.com")}\"" + } +} +``` + +## Best Practices + +1. **Initialize Early**: Initialize PostHog in your `Application.onCreate()` method +2. **Identify Once**: Call `identify()` once when the user logs in or signs up +3. **Use Meaningful Event Names**: Use clear, descriptive event names (e.g., `user_logged_in` instead of `login`) +4. **Include Context**: Add relevant properties to events for better analysis +5. **Handle Errors Gracefully**: Don't let PostHog errors break your app +6. **Test in Development**: Use a separate PostHog project for development/testing +7. **Respect Privacy**: Be mindful of PII (Personally Identifiable Information) in events and properties + +## Learn More + +- [PostHog Documentation](https://posthog.com/docs) +- [Android Documentation](https://developer.android.com) +- [PostHog Android Integration Guide](https://posthog.com/docs/libraries/android) +- [PostHog Android SDK](https://github.com/PostHog/posthog-android) + +--- + +## app/proguard-rules.pro + +```pro +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile +``` + +--- + +## app/src/main/AndroidManifest.xml + +```xml + + + + + + + + + + + + + + + + + +``` + +--- + +## app/src/main/java/com/example/posthog/BurritoApp.kt + +```kt +package com.example.posthog + +import android.app.Application +import com.posthog.android.PostHogAndroid +import com.posthog.android.PostHogAndroidConfig + +class BurritoApplication : Application() { + override fun onCreate() { + super.onCreate() + + // Initialize PostHog early in Application lifecycle + val config = PostHogAndroidConfig( + apiKey = BuildConfig.POSTHOG_PROJECT_TOKEN, + host = BuildConfig.POSTHOG_HOST, + ).apply { + debug = true + errorTrackingConfig.autoCapture = true + } + + PostHogAndroid.setup(this, config) + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/data/User.kt + +```kt +package com.example.posthog.data + +data class User( + val username: String, + val burritoConsiderations: Int = 0 +) + +``` + +--- + +## app/src/main/java/com/example/posthog/data/UserRepository.kt + +```kt +package com.example.posthog.data + +import android.content.Context +import android.content.SharedPreferences +import org.json.JSONObject + +class UserRepository(context: Context) { + + private val prefs: SharedPreferences = context.getSharedPreferences( + PREFS_NAME, Context.MODE_PRIVATE + ) + + companion object { + private const val PREFS_NAME = "burrito_app_prefs" + private const val KEY_CURRENT_USERNAME = "current_username" + private const val KEY_USER_DATA_PREFIX = "user_data_" + } + + fun getCurrentUsername(): String? { + return prefs.getString(KEY_CURRENT_USERNAME, null) + } + + fun getUser(username: String): User? { + val json = prefs.getString("$KEY_USER_DATA_PREFIX$username", null) ?: return null + return try { + val obj = JSONObject(json) + User( + username = obj.getString("username"), + burritoConsiderations = obj.getInt("burritoConsiderations") + ) + } catch (e: Exception) { + null + } + } + + fun saveUser(user: User) { + val json = JSONObject().apply { + put("username", user.username) + put("burritoConsiderations", user.burritoConsiderations) + }.toString() + + prefs.edit() + .putString("$KEY_USER_DATA_PREFIX${user.username}", json) + .putString(KEY_CURRENT_USERNAME, user.username) + .apply() + } + + fun clearCurrentUser() { + prefs.edit() + .remove(KEY_CURRENT_USERNAME) + .apply() + } + + fun getCurrentUser(): User? { + val username = getCurrentUsername() ?: return null + return getUser(username) + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/MainActivity.kt + +```kt +package com.example.posthog + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.example.posthog.navigation.NavGraph +import com.example.posthog.navigation.Screen +import com.example.posthog.ui.components.AppHeader +import com.example.posthog.ui.components.BottomNavBar +import com.example.posthog.ui.theme.BackgroundGray +import com.example.posthog.ui.theme.PostHogTheme +import com.example.posthog.viewmodel.AuthViewModel + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PostHogTheme { + BurritoApp() + } + } + } +} + +@Composable +fun BurritoApp() { + val navController = rememberNavController() + val viewModel: AuthViewModel = viewModel() + + val isAuthenticated by viewModel.isAuthenticated.collectAsState() + val currentUser by viewModel.currentUser.collectAsState() + + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + AppHeader( + isAuthenticated = isAuthenticated, + username = currentUser?.username, + currentRoute = currentRoute, + onNavigate = { route -> + navController.navigate(route) { + popUpTo(Screen.Home.route) + launchSingleTop = true + } + }, + onLogout = { + viewModel.logout() + navController.navigate(Screen.Home.route) { + popUpTo(Screen.Home.route) { inclusive = true } + } + } + ) + }, + bottomBar = { + BottomNavBar( + isAuthenticated = isAuthenticated, + currentRoute = currentRoute, + onNavigate = { route -> + navController.navigate(route) { + popUpTo(Screen.Home.route) + launchSingleTop = true + } + } + ) + }, + containerColor = BackgroundGray + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .background(BackgroundGray) + .padding(innerPadding) + ) { + NavGraph( + navController = navController, + viewModel = viewModel + ) + } + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/navigation/NavGraph.kt + +```kt +package com.example.posthog.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.example.posthog.ui.screens.BurritoScreen +import com.example.posthog.ui.screens.HomeScreen +import com.example.posthog.ui.screens.ProfileScreen +import com.example.posthog.viewmodel.AuthViewModel + +sealed class Screen(val route: String) { + object Home : Screen("home") + object Burrito : Screen("burrito") + object Profile : Screen("profile") +} + +@Composable +fun NavGraph( + navController: NavHostController, + viewModel: AuthViewModel +) { + val isAuthenticated by viewModel.isAuthenticated.collectAsState() + val currentUser by viewModel.currentUser.collectAsState() + + NavHost( + navController = navController, + startDestination = Screen.Home.route + ) { + composable(Screen.Home.route) { + HomeScreen( + isAuthenticated = isAuthenticated, + username = currentUser?.username, + onLogin = { username -> viewModel.login(username) } + ) + } + + composable(Screen.Burrito.route) { + if (!isAuthenticated) { + LaunchedEffect(Unit) { + navController.navigate(Screen.Home.route) { + popUpTo(Screen.Home.route) { inclusive = true } + } + } + } else { + BurritoScreen( + burritoCount = currentUser?.burritoConsiderations ?: 0, + onConsiderBurrito = { viewModel.incrementBurritoCount() } + ) + } + } + + composable(Screen.Profile.route) { + if (!isAuthenticated) { + LaunchedEffect(Unit) { + navController.navigate(Screen.Home.route) { + popUpTo(Screen.Home.route) { inclusive = true } + } + } + } else { + ProfileScreen( + username = currentUser?.username ?: "", + burritoCount = currentUser?.burritoConsiderations ?: 0 + ) + } + } + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/components/AppHeader.kt + +```kt +package com.example.posthog.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.posthog.ui.theme.DarkHeader +import com.example.posthog.ui.theme.ErrorRed +import com.example.posthog.ui.theme.White + +@Composable +fun AppHeader( + isAuthenticated: Boolean, + username: String?, + currentRoute: String?, + onNavigate: (String) -> Unit, + onLogout: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(DarkHeader) + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // App title + Text( + text = "Burrito App", + color = White, + fontSize = 18.sp + ) + + // User section (right side) + if (isAuthenticated && username != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = username, + color = White, + fontSize = 14.sp + ) + + Button( + onClick = onLogout, + colors = ButtonDefaults.buttonColors( + containerColor = ErrorRed + ), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp) + ) { + Text( + text = "Logout", + color = White, + fontSize = 14.sp + ) + } + } + } + } + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/components/BottomNavBar.kt + +```kt +package com.example.posthog.ui.components + +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.posthog.navigation.Screen +import com.example.posthog.ui.theme.PrimaryBlue +import com.example.posthog.ui.theme.TextGray +import com.example.posthog.ui.theme.White + +sealed class BottomNavItem( + val route: String, + val label: String, + val selectedIcon: ImageVector?, + val unselectedIcon: ImageVector? +) { + object Home : BottomNavItem( + route = Screen.Home.route, + label = "Home", + selectedIcon = Icons.Filled.Home, + unselectedIcon = Icons.Outlined.Home + ) + + object Burrito : BottomNavItem( + route = Screen.Burrito.route, + label = "Burrito", + selectedIcon = null, // We'll use a custom icon or emoji + unselectedIcon = null + ) + + object Profile : BottomNavItem( + route = Screen.Profile.route, + label = "Profile", + selectedIcon = Icons.Filled.Person, + unselectedIcon = Icons.Outlined.Person + ) +} + +@Composable +fun BottomNavBar( + isAuthenticated: Boolean, + currentRoute: String?, + onNavigate: (String) -> Unit +) { + val items = if (isAuthenticated) { + listOf(BottomNavItem.Home, BottomNavItem.Burrito, BottomNavItem.Profile) + } else { + listOf(BottomNavItem.Home) + } + + NavigationBar( + containerColor = White + ) { + items.forEach { item -> + val selected = currentRoute == item.route + + NavigationBarItem( + selected = selected, + onClick = { onNavigate(item.route) }, + icon = { + if (item.selectedIcon != null && item.unselectedIcon != null) { + Icon( + imageVector = if (selected) item.selectedIcon else item.unselectedIcon, + contentDescription = item.label, + modifier = Modifier.size(24.dp) + ) + } else { + // For Burrito, use text emoji as icon + Text( + text = "🌯", + fontSize = 24.sp + ) + } + }, + label = { + Text( + text = item.label, + fontSize = 12.sp + ) + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = PrimaryBlue, + selectedTextColor = PrimaryBlue, + unselectedIconColor = TextGray, + unselectedTextColor = TextGray, + indicatorColor = PrimaryBlue.copy(alpha = 0.1f) + ) + ) + } + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/components/StatsCard.kt + +```kt +package com.example.posthog.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.posthog.ui.theme.LightGray +import com.example.posthog.ui.theme.TextDark +import com.example.posthog.ui.theme.TextGray + +@Composable +fun StatsCard( + title: String, + value: String, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = LightGray, + shape = RoundedCornerShape(4.dp) + ) + .padding(16.dp) + ) { + Text( + text = title, + color = TextGray, + fontSize = 14.sp + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = value, + color = TextDark, + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/screens/BurritoScreen.kt + +```kt +package com.example.posthog.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.posthog.ui.components.StatsCard +import com.example.posthog.ui.theme.BackgroundGray +import com.example.posthog.ui.theme.SuccessGreen +import com.example.posthog.ui.theme.TextDark +import com.example.posthog.ui.theme.TextGray +import com.example.posthog.ui.theme.White +import kotlinx.coroutines.delay + +@Composable +fun BurritoScreen( + burritoCount: Int, + onConsiderBurrito: () -> Unit +) { + var showSuccess by remember { mutableStateOf(false) } + + LaunchedEffect(showSuccess) { + if (showSuccess) { + delay(2000) + showSuccess = false + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(BackgroundGray) + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .widthIn(max = 600.dp) + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Main content card + Column( + modifier = Modifier + .fillMaxWidth() + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = TextDark.copy(alpha = 0.1f), + spotColor = TextDark.copy(alpha = 0.1f) + ) + .background( + color = White, + shape = RoundedCornerShape(8.dp) + ) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Burrito consideration zone", + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Take a moment to truly consider the burrito.", + fontSize = 16.sp, + color = TextGray, + textAlign = TextAlign.Center, + lineHeight = 26.sp + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { + onConsiderBurrito() + showSuccess = true + }, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = SuccessGreen + ), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "Consider the Burrito", + fontSize = 18.sp, + color = White + ) + } + + if (showSuccess) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "You have considered the burrito. Well done!", + color = SuccessGreen, + fontSize = 16.sp, + textAlign = TextAlign.Center + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Consideration stats", + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + StatsCard( + title = "Total Burrito Considerations", + value = burritoCount.toString() + ) + } + } + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/screens/HomeScreen.kt + +```kt +package com.example.posthog.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.posthog.ui.theme.BackgroundGray +import com.example.posthog.ui.theme.BorderGray +import com.example.posthog.ui.theme.PrimaryBlue +import com.example.posthog.ui.theme.TextDark +import com.example.posthog.ui.theme.TextGray +import com.example.posthog.ui.theme.White + +@Composable +fun HomeScreen( + isAuthenticated: Boolean, + username: String?, + onLogin: (String) -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(BackgroundGray) + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + contentAlignment = if (isAuthenticated) Alignment.TopCenter else Alignment.Center + ) { + Column( + modifier = Modifier + .widthIn(max = 600.dp) + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (isAuthenticated && username != null) { + LoggedInContent(username = username) + } else { + LoginForm(onLogin = onLogin) + } + } + } +} + +@Composable +private fun LoggedInContent(username: String) { + ContentCard { + Text( + text = "Welcome back, $username!", + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Ready to consider some burritos?", + fontSize = 16.sp, + color = TextGray, + lineHeight = 26.sp + ) + } +} + +@Composable +private fun LoginForm(onLogin: (String) -> Unit) { + var username by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + ContentCard { + Text( + text = "Welcome to Burrito Consideration App", + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // Username field + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Username", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = TextDark, + modifier = Modifier.padding(bottom = 8.dp) + ) + OutlinedTextField( + value = username, + onValueChange = { username = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(4.dp), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = BorderGray, + focusedBorderColor = PrimaryBlue, + unfocusedContainerColor = White, + focusedContainerColor = White + ) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Password field + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Password", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = TextDark, + modifier = Modifier.padding(bottom = 8.dp) + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + shape = RoundedCornerShape(4.dp), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = BorderGray, + focusedBorderColor = PrimaryBlue, + unfocusedContainerColor = White, + focusedContainerColor = White + ) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { + if (username.isNotBlank()) { + onLogin(username) + } + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = PrimaryBlue + ), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "Sign In", + fontSize = 16.sp, + color = White + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Note: This is a demo app. Enter any username to sign in.", + fontSize = 14.sp, + color = TextGray, + textAlign = TextAlign.Center, + lineHeight = 21.sp + ) + } +} + +@Composable +private fun ContentCard( + content: @Composable () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = TextDark.copy(alpha = 0.1f), + spotColor = TextDark.copy(alpha = 0.1f) + ) + .background( + color = White, + shape = RoundedCornerShape(8.dp) + ) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + content() + } +} + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/screens/ProfileScreen.kt + +```kt +package com.example.posthog.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.posthog.ui.components.StatsCard +import com.example.posthog.ui.theme.BackgroundGray +import com.example.posthog.ui.theme.TextDark +import com.example.posthog.ui.theme.TextGray +import com.example.posthog.ui.theme.White + +@Composable +fun ProfileScreen( + username: String, + burritoCount: Int +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(BackgroundGray) + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + contentAlignment = Alignment.TopCenter + ) { + Column( + modifier = Modifier + .widthIn(max = 600.dp) + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Main content card + Column( + modifier = Modifier + .fillMaxWidth() + .shadow( + elevation = 4.dp, + shape = RoundedCornerShape(8.dp), + ambientColor = TextDark.copy(alpha = 0.1f), + spotColor = TextDark.copy(alpha = 0.1f) + ) + .background( + color = White, + shape = RoundedCornerShape(8.dp) + ) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "User Profile", + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // Your Information section + Text( + text = "Your Information", + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Username display + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = "Username", + fontSize = 14.sp, + color = TextGray + ) + Text( + text = username, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Stats card + StatsCard( + title = "Total Burrito Considerations", + value = burritoCount.toString() + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // Your Burrito Journey section + Text( + text = "Your Burrito Journey", + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + color = TextDark, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = getJourneyMessage(burritoCount), + fontSize = 16.sp, + color = TextGray, + textAlign = TextAlign.Start, + lineHeight = 26.sp, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} + +private fun getJourneyMessage(count: Int): String = when { + count == 0 -> "You haven't considered any burritos yet. Start your journey!" + count == 1 -> "You've considered the burrito potential once. The journey begins!" + count in 2..4 -> "You're getting the hang of burrito consideration!" + count in 5..9 -> "You're becoming a burrito consideration expert!" + else -> "You are a true burrito consideration master!" +} + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/theme/Color.kt + +```kt +package com.example.posthog.ui.theme + +import androidx.compose.ui.graphics.Color + +val PrimaryBlue = Color(0xFF0070F3) +val PrimaryBlueHover = Color(0xFF0051CC) +val SuccessGreen = Color(0xFF28A745) +val SuccessGreenHover = Color(0xFF218838) +val ErrorRed = Color(0xFFDC3545) +val ErrorRedHover = Color(0xFFC82333) +val DarkHeader = Color(0xFF333333) +val DarkHeaderHover = Color(0xFF555555) +val LightGray = Color(0xFFF8F9FA) +val BorderGray = Color(0xFFDDDDDD) +val TextGray = Color(0xFF666666) +val BackgroundGray = Color(0xFFF5F5F5) +val TextDark = Color(0xFF333333) +val White = Color(0xFFFFFFFF) + +``` + +--- + +## app/src/main/java/com/example/posthog/ui/theme/Theme.kt + +```kt +package com.example.posthog.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val LightColorScheme = lightColorScheme( + primary = PrimaryBlue, + secondary = SuccessGreen, + tertiary = DarkHeader, + background = BackgroundGray, + surface = White, + onPrimary = White, + onSecondary = White, + onTertiary = White, + onBackground = TextDark, + onSurface = TextDark +) + +@Composable +fun PostHogTheme( + content: @Composable () -> Unit +) { + MaterialTheme( + colorScheme = LightColorScheme, + typography = Typography, + content = content + ) +} +``` + +--- + +## app/src/main/java/com/example/posthog/ui/theme/Type.kt + +```kt +package com.example.posthog.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Typography based on design specification +// Uses system font stack (FontFamily.Default maps to Roboto on Android) +val Typography = Typography( + // H1 - Page titles (32sp) + displayLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp + ), + // H2 - Section titles (24sp) + displayMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp + ), + // H3 - Subsection titles (20sp) + displaySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + lineHeight = 26.sp, + letterSpacing = 0.sp + ), + // Body text (16sp with 1.6 line height = 25.6sp) + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 26.sp, + letterSpacing = 0.sp + ), + // Small/Note text (14sp) + bodySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 21.sp, + letterSpacing = 0.sp + ), + // Labels (16sp, medium weight) + labelLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp + ), + // Button text - Burrito button (18sp) + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 18.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp + ), + // Button text - Primary/Logout (16sp/14sp) + titleMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp + ), + titleSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.sp + ) +) +``` + +--- + +## app/src/main/java/com/example/posthog/viewmodel/AuthViewModel.kt + +```kt +package com.example.posthog.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.example.posthog.data.User +import com.example.posthog.data.UserRepository +import com.posthog.PostHog +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class AuthViewModel(application: Application) : AndroidViewModel(application) { + + private val repository = UserRepository(application) + + private val _currentUser = MutableStateFlow(null) + val currentUser: StateFlow = _currentUser.asStateFlow() + + private val _isAuthenticated = MutableStateFlow(false) + val isAuthenticated: StateFlow = _isAuthenticated.asStateFlow() + + init { + loadCurrentUser() + } + + private fun loadCurrentUser() { + viewModelScope.launch { + val user = repository.getCurrentUser() + _currentUser.value = user + _isAuthenticated.value = user != null + } + } + + fun login(username: String) { + viewModelScope.launch { + val existingUser = repository.getUser(username) + val user = existingUser ?: User(username = username, burritoConsiderations = 0) + repository.saveUser(user) + _currentUser.value = user + _isAuthenticated.value = true + + PostHog.identify(username) + PostHog.capture(event = "user_logged_in") + } + } + + fun logout() { + viewModelScope.launch { + PostHog.capture("user_logged_out") + PostHog.reset() + repository.clearCurrentUser() + _currentUser.value = null + _isAuthenticated.value = false + } + } + + fun incrementBurritoCount() { + viewModelScope.launch { + val user = _currentUser.value ?: return@launch + val updatedUser = user.copy(burritoConsiderations = user.burritoConsiderations + 1) + repository.saveUser(updatedUser) + _currentUser.value = updatedUser + + PostHog.capture( + event = "burrito_considered", + properties = mapOf( + "total_considerations" to updatedUser.burritoConsiderations, + "username" to updatedUser.username + ) + ) + } + } +} + +``` + +--- + +## app/src/main/res/drawable/ic_launcher_background.xml + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +## app/src/main/res/drawable/ic_launcher_foreground.xml + +```xml + + + + + + + + + + + +``` + +--- + +## app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml + +```xml + + + + + + +``` + +--- + +## app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml + +```xml + + + + + + +``` + +--- + +## app/src/main/res/values/colors.xml + +```xml + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + +``` + +--- + +## app/src/main/res/values/strings.xml + +```xml + + PostHog + +``` + +--- + +## app/src/main/res/values/themes.xml + +```xml + + + +