<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][CHANGELOG.md]" value="# shopping-list-react-native

## 1.0.2

### Patch Changes

- Updated dependencies [[`cde8af1`](https://github.com/TanStack/db/commit/cde8af1f21308e5206601f10e9aa0a7c258f66dc), [`3fe689a`](https://github.com/TanStack/db/commit/3fe689a4444d53a075a0dbe6e2649f8852137fc8), [`c314c36`](https://github.com/TanStack/db/commit/c314c36b8bd02f8be86865c13f31f817ce21dc66), [`6428fe3`](https://github.com/TanStack/db/commit/6428fe3c4dae5e126bf12ce50c0467b2f1cb6f69)]:
  - @tanstack/electric-db-collection@0.3.0
  - @tanstack/db@0.6.2
  - @tanstack/react-db@0.1.80
  - @tanstack/offline-transactions@1.0.27
  - @tanstack/react-native-db-sqlite-persistence@0.1.6

## 1.0.1

### Patch Changes

- Updated dependencies [[`f60384b`](https://github.com/TanStack/db/commit/f60384b0fbde019865cbac5a7af341ff8a46d483), [`b8abc02`](https://github.com/TanStack/db/commit/b8abc0230096900746f92c51496489460b4d75e1), [`09c7afc`](https://github.com/TanStack/db/commit/09c7afc47a5ef3f3415ae601b6b00155ab64650b), [`bb09eb1`](https://github.com/TanStack/db/commit/bb09eb1eecbf680bb95a0bb08639f337e9982043), [`179d666`](https://github.com/TanStack/db/commit/179d66685449bcdf9f785c8765bc57cc19c2f7bd), [`43ecbfa`](https://github.com/TanStack/db/commit/43ecbfae5be5e59ffdce6c545d90ca5a810159e6), [`055fd94`](https://github.com/TanStack/db/commit/055fd94bd4654d27d5366af12a90da4c0e670fc0), [`055fd94`](https://github.com/TanStack/db/commit/055fd94bd4654d27d5366af12a90da4c0e670fc0), [`055fd94`](https://github.com/TanStack/db/commit/055fd94bd4654d27d5366af12a90da4c0e670fc0), [`055fd94`](https://github.com/TanStack/db/commit/055fd94bd4654d27d5366af12a90da4c0e670fc0), [`85f5435`](https://github.com/TanStack/db/commit/85f54355a426baefc88ccc55179e0cfcb4dac168), [`b65d8f7`](https://github.com/TanStack/db/commit/b65d8f767dafb1aeede26766c644f9ef0694f20c), [`e0df07e`](https://github.com/TanStack/db/commit/e0df07e1eb2eefbc829407f337cee1d443a7e9b6), [`9952921`](https://github.com/TanStack/db/commit/9952921e02ed8bca5653f0afa64862fc22ffbf9d), [`d351c67`](https://github.com/TanStack/db/commit/d351c677d687e667450138f66ab3bd0e11e7e347), [`1654c41`](https://github.com/TanStack/db/commit/1654c41759e1caabcd5ddea8a433f0634be60f86)]:
  - @tanstack/db@0.6.0
  - @tanstack/offline-transactions@1.0.25
  - @tanstack/electric-db-collection@0.2.42
  - @tanstack/react-native-db-sqlite-persistence@0.1.1
  - @tanstack/react-db@0.1.78
">
<input type="hidden" name="project[files][README.md]" value="# React Native Shopping List (Electric + Persistence + Offline Queue)

This example uses:

- `electricCollectionOptions` for realtime sync from Electric shape streams
- `persistedCollectionOptions` with React Native SQLite persistence
- `@tanstack/offline-transactions` for queued optimistic mutations and retry
- A local Express + Postgres API that returns `txid` values for Electric mutation matching
- Dedicated API shape proxy endpoints (`/api/shapes/*`) so Electric is not exposed directly to clients
- In-app `Simulate offline` toggle to demo offline queue + persistence behavior without disabling device network

## Run

From `examples/react-native/shopping-list`:

1. Start Docker Desktop (required for Postgres + Electric).
2. Start Postgres + Electric:
   - `pnpm db:up`
3. Start the API server in a separate terminal:
   - `pnpm server`
4. Start Expo in another terminal:
   - `pnpm start`
5. Launch iOS simulator:
   - `open -a Simulator`
   - then press `i` in the Expo terminal (or run `pnpm ios`)
6. Launch Android emulator:
   - start an AVD from Android Studio Device Manager
   - then press `a` in the Expo terminal (or run `pnpm android`)

## Troubleshooting

- If the server exits at startup, ensure Docker services are running and re-run `pnpm db:up`.
- Android emulator uses `10.0.2.2` for local host mapping.
- iOS simulator uses `localhost`.

## Verification checklist

- Add list and items while online: changes should sync and persist.
- Restart app: local data should load from SQLite immediately.
- Restart API/Electric: app should recover and continue syncing.
- Confirm there are no `Date value out of bounds` errors in shape sync logs.
">
<input type="hidden" name="project[files][app.json]" value="{
  &quot;expo&quot;: {
    &quot;name&quot;: &quot;Shopping List Demo&quot;,
    &quot;slug&quot;: &quot;shopping-list-demo&quot;,
    &quot;version&quot;: &quot;1.0.0&quot;,
    &quot;orientation&quot;: &quot;portrait&quot;,
    &quot;userInterfaceStyle&quot;: &quot;light&quot;,
    &quot;newArchEnabled&quot;: true,
    &quot;ios&quot;: {
      &quot;supportsTablet&quot;: true,
      &quot;bundleIdentifier&quot;: &quot;com.tanstack.shoppinglist&quot;
    },
    &quot;android&quot;: {
      &quot;package&quot;: &quot;com.tanstack.shoppinglist&quot;
    },
    &quot;scheme&quot;: &quot;shopping-list&quot;,
    &quot;plugins&quot;: [&quot;expo-router&quot;]
  }
}
">
<input type="hidden" name="project[files][babel.config.js]" value="module.exports = function (api) {
  api.cache(true)
  return {
    presets: [&#39;babel-preset-expo&#39;],
  }
}
">
<input type="hidden" name="project[files][docker-compose.yml]" value="version: &#39;3.8&#39;
services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: shopping_list
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - &#39;54322:5432&#39;
    volumes:
      - ./postgres.conf:/etc/postgresql/postgresql.conf:ro
    tmpfs:
      - /var/lib/postgresql/data
      - /tmp
    command:
      - postgres
      - -c
      - config_file=/etc/postgresql/postgresql.conf
    healthcheck:
      test: [&#39;CMD-SHELL&#39;, &#39;pg_isready -U postgres&#39;]
      interval: 5s
      timeout: 5s
      retries: 5

  electric:
    image: electricsql/electric:canary
    environment:
      DATABASE_URL: postgresql://postgres:postgres@postgres:5432/shopping_list?sslmode=disable
      ELECTRIC_INSECURE: true
    ports:
      - &#39;3003:3000&#39;
    depends_on:
      postgres:
        condition: service_healthy
">
<input type="hidden" name="project[files][metro.config.js]" value="const { getDefaultConfig } = require(&#39;expo/metro-config&#39;)
const path = require(&#39;path&#39;)

const projectRoot = __dirname
const monorepoRoot = path.resolve(projectRoot, &#39;../../..&#39;)

const config = getDefaultConfig(projectRoot)

// Watch all files in the monorepo
config.watchFolders = [monorepoRoot]

// Ensure symlinks are followed (important for pnpm)
config.resolver.unstable_enableSymlinks = true
config.resolver.unstable_enablePackageExports = true
config.resolver.unstable_conditionNames = [&#39;react-native&#39;]

const localNodeModules = path.resolve(projectRoot, &#39;node_modules&#39;)

// Singleton packages that must resolve to exactly one copy.
// In a pnpm monorepo, workspace packages may resolve these to a different
// version in the .pnpm store. This custom resolveRequest forces every import
// of these packages (from anywhere) to the app&#39;s local node_modules copy.
const singletonPackages = [&#39;react&#39;, &#39;react-native&#39;]
const singletonPaths = {}
for (const pkg of singletonPackages) {
  singletonPaths[pkg] = path.resolve(localNodeModules, pkg)
}

const defaultResolveRequest = config.resolver.resolveRequest
config.resolver.resolveRequest = (context, moduleName, platform) =&gt; {
  // Force singleton packages to resolve from the app&#39;s local node_modules,
  // regardless of where the import originates. This prevents workspace
  // packages (e.g. react-db) from pulling in their own copy of React.
  for (const pkg of singletonPackages) {
    if (moduleName === pkg || moduleName.startsWith(pkg + &#39;/&#39;)) {
      try {
        const filePath = require.resolve(moduleName, {
          paths: [projectRoot],
        })
        return { type: &#39;sourceFile&#39;, filePath }
      } catch {}
    }
  }

  if (defaultResolveRequest) {
    return defaultResolveRequest(context, moduleName, platform)
  }
  return context.resolveRequest(
    { ...context, resolveRequest: undefined },
    moduleName,
    platform,
  )
}

// Only singleton packages need explicit remapping. Let Metro resolve all other
// transitive dependencies from the package that requested them.
config.resolver.extraNodeModules = singletonPaths

// Block react-native 0.83 from root node_modules
const escMonorepoRoot = monorepoRoot.replace(/[.*+?^${}()|[\]\\]/g, &#39;\\$&amp;&#39;)
config.resolver.blockList = [
  new RegExp(`${escMonorepoRoot}/node_modules/\\.pnpm/react-native@0\\.83.*`),
]

// Let Metro know where to resolve packages from (local first, then root)
config.resolver.nodeModulesPaths = [
  localNodeModules,
  path.resolve(monorepoRoot, &#39;node_modules&#39;),
]

// Allow dynamic imports with non-literal arguments (used by workspace packages
// for optional Node.js-only code paths that are never reached on React Native)
config.transformer.dynamicDepsInPackages = &#39;throwAtRuntime&#39;

module.exports = config
">
<input type="hidden" name="project[files][package.json]" value="{
  &quot;name&quot;: &quot;shopping-list-react-native&quot;,
  &quot;version&quot;: &quot;1.0.2&quot;,
  &quot;private&quot;: true,
  &quot;main&quot;: &quot;expo-router/entry&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;expo start&quot;,
    &quot;ios&quot;: &quot;expo run:ios&quot;,
    &quot;android&quot;: &quot;expo run:android&quot;,
    &quot;reset-cache&quot;: &quot;expo start --clear&quot;,
    &quot;server&quot;: &quot;npx tsx server/index.ts&quot;,
    &quot;db:up&quot;: &quot;docker compose up -d&quot;,
    &quot;db:down&quot;: &quot;docker compose down&quot;
  },
  &quot;dependencies&quot;: {
    &quot;@electric-sql/client&quot;: &quot;^1.5.15&quot;,
    &quot;@expo/metro-runtime&quot;: &quot;~5.0.5&quot;,
    &quot;@op-engineering/op-sqlite&quot;: &quot;^15.2.5&quot;,
    &quot;@react-native-async-storage/async-storage&quot;: &quot;2.1.2&quot;,
    &quot;@react-native-community/netinfo&quot;: &quot;11.4.1&quot;,
    &quot;@tanstack/db&quot;: &quot;https://pkg.pr.new/TanStack/db/@tanstack/db@50332f1&quot;,
    &quot;@tanstack/electric-db-collection&quot;: &quot;https://pkg.pr.new/TanStack/db/@tanstack/electric-db-collection@50332f1&quot;,
    &quot;@tanstack/offline-transactions&quot;: &quot;https://pkg.pr.new/TanStack/db/@tanstack/offline-transactions@50332f1&quot;,
    &quot;@tanstack/react-db&quot;: &quot;https://pkg.pr.new/TanStack/db/@tanstack/react-db@50332f1&quot;,
    &quot;@tanstack/react-native-db-sqlite-persistence&quot;: &quot;https://pkg.pr.new/TanStack/db/@tanstack/react-native-db-sqlite-persistence@50332f1&quot;,
    &quot;@tanstack/react-query&quot;: &quot;^5.90.20&quot;,
    &quot;expo&quot;: &quot;~53.0.26&quot;,
    &quot;expo-constants&quot;: &quot;~17.1.0&quot;,
    &quot;expo-linking&quot;: &quot;~7.1.0&quot;,
    &quot;expo-router&quot;: &quot;~5.1.11&quot;,
    &quot;expo-status-bar&quot;: &quot;~2.2.0&quot;,
    &quot;metro&quot;: &quot;0.82.5&quot;,
    &quot;metro-cache&quot;: &quot;0.82.5&quot;,
    &quot;react&quot;: &quot;19.0.0&quot;,
    &quot;react-native&quot;: &quot;0.79.6&quot;,
    &quot;react-native-safe-area-context&quot;: &quot;5.4.0&quot;,
    &quot;react-native-screens&quot;: &quot;~4.11.1&quot;,
    &quot;use-latest-callback&quot;: &quot;^0.3.3&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;@babel/core&quot;: &quot;^7.29.0&quot;,
    &quot;@types/cors&quot;: &quot;^2.8.19&quot;,
    &quot;@types/express&quot;: &quot;^5.0.6&quot;,
    &quot;@types/react&quot;: &quot;^19.2.13&quot;,
    &quot;cors&quot;: &quot;^2.8.6&quot;,
    &quot;express&quot;: &quot;^5.2.1&quot;,
    &quot;postgres&quot;: &quot;^3.4.8&quot;,
    &quot;tsx&quot;: &quot;^4.21.0&quot;,
    &quot;typescript&quot;: &quot;^5.9.2&quot;
  }
}">
<input type="hidden" name="project[files][postgres.conf]" value="listen_addresses = &#39;*&#39;
max_connections = 100
shared_buffers = 128MB
dynamic_shared_memory_type = posix
max_wal_size = 1GB
min_wal_size = 80MB
log_timezone = &#39;UTC&#39;
datestyle = &#39;iso, mdy&#39;
timezone = &#39;UTC&#39;
lc_messages = &#39;en_US.utf8&#39;
lc_monetary = &#39;en_US.utf8&#39;
lc_numeric = &#39;en_US.utf8&#39;
lc_time = &#39;en_US.utf8&#39;
default_text_search_config = &#39;pg_catalog.english&#39;
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;extends&quot;: &quot;expo/tsconfig.base&quot;,
  &quot;compilerOptions&quot;: {
    &quot;strict&quot;: true,
    &quot;paths&quot;: {
      &quot;~/*&quot;: [&quot;./src/*&quot;]
    }
  },
  &quot;include&quot;: [&quot;**/*.ts&quot;, &quot;**/*.tsx&quot;]
}
">
<input type="hidden" name="project[files][android/.gitignore]" value="# OSX
#
.DS_Store

# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/

# Bundle artifacts
*.jsbundle
">
<input type="hidden" name="project[files][android/build.gradle]" value="// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
  repositories {
    google()
    mavenCentral()
  }
  dependencies {
    classpath(&#39;com.android.tools.build:gradle&#39;)
    classpath(&#39;com.facebook.react:react-native-gradle-plugin&#39;)
    classpath(&#39;org.jetbrains.kotlin:kotlin-gradle-plugin&#39;)
  }
}

def reactNativeAndroidDir = new File(
  providers.exec {
    workingDir(rootDir)
    commandLine(&quot;node&quot;, &quot;--print&quot;, &quot;require.resolve(&#39;react-native/package.json&#39;)&quot;)
  }.standardOutput.asText.get().trim(),
  &quot;../android&quot;
)

allprojects {
  repositories {
    maven {
      // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
      url(reactNativeAndroidDir)
    }

    google()
    mavenCentral()
    maven { url &#39;https://www.jitpack.io&#39; }
  }
}

apply plugin: &quot;expo-root-project&quot;
apply plugin: &quot;com.facebook.react.rootproject&quot;
">
<input type="hidden" name="project[files][android/gradle.properties]" value="# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app&#39;s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true

# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true

# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew &lt;task&gt; -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64

# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=true

# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true

# Enable GIF support in React Native images (~200 B increase)
expo.gif.enabled=true
# Enable webp support in React Native images (~85 KB increase)
expo.webp.enabled=true
# Enable animated webp support (~3.4 MB increase)
# Disabled by default because iOS doesn&#39;t support animated webp
expo.webp.animated=false

# Enable network inspector
EX_DEV_CLIENT_NETWORK_INSPECTOR=true

# Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false

# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
expo.edgeToEdgeEnabled=false">
<input type="hidden" name="project[files][android/gradlew]" value="#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The &quot;traditional&quot; practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in &quot;$@&quot;, and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%&quot;${app_path##*/}&quot;}  # leaves a trailing /; empty if no leading path
    [ -h &quot;$app_path&quot; ]
do
    ls=$( ls -ld &quot;$app_path&quot; )
    link=${ls#*&#39; -&gt; &#39;}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P &quot;${APP_HOME:-./}&quot; &gt; /dev/null &amp;&amp; printf &#39;%s\n&#39; &quot;$PWD&quot; ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo &quot;$*&quot;
} &gt;&amp;2

die () {
    echo
    echo &quot;$*&quot;
    echo
    exit 1
} &gt;&amp;2

# OS specific support (must be &#39;true&#39; or &#39;false&#39;).
cygwin=false
msys=false
darwin=false
nonstop=false
case &quot;$( uname )&quot; in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n &quot;$JAVA_HOME&quot; ] ; then
    if [ -x &quot;$JAVA_HOME/jre/sh/java&quot; ] ; then
        # IBM&#39;s JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x &quot;$JAVACMD&quot; ] ; then
        die &quot;ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation.&quot;
    fi
else
    JAVACMD=java
    if ! command -v java &gt;/dev/null 2&gt;&amp;1
    then
        die &quot;ERROR: JAVA_HOME is not set and no &#39;java&#39; command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation.&quot;
    fi
fi

# Increase the maximum file descriptors if we can.
if ! &quot;$cygwin&quot; &amp;&amp; ! &quot;$darwin&quot; &amp;&amp; ! &quot;$nonstop&quot; ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That&#39;s why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn &quot;Could not query maximum file descriptor limit&quot;
    esac
    case $MAX_FD in  #(
      &#39;&#39; | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That&#39;s why the result is checked to see if it worked.
        # shellcheck disable=SC2039,SC3045
        ulimit -n &quot;$MAX_FD&quot; ||
            warn &quot;Could not set maximum file descriptor limit to $MAX_FD&quot;
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if &quot;$cygwin&quot; || &quot;$msys&quot; ; then
    APP_HOME=$( cygpath --path --mixed &quot;$APP_HOME&quot; )
    CLASSPATH=$( cygpath --path --mixed &quot;$CLASSPATH&quot; )

    JAVACMD=$( cygpath --unix &quot;$JAVACMD&quot; )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don&#39;t mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e &quot;$t&quot; ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed &quot;$arg&quot; )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- &quot;$@&quot; &quot;$arg&quot;      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=&#39;&quot;-Xmx64m&quot; &quot;-Xms64m&quot;&#39;

# Collect all arguments for the java command:
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
#     and any embedded shellness will be escaped.
#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
#     treated as &#39;${Hostname}&#39; itself on the command line.

set -- \
        &quot;-Dorg.gradle.appname=$APP_BASE_NAME&quot; \
        -classpath &quot;$CLASSPATH&quot; \
        org.gradle.wrapper.GradleWrapperMain \
        &quot;$@&quot;

# Stop when &quot;xargs&quot; is not available.
if ! command -v xargs &gt;/dev/null 2&gt;&amp;1
then
    die &quot;xargs is not available&quot;
fi

# Use &quot;xargs&quot; to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS &lt; &lt;( xargs -n1 &lt;&lt;&lt;&quot;$var&quot; ) &amp;&amp;
#   set -- &quot;${ARGS[@]}&quot; &quot;$@&quot;
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single &quot;set&quot; statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval &quot;set -- $(
        printf &#39;%s\n&#39; &quot;$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS&quot; |
        xargs -n1 |
        sed &#39; s~[^-[:alnum:]+,./:=@_]~\\&amp;~g; &#39; |
        tr &#39;\n&#39; &#39; &#39;
    )&quot; &#39;&quot;$@&quot;&#39;

exec &quot;$JAVACMD&quot; &quot;$@&quot;
">
<input type="hidden" name="project[files][android/gradlew.bat]" value="@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem

@if &quot;%DEBUG%&quot;==&quot;&quot; @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if &quot;%OS%&quot;==&quot;Windows_NT&quot; setlocal

set DIRNAME=%~dp0
if &quot;%DIRNAME%&quot;==&quot;&quot; set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any &quot;.&quot; and &quot;..&quot; in APP_HOME to make it shorter.
for %%i in (&quot;%APP_HOME%&quot;) do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=&quot;-Xmx64m&quot; &quot;-Xms64m&quot;

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version &gt;NUL 2&gt;&amp;1
if %ERRORLEVEL% equ 0 goto execute

echo. 1&gt;&amp;2
echo ERROR: JAVA_HOME is not set and no &#39;java&#39; command could be found in your PATH. 1&gt;&amp;2
echo. 1&gt;&amp;2
echo Please set the JAVA_HOME variable in your environment to match the 1&gt;&amp;2
echo location of your Java installation. 1&gt;&amp;2

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:&quot;=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist &quot;%JAVA_EXE%&quot; goto execute

echo. 1&gt;&amp;2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1&gt;&amp;2
echo. 1&gt;&amp;2
echo Please set the JAVA_HOME variable in your environment to match the 1&gt;&amp;2
echo location of your Java installation. 1&gt;&amp;2

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
&quot;%JAVA_EXE%&quot; %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% &quot;-Dorg.gradle.appname=%APP_BASE_NAME%&quot; -classpath &quot;%CLASSPATH%&quot; org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not &quot;&quot;==&quot;%GRADLE_EXIT_CONSOLE%&quot; exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if &quot;%OS%&quot;==&quot;Windows_NT&quot; endlocal

:omega
">
<input type="hidden" name="project[files][android/settings.gradle]" value="pluginManagement {
  def reactNativeGradlePlugin = new File(
    providers.exec {
      workingDir(rootDir)
      commandLine(&quot;node&quot;, &quot;--print&quot;, &quot;require.resolve(&#39;@react-native/gradle-plugin/package.json&#39;, { paths: [require.resolve(&#39;react-native/package.json&#39;)] })&quot;)
    }.standardOutput.asText.get().trim()
  ).getParentFile().absolutePath
  includeBuild(reactNativeGradlePlugin)
  
  def expoPluginsPath = new File(
    providers.exec {
      workingDir(rootDir)
      commandLine(&quot;node&quot;, &quot;--print&quot;, &quot;require.resolve(&#39;expo-modules-autolinking/package.json&#39;, { paths: [require.resolve(&#39;expo/package.json&#39;)] })&quot;)
    }.standardOutput.asText.get().trim(),
    &quot;../android/expo-gradle-plugin&quot;
  ).absolutePath
  includeBuild(expoPluginsPath)
}

plugins {
  id(&quot;com.facebook.react.settings&quot;)
  id(&quot;expo-autolinking-settings&quot;)
}

extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -&gt;
  if (System.getenv(&#39;EXPO_USE_COMMUNITY_AUTOLINKING&#39;) == &#39;1&#39;) {
    ex.autolinkLibrariesFromCommand()
  } else {
    ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
  }
}
expoAutolinking.useExpoModules()

rootProject.name = &#39;Shopping List Demo&#39;

expoAutolinking.useExpoVersionCatalog()

include &#39;:app&#39;
includeBuild(expoAutolinking.reactNativeGradlePlugin)
">
<input type="hidden" name="project[files][app/_layout.tsx]" value="// Must be first import to polyfill crypto before anything else loads
import &#39;../src/polyfills&#39;

import React, { useCallback, useState } from &#39;react&#39;
import { Stack } from &#39;expo-router&#39;
import { QueryClientProvider } from &#39;@tanstack/react-query&#39;
import { SafeAreaProvider } from &#39;react-native-safe-area-context&#39;
import { StatusBar } from &#39;expo-status-bar&#39;
import {
  Alert,
  Modal,
  Platform,
  Pressable,
  StyleSheet,
  Text,
  View,
} from &#39;react-native&#39;
import { queryClient } from &#39;../src/utils/queryClient&#39;
import { ShoppingProvider, useShopping } from &#39;../src/db/ShoppingContext&#39;

function HeaderControls({ onAppRefresh }: { onAppRefresh: () =&gt; void }) {
  const {
    isOnline,
    isSimulatedOffline,
    setSimulateOffline,
    clearLocalState,
    pendingCount,
  } = useShopping()
  const [menuVisible, setMenuVisible] = useState(false)
  const [isClearingState, setIsClearingState] = useState(false)

  const closeMenu = useCallback(() =&gt; {
    setMenuVisible(false)
  }, [])

  const toggleSimulatedOffline = useCallback(() =&gt; {
    setMenuVisible(false)
    void setSimulateOffline(!isSimulatedOffline)
  }, [isSimulatedOffline, setSimulateOffline])

  const clearAndRefresh = useCallback(() =&gt; {
    setMenuVisible(false)
    // Delay the alert one tick so iOS can fully dismiss the modal first.
    setTimeout(() =&gt; {
      Alert.alert(
        `Clear local state`,
        `This clears local SQLite data and queued offline transactions, then refreshes the app.`,
        [
          { text: `Cancel`, style: `cancel` },
          {
            text: `Clear`,
            style: `destructive`,
            onPress: () =&gt; {
              if (isClearingState) return
              setIsClearingState(true)
              void (async () =&gt; {
                try {
                  await clearLocalState()
                  onAppRefresh()
                } catch (error) {
                  console.error(`[Shopping] Failed to clear local state`, error)
                  Alert.alert(
                    `Clear failed`,
                    error instanceof Error ? error.message : `Unknown error`,
                  )
                } finally {
                  setIsClearingState(false)
                }
              })()
            },
          },
        ],
      )
    }, 0)
  }, [clearLocalState, isClearingState, onAppRefresh])

  const statusLabel = isOnline
    ? `Online`
    : isSimulatedOffline
      ? `Offline (sim)`
      : `Offline`

  return (
    &lt;View style={{ flexDirection: `row`, alignItems: `center`, gap: 8 }}&gt;
      &lt;View
        style={{
          backgroundColor: isOnline ? `#dcfce7` : `#fee2e2`,
          paddingHorizontal: 8,
          paddingVertical: 4,
          borderRadius: 999,
        }}
      &gt;
        &lt;Text
          style={{
            color: isOnline ? `#166534` : `#991b1b`,
            fontSize: 11,
            fontWeight: `600`,
          }}
        &gt;
          {statusLabel}
          {pendingCount &gt; 0 ? ` · ${pendingCount} pending` : ``}
        &lt;/Text&gt;
      &lt;/View&gt;
      &lt;Pressable
        onPress={() =&gt; setMenuVisible(true)}
        style={{
          backgroundColor: `#e5e7eb`,
          paddingHorizontal: 8,
          paddingVertical: 4,
          borderRadius: 8,
        }}
        accessibilityLabel=&quot;Open demo menu&quot;
      &gt;
        &lt;Text style={{ fontSize: 16, fontWeight: `700`, color: `#111827` }}&gt;
          ☰
        &lt;/Text&gt;
      &lt;/Pressable&gt;
      &lt;Modal
        transparent
        visible={menuVisible}
        animationType=&quot;fade&quot;
        onRequestClose={closeMenu}
      &gt;
        &lt;View style={styles.modalRoot}&gt;
          &lt;Pressable style={styles.backdrop} onPress={closeMenu} /&gt;
          &lt;View style={styles.menuAnchor}&gt;
            &lt;View style={styles.menuCard}&gt;
              &lt;Pressable
                style={styles.menuItem}
                onPress={toggleSimulatedOffline}
              &gt;
                &lt;Text style={styles.menuText}&gt;
                  {isSimulatedOffline
                    ? `Disable simulated offline mode`
                    : `Enable simulated offline mode`}
                &lt;/Text&gt;
              &lt;/Pressable&gt;
              &lt;View style={styles.menuDivider} /&gt;
              &lt;Pressable style={styles.menuItem} onPress={clearAndRefresh}&gt;
                &lt;Text style={[styles.menuText, styles.menuTextDanger]}&gt;
                  Clear local state
                &lt;/Text&gt;
              &lt;/Pressable&gt;
            &lt;/View&gt;
          &lt;/View&gt;
        &lt;/View&gt;
      &lt;/Modal&gt;
    &lt;/View&gt;
  )
}

export default function RootLayout() {
  const [refreshKey, setRefreshKey] = useState(0)
  const refreshApp = useCallback(() =&gt; {
    queryClient.clear()
    setRefreshKey((current) =&gt; current + 1)
  }, [])

  return (
    &lt;SafeAreaProvider&gt;
      &lt;QueryClientProvider client={queryClient} key={refreshKey}&gt;
        &lt;ShoppingProvider key={refreshKey}&gt;
          &lt;StatusBar style=&quot;dark&quot; translucent={false} /&gt;
          &lt;Stack
            screenOptions={{
              ...(Platform.OS === `android`
                ? {
                    statusBarTranslucent: false,
                    statusBarStyle: `dark` as const,
                  }
                : {}),
              headerRight: () =&gt; &lt;HeaderControls onAppRefresh={refreshApp} /&gt;,
            }}
          &gt;
            &lt;Stack.Screen name=&quot;index&quot; options={{ title: `Shopping Lists` }} /&gt;
            &lt;Stack.Screen name=&quot;list/[id]&quot; options={{ title: `List` }} /&gt;
          &lt;/Stack&gt;
        &lt;/ShoppingProvider&gt;
      &lt;/QueryClientProvider&gt;
    &lt;/SafeAreaProvider&gt;
  )
}

const styles = StyleSheet.create({
  modalRoot: {
    flex: 1,
  },
  backdrop: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: `rgba(0, 0, 0, 0.12)`,
  },
  menuAnchor: {
    flex: 1,
    alignItems: `flex-end`,
    paddingTop: 70,
    paddingRight: 12,
  },
  menuCard: {
    width: 260,
    backgroundColor: `#fff`,
    borderRadius: 12,
    overflow: `hidden`,
    borderWidth: StyleSheet.hairlineWidth,
    borderColor: `#d1d5db`,
  },
  menuItem: {
    paddingHorizontal: 14,
    paddingVertical: 12,
  },
  menuText: {
    fontSize: 14,
    color: `#111827`,
    fontWeight: `500`,
  },
  menuTextDanger: {
    color: `#b91c1c`,
  },
  menuDivider: {
    height: StyleSheet.hairlineWidth,
    backgroundColor: `#e5e7eb`,
  },
})
">
<input type="hidden" name="project[files][app/index.tsx]" value="import { SafeAreaView } from &#39;react-native-safe-area-context&#39;
import { ListsScreen } from &#39;../src/components/ListsScreen&#39;

export default function HomeScreen() {
  return (
    &lt;SafeAreaView style={{ flex: 1 }} edges={[&#39;bottom&#39;]}&gt;
      &lt;ListsScreen /&gt;
    &lt;/SafeAreaView&gt;
  )
}
">
<input type="hidden" name="project[files][server/index.ts]" value="import { Readable } from &#39;node:stream&#39;
import { pipeline } from &#39;node:stream/promises&#39;
import cors from &#39;cors&#39;
import express from &#39;express&#39;
import postgres from &#39;postgres&#39;
import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from &#39;@electric-sql/client&#39;

const app = express()
const PORT = 3001
const ELECTRIC_URL = process.env.ELECTRIC_URL ?? `http://localhost:3003`
const sql = postgres({
  host: `localhost`,
  port: 54322,
  user: `postgres`,
  password: `postgres`,
  database: `shopping_list`,
})

const HOP_BY_HOP_HEADERS = new Set([
  `connection`,
  `content-length`,
  `keep-alive`,
  `proxy-authenticate`,
  `proxy-authorization`,
  `te`,
  `trailer`,
  `transfer-encoding`,
  `upgrade`,
  `host`,
])

app.use(cors())
app.use(express.json())

interface ShoppingList {
  id: string
  name: string
  createdAt: string
}

interface ShoppingItem {
  id: string
  listId: string
  text: string
  checked: boolean
  createdAt: string
}

function asIso(value: unknown): string {
  if (value instanceof Date) return value.toISOString()
  return new Date(String(value)).toISOString()
}

function toShoppingList(row: {
  id: string
  name: string
  createdAt: unknown
}): ShoppingList {
  return {
    id: row.id,
    name: row.name,
    createdAt: asIso(row.createdAt),
  }
}

function toShoppingItem(row: {
  id: string
  listId: string
  text: string
  checked: boolean
  createdAt: unknown
}): ShoppingItem {
  return {
    id: row.id,
    listId: row.listId,
    text: row.text,
    checked: row.checked,
    createdAt: asIso(row.createdAt),
  }
}

function buildElectricShapeUrl(requestUrl: string, table: string): URL {
  const url = new URL(requestUrl)
  const originUrl = new URL(`/v1/shape`, ELECTRIC_URL)

  url.searchParams.forEach((value, key) =&gt; {
    if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) {
      originUrl.searchParams.set(key, value)
    }
  })

  originUrl.searchParams.set(`table`, table)

  if (process.env.ELECTRIC_SOURCE_ID) {
    originUrl.searchParams.set(`source_id`, process.env.ELECTRIC_SOURCE_ID)
  }
  const sourceSecret =
    process.env.ELECTRIC_SOURCE_SECRET ?? process.env.ELECTRIC_SECRET
  if (sourceSecret) {
    originUrl.searchParams.set(`secret`, sourceSecret)
  }

  return originUrl
}

function buildForwardHeaders(req: express.Request): Headers {
  const headers = new Headers()
  for (const [key, value] of Object.entries(req.headers)) {
    const lowerKey = key.toLowerCase()
    if (HOP_BY_HOP_HEADERS.has(lowerKey)) {
      continue
    }
    if (value === undefined) {
      continue
    }
    headers.set(key, Array.isArray(value) ? value.join(`,`) : value)
  }
  return headers
}

async function proxyElectricShape(
  req: express.Request,
  res: express.Response,
  table: string,
) {
  const requestUrl = `${req.protocol}://${req.get(`host`)}${req.originalUrl}`
  const originUrl = buildElectricShapeUrl(requestUrl, table)
  const forwardHeaders = buildForwardHeaders(req)
  const body =
    req.method === `POST` &amp;&amp; req.body !== undefined
      ? JSON.stringify(req.body)
      : undefined

  if (body &amp;&amp; !forwardHeaders.has(`content-type`)) {
    forwardHeaders.set(`content-type`, `application/json`)
  }

  const response = await fetch(originUrl, {
    method: req.method,
    headers: forwardHeaders,
    body,
  })

  response.headers.forEach((value, key) =&gt; {
    const lower = key.toLowerCase()
    if (
      lower === `content-encoding` ||
      lower === `content-length` ||
      lower === `transfer-encoding`
    ) {
      return
    }
    res.setHeader(key, value)
  })
  const varyHeader = response.headers.get(`vary`)
  res.setHeader(
    `Vary`,
    varyHeader ? `${varyHeader}, Authorization` : `Authorization`,
  )
  res.status(response.status)

  if (!response.body) {
    res.end()
    return
  }

  const nodeStream = Readable.fromWeb(response.body as any)
  res.on(`close`, () =&gt; nodeStream.destroy())
  await pipeline(nodeStream, res)
}

async function ensureDb() {
  await sql`
    CREATE TABLE IF NOT EXISTS shopping_lists (
      id text PRIMARY KEY,
      name text NOT NULL,
      &quot;createdAt&quot; timestamptz NOT NULL DEFAULT now()
    )
  `

  await sql`
    CREATE TABLE IF NOT EXISTS shopping_items (
      id text PRIMARY KEY,
      &quot;listId&quot; text NOT NULL REFERENCES shopping_lists(id) ON DELETE CASCADE,
      text text NOT NULL,
      checked boolean NOT NULL DEFAULT false,
      &quot;createdAt&quot; timestamptz NOT NULL DEFAULT now()
    )
  `

  const [{ count }] = await sql&lt;Array&lt;{ count: string }&gt;&gt;`
    SELECT count(*)::text as count FROM shopping_lists
  `
  if (Number.parseInt(count, 10) &gt; 0) {
    return
  }

  await sql`
    INSERT INTO shopping_lists (id, name)
    VALUES
      (&#39;list-grocery&#39;, &#39;Grocery&#39;),
      (&#39;list-hardware&#39;, &#39;Hardware Store&#39;)
    ON CONFLICT (id) DO NOTHING
  `

  await sql`
    INSERT INTO shopping_items (id, &quot;listId&quot;, text, checked)
    VALUES
      (&#39;item-milk&#39;, &#39;list-grocery&#39;, &#39;Milk&#39;, false),
      (&#39;item-eggs&#39;, &#39;list-grocery&#39;, &#39;Eggs&#39;, false),
      (&#39;item-bread&#39;, &#39;list-grocery&#39;, &#39;Bread&#39;, true),
      (&#39;item-screwdriver&#39;, &#39;list-hardware&#39;, &#39;Screwdriver&#39;, false),
      (&#39;item-nails&#39;, &#39;list-hardware&#39;, &#39;Nails&#39;, false)
    ON CONFLICT (id) DO NOTHING
  `
}

app.get(&#39;/api/shapes/lists&#39;, async (req, res) =&gt; {
  try {
    await proxyElectricShape(req, res, `shopping_lists`)
  } catch (error) {
    console.error(`Failed to proxy lists shape`, error)
    if (!res.headersSent) {
      res.status(502).json({ error: `Failed to proxy lists shape` })
    }
  }
})

app.post(&#39;/api/shapes/lists&#39;, async (req, res) =&gt; {
  try {
    await proxyElectricShape(req, res, `shopping_lists`)
  } catch (error) {
    console.error(`Failed to proxy lists shape`, error)
    if (!res.headersSent) {
      res.status(502).json({ error: `Failed to proxy lists shape` })
    }
  }
})

app.get(&#39;/api/shapes/items&#39;, async (req, res) =&gt; {
  try {
    await proxyElectricShape(req, res, `shopping_items`)
  } catch (error) {
    console.error(`Failed to proxy items shape`, error)
    if (!res.headersSent) {
      res.status(502).json({ error: `Failed to proxy items shape` })
    }
  }
})

app.post(&#39;/api/shapes/items&#39;, async (req, res) =&gt; {
  try {
    await proxyElectricShape(req, res, `shopping_items`)
  } catch (error) {
    console.error(`Failed to proxy items shape`, error)
    if (!res.headersSent) {
      res.status(502).json({ error: `Failed to proxy items shape` })
    }
  }
})

app.get(&#39;/api/lists&#39;, async (_req, res) =&gt; {
  const rows = await sql&lt;
    Array&lt;{
      id: string
      name: string
      createdAt: unknown
    }&gt;
  &gt;`
    SELECT id, name, &quot;createdAt&quot;
    FROM shopping_lists
    ORDER BY &quot;createdAt&quot; DESC
  `
  res.json(rows.map(toShoppingList))
})

app.post(&#39;/api/lists&#39;, async (req, res) =&gt; {
  const { id, name, createdAt } = req.body as {
    id?: string
    name?: string
    createdAt?: string
  }
  if (!name?.trim()) {
    return res.status(400).json({ error: `List name is required` })
  }

  const [inserted] = await sql&lt;
    Array&lt;{
      txid: string
      id: string
      name: string
      createdAt: unknown
    }&gt;
  &gt;`
    WITH tx AS (
      SELECT pg_current_xact_id()::xid::text as txid
    ),
    inserted AS (
      INSERT INTO shopping_lists (id, name, &quot;createdAt&quot;)
      VALUES (
        ${id ?? crypto.randomUUID()},
        ${name.trim()},
        COALESCE(${createdAt ?? null}, now())
      )
      ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name
      RETURNING id, name, &quot;createdAt&quot;
    )
    SELECT tx.txid, inserted.id, inserted.name, inserted.&quot;createdAt&quot;
    FROM tx, inserted
  `

  return res.status(201).json({
    list: toShoppingList(inserted),
    txid: Number.parseInt(inserted.txid, 10),
  })
})

app.put(&#39;/api/lists/:id&#39;, async (req, res) =&gt; {
  const { id } = req.params
  const { name } = req.body as { name?: string }
  if (name !== undefined &amp;&amp; !name.trim()) {
    return res.status(400).json({ error: `List name cannot be empty` })
  }

  const updatedRows = await sql`
    WITH tx AS (
      SELECT pg_current_xact_id()::xid::text as txid
    ),
    updated AS (
      UPDATE shopping_lists
      SET name = COALESCE(${name?.trim() ?? null}, name)
      WHERE id = ${id}
      RETURNING id, name, &quot;createdAt&quot;
    )
    SELECT tx.txid, updated.id, updated.name, updated.&quot;createdAt&quot;
    FROM tx, updated
  `
  const updated = updatedRows[0] as
    | { txid: string; id: string; name: string; createdAt: unknown }
    | undefined

  if (!updated) {
    return res.status(404).json({ error: `List not found` })
  }

  return res.json({
    list: toShoppingList(updated),
    txid: Number.parseInt(updated.txid, 10),
  })
})

app.delete(&#39;/api/lists/:id&#39;, async (req, res) =&gt; {
  const { id } = req.params

  const deletedRows = await sql`
    WITH tx AS (
      SELECT pg_current_xact_id()::xid::text as txid
    ),
    deleted AS (
      DELETE FROM shopping_lists
      WHERE id = ${id}
      RETURNING id
    )
    SELECT tx.txid, deleted.id
    FROM tx, deleted
  `
  const deleted = deletedRows[0] as { txid: string; id: string } | undefined

  if (!deleted) {
    return res.status(404).json({ error: `List not found` })
  }

  return res.json({ success: true, txid: Number.parseInt(deleted.txid, 10) })
})

app.get(&#39;/api/items&#39;, async (_req, res) =&gt; {
  const rows = await sql&lt;
    Array&lt;{
      id: string
      listId: string
      text: string
      checked: boolean
      createdAt: unknown
    }&gt;
  &gt;`
    SELECT id, &quot;listId&quot;, text, checked, &quot;createdAt&quot;
    FROM shopping_items
    ORDER BY &quot;createdAt&quot; ASC
  `
  res.json(rows.map(toShoppingItem))
})

app.post(&#39;/api/items&#39;, async (req, res) =&gt; {
  const { id, listId, text, checked, createdAt } = req.body as {
    id?: string
    listId?: string
    text?: string
    checked?: boolean
    createdAt?: string
  }

  if (!listId || !text?.trim()) {
    return res.status(400).json({ error: `listId and text are required` })
  }

  const [inserted] = await sql&lt;
    Array&lt;{
      txid: string
      id: string
      listId: string
      text: string
      checked: boolean
      createdAt: unknown
    }&gt;
  &gt;`
    WITH tx AS (
      SELECT pg_current_xact_id()::xid::text as txid
    ),
    inserted AS (
      INSERT INTO shopping_items (id, &quot;listId&quot;, text, checked, &quot;createdAt&quot;)
      VALUES (
        ${id ?? crypto.randomUUID()},
        ${listId},
        ${text.trim()},
        COALESCE(${checked ?? null}, false),
        COALESCE(${createdAt ?? null}, now())
      )
      ON CONFLICT (id) DO UPDATE SET
        text = EXCLUDED.text,
        checked = EXCLUDED.checked
      RETURNING id, &quot;listId&quot;, text, checked, &quot;createdAt&quot;
    )
    SELECT tx.txid, inserted.id, inserted.&quot;listId&quot;, inserted.text, inserted.checked, inserted.&quot;createdAt&quot;
    FROM tx, inserted
  `

  return res.status(201).json({
    item: toShoppingItem(inserted),
    txid: Number.parseInt(inserted.txid, 10),
  })
})

app.put(&#39;/api/items/:id&#39;, async (req, res) =&gt; {
  const { id } = req.params
  const { text, checked } = req.body as { text?: string; checked?: boolean }
  if (text !== undefined &amp;&amp; !text.trim()) {
    return res.status(400).json({ error: `Item text cannot be empty` })
  }

  const updatedRows = await sql`
    WITH tx AS (
      SELECT pg_current_xact_id()::xid::text as txid
    ),
    updated AS (
      UPDATE shopping_items
      SET
        text = COALESCE(${text?.trim() ?? null}, text),
        checked = COALESCE(${checked ?? null}, checked)
      WHERE id = ${id}
      RETURNING id, &quot;listId&quot;, text, checked, &quot;createdAt&quot;
    )
    SELECT tx.txid, updated.id, updated.&quot;listId&quot;, updated.text, updated.checked, updated.&quot;createdAt&quot;
    FROM tx, updated
  `
  const updated = updatedRows[0] as
    | {
        txid: string
        id: string
        listId: string
        text: string
        checked: boolean
        createdAt: unknown
      }
    | undefined

  if (!updated) {
    return res.status(404).json({ error: `Item not found` })
  }

  return res.json({
    item: toShoppingItem(updated),
    txid: Number.parseInt(updated.txid, 10),
  })
})

app.delete(&#39;/api/items/:id&#39;, async (req, res) =&gt; {
  const { id } = req.params

  const deletedRows = await sql`
    WITH tx AS (
      SELECT pg_current_xact_id()::xid::text as txid
    ),
    deleted AS (
      DELETE FROM shopping_items
      WHERE id = ${id}
      RETURNING id
    )
    SELECT tx.txid, deleted.id
    FROM tx, deleted
  `
  const deleted = deletedRows[0] as { txid: string; id: string } | undefined

  if (!deleted) {
    return res.status(404).json({ error: `Item not found` })
  }

  return res.json({ success: true, txid: Number.parseInt(deleted.txid, 10) })
})

async function start() {
  try {
    await ensureDb()

    app.listen(PORT, &#39;0.0.0.0&#39;, () =&gt; {
      console.log(`Server running at http://0.0.0.0:${PORT}`)
      console.log(`For Android emulator API: http://10.0.2.2:${PORT}`)
      console.log(`For iOS simulator API: http://localhost:${PORT}`)
      console.log(`Electric shape endpoint: http://localhost:3003/v1/shape`)
    })
  } catch (error) {
    console.error(`Failed to start shopping-list server`, error)
    console.error(
      `Did you run &#39;pnpm db:up&#39; in examples/react-native/shopping-list?`,
    )
    process.exit(1)
  }
}

void start()
">
<input type="hidden" name="project[files][src/polyfills.ts]" value="// Polyfill for crypto.randomUUID() which is not available in React Native Hermes
if (typeof global.crypto === &#39;undefined&#39;) {
  global.crypto = {} as Crypto
}

if (typeof global.crypto.randomUUID !== &#39;function&#39;) {
  global.crypto.randomUUID =
    function randomUUID(): `${string}-${string}-${string}-${string}-${string}` {
      // Simple UUID v4 implementation
      const hex = &#39;0123456789abcdef&#39;
      let uuid = &#39;&#39;
      for (let i = 0; i &lt; 36; i++) {
        if (i === 8 || i === 13 || i === 18 || i === 23) {
          uuid += &#39;-&#39;
        } else if (i === 14) {
          uuid += &#39;4&#39; // Version 4
        } else if (i === 19) {
          uuid += hex[(Math.random() * 4) | 8] // Variant bits
        } else {
          uuid += hex[(Math.random() * 16) | 0]
        }
      }
      return uuid as `${string}-${string}-${string}-${string}-${string}`
    }
}
">
<input type="hidden" name="project[files][android/app/build.gradle]" value="apply plugin: &quot;com.android.application&quot;
apply plugin: &quot;org.jetbrains.kotlin.android&quot;
apply plugin: &quot;com.facebook.react&quot;

def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()

/**
 * This is the configuration block to customize your React Native Android app.
 * By default you don&#39;t need to apply any configuration, just uncomment the lines you need.
 */
react {
    entryFile = file([&quot;node&quot;, &quot;-e&quot;, &quot;require(&#39;expo/scripts/resolveAppEntry&#39;)&quot;, projectRoot, &quot;android&quot;, &quot;absolute&quot;].execute(null, rootDir).text.trim())
    reactNativeDir = new File([&quot;node&quot;, &quot;--print&quot;, &quot;require.resolve(&#39;react-native/package.json&#39;)&quot;].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
    hermesCommand = new File([&quot;node&quot;, &quot;--print&quot;, &quot;require.resolve(&#39;react-native/package.json&#39;)&quot;].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + &quot;/sdks/hermesc/%OS-BIN%/hermesc&quot;
    codegenDir = new File([&quot;node&quot;, &quot;--print&quot;, &quot;require.resolve(&#39;@react-native/codegen/package.json&#39;, { paths: [require.resolve(&#39;react-native/package.json&#39;)] })&quot;].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()

    enableBundleCompression = (findProperty(&#39;android.enableBundleCompression&#39;) ?: false).toBoolean()
    // Use Expo CLI to bundle the app, this ensures the Metro config
    // works correctly with Expo projects.
    cliFile = new File([&quot;node&quot;, &quot;--print&quot;, &quot;require.resolve(&#39;@expo/cli&#39;, { paths: [require.resolve(&#39;expo/package.json&#39;)] })&quot;].execute(null, rootDir).text.trim())
    bundleCommand = &quot;export:embed&quot;

    /* Folders */
     //   The root of your project, i.e. where &quot;package.json&quot; lives. Default is &#39;../..&#39;
    // root = file(&quot;../../&quot;)
    //   The folder where the react-native NPM package is. Default is ../../node_modules/react-native
    // reactNativeDir = file(&quot;../../node_modules/react-native&quot;)
    //   The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
    // codegenDir = file(&quot;../../node_modules/@react-native/codegen&quot;)

    /* Variants */
    //   The list of variants to that are debuggable. For those we&#39;re going to
    //   skip the bundling of the JS bundle and the assets. By default is just &#39;debug&#39;.
    //   If you add flavors like lite, prod, etc. you&#39;ll have to list your debuggableVariants.
    // debuggableVariants = [&quot;liteDebug&quot;, &quot;prodDebug&quot;]

    /* Bundling */
    //   A list containing the node command and its flags. Default is just &#39;node&#39;.
    // nodeExecutableAndArgs = [&quot;node&quot;]

    //
    //   The path to the CLI configuration file. Default is empty.
    // bundleConfig = file(../rn-cli.config.js)
    //
    //   The name of the generated asset file containing your JS bundle
    // bundleAssetName = &quot;MyApplication.android.bundle&quot;
    //
    //   The entry file for bundle generation. Default is &#39;index.android.js&#39; or &#39;index.js&#39;
    // entryFile = file(&quot;../js/MyApplication.android.js&quot;)
    //
    //   A list of extra flags to pass to the &#39;bundle&#39; commands.
    //   See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
    // extraPackagerArgs = []

    /* Hermes Commands */
    //   The hermes compiler command to run. By default it is &#39;hermesc&#39;
    // hermesCommand = &quot;$rootDir/my-custom-hermesc/bin/hermesc&quot;
    //
    //   The list of flags to pass to the Hermes compiler. By default is &quot;-O&quot;, &quot;-output-source-map&quot;
    // hermesFlags = [&quot;-O&quot;, &quot;-output-source-map&quot;]

    /* Autolinking */
    autolinkLibrariesWithApp()
}

/**
 * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
 */
def enableProguardInReleaseBuilds = (findProperty(&#39;android.enableProguardInReleaseBuilds&#39;) ?: false).toBoolean()

/**
 * The preferred build flavor of JavaScriptCore (JSC)
 *
 * For example, to use the international variant, you can use:
 * `def jscFlavor = &#39;org.webkit:android-jsc-intl:+&#39;`
 *
 * The international variant includes ICU i18n library and necessary data
 * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
 * give correct results when using with locales other than en-US. Note that
 * this variant is about 6MiB larger per architecture than default.
 */
def jscFlavor = &#39;io.github.react-native-community:jsc-android:2026004.+&#39;

android {
    ndkVersion rootProject.ext.ndkVersion

    buildToolsVersion rootProject.ext.buildToolsVersion
    compileSdk rootProject.ext.compileSdkVersion

    namespace &#39;com.tanstack.shoppinglist&#39;
    defaultConfig {
        applicationId &#39;com.tanstack.shoppinglist&#39;
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName &quot;1.0.0&quot;
    }
    signingConfigs {
        debug {
            storeFile file(&#39;debug.keystore&#39;)
            storePassword &#39;android&#39;
            keyAlias &#39;androiddebugkey&#39;
            keyPassword &#39;android&#39;
        }
    }
    buildTypes {
        debug {
            signingConfig signingConfigs.debug
        }
        release {
            // Caution! In production, you need to generate your own keystore file.
            // see https://reactnative.dev/docs/signed-apk-android.
            signingConfig signingConfigs.debug
            shrinkResources (findProperty(&#39;android.enableShrinkResourcesInReleaseBuilds&#39;)?.toBoolean() ?: false)
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile(&quot;proguard-android.txt&quot;), &quot;proguard-rules.pro&quot;
            crunchPngs (findProperty(&#39;android.enablePngCrunchInReleaseBuilds&#39;)?.toBoolean() ?: true)
        }
    }
    packagingOptions {
        jniLibs {
            useLegacyPackaging (findProperty(&#39;expo.useLegacyPackaging&#39;)?.toBoolean() ?: false)
        }
    }
    androidResources {
        ignoreAssetsPattern &#39;!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~&#39;
    }
}

// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
[&quot;pickFirsts&quot;, &quot;excludes&quot;, &quot;merges&quot;, &quot;doNotStrip&quot;].each { prop -&gt;
    // Split option: &#39;foo,bar&#39; -&gt; [&#39;foo&#39;, &#39;bar&#39;]
    def options = (findProperty(&quot;android.packagingOptions.$prop&quot;) ?: &quot;&quot;).split(&quot;,&quot;);
    // Trim all elements in place.
    for (i in 0..&lt;options.size()) options[i] = options[i].trim();
    // `[] - &quot;&quot;` is essentially `[&quot;&quot;].filter(Boolean)` removing all empty strings.
    options -= &quot;&quot;

    if (options.length &gt; 0) {
        println &quot;android.packagingOptions.$prop += $options ($options.length)&quot;
        // Ex: android.packagingOptions.pickFirsts += &#39;**/SCCS/**&#39;
        options.each {
            android.packagingOptions[prop] += it
        }
    }
}

dependencies {
    // The version of react-native is set by the React Native Gradle Plugin
    implementation(&quot;com.facebook.react:react-android&quot;)

    def isGifEnabled = (findProperty(&#39;expo.gif.enabled&#39;) ?: &quot;&quot;) == &quot;true&quot;;
    def isWebpEnabled = (findProperty(&#39;expo.webp.enabled&#39;) ?: &quot;&quot;) == &quot;true&quot;;
    def isWebpAnimatedEnabled = (findProperty(&#39;expo.webp.animated&#39;) ?: &quot;&quot;) == &quot;true&quot;;

    if (isGifEnabled) {
        // For animated gif support
        implementation(&quot;com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}&quot;)
    }

    if (isWebpEnabled) {
        // For webp support
        implementation(&quot;com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}&quot;)
        if (isWebpAnimatedEnabled) {
            // Animated webp support
            implementation(&quot;com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}&quot;)
        }
    }

    if (hermesEnabled.toBoolean()) {
        implementation(&quot;com.facebook.react:hermes-android&quot;)
    } else {
        implementation jscFlavor
    }
}
">
<input type="hidden" name="project[files][android/app/debug.keystore]" value="https://pkg.pr.new/template/73495c1b-0d1c-4ecc-9709-fda71b332b40">
<input type="hidden" name="project[files][android/app/proguard-rules.pro]" value="# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }

# Add any project specific keep options here:
">
<input type="hidden" name="project[files][app/list/%5Bid%5D.tsx]" value="import { useLocalSearchParams, Stack } from &#39;expo-router&#39;
import { SafeAreaView } from &#39;react-native-safe-area-context&#39;
import { useLiveQuery } from &#39;@tanstack/react-db&#39;
import { eq } from &#39;@tanstack/react-db&#39;
import { listsCollection } from &#39;../../src/db/collections&#39;
import { ListDetail } from &#39;../../src/components/ListDetail&#39;

export default function ListScreen() {
  const { id } = useLocalSearchParams&lt;{ id: string }&gt;() as { id: string }

  // Get the list name for the header
  const listResult = useLiveQuery((q) =&gt;
    q
      .from({ list: listsCollection })
      .where(({ list }) =&gt; eq(list.id, id))
      .select(({ list }) =&gt; ({ id: list.id, name: list.name })),
  )
  const list = (listResult.data ?? [])[0] as
    | { id: string; name: string }
    | undefined

  return (
    &lt;&gt;
      &lt;Stack.Screen options={{ title: list?.name ?? `List` }} /&gt;
      &lt;SafeAreaView style={{ flex: 1 }} edges={[&#39;bottom&#39;]}&gt;
        &lt;ListDetail listId={id} /&gt;
      &lt;/SafeAreaView&gt;
    &lt;/&gt;
  )
}
">
<input type="hidden" name="project[files][src/components/ListDetail.tsx]" value="import React, { useEffect, useState } from &#39;react&#39;
import {
  FlatList,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from &#39;react-native&#39;
import { eq, useLiveQuery } from &#39;@tanstack/react-db&#39;
import { itemsCollection } from &#39;../db/collections&#39;
import { useShopping } from &#39;../db/ShoppingContext&#39;

interface ListDetailProps {
  listId: string
}

type ListItemRow = {
  id: string
  listId: string
  text: string
  checked: boolean
  createdAt: string
  $synced?: boolean
}

function ItemRow({
  item,
  onToggle,
  onDelete,
}: {
  item: ListItemRow
  onToggle: () =&gt; void
  onDelete: () =&gt; void
}) {
  const [showSavingBadge, setShowSavingBadge] = useState(false)

  useEffect(() =&gt; {
    if (item.$synced !== false) {
      setShowSavingBadge(false)
      return
    }

    setShowSavingBadge(false)
    const timer = setTimeout(() =&gt; {
      setShowSavingBadge(true)
    }, 200)
    return () =&gt; {
      clearTimeout(timer)
    }
  }, [item.id, item.$synced])

  return (
    &lt;View style={styles.itemRow}&gt;
      &lt;TouchableOpacity
        style={[styles.checkbox, item.checked &amp;&amp; styles.checkboxChecked]}
        onPress={onToggle}
      &gt;
        {item.checked &amp;&amp; &lt;Text style={styles.checkmark}&gt;✓&lt;/Text&gt;}
      &lt;/TouchableOpacity&gt;
      &lt;Text style={[styles.itemText, item.checked &amp;&amp; styles.itemTextChecked]}&gt;
        {item.text}
      &lt;/Text&gt;
      {showSavingBadge ? (
        &lt;View style={styles.savingBadge}&gt;
          &lt;Text style={styles.savingBadgeText}&gt;Saving&lt;/Text&gt;
        &lt;/View&gt;
      ) : null}
      &lt;TouchableOpacity
        style={styles.deleteButton}
        onPress={onDelete}
        hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
      &gt;
        &lt;Text style={styles.deleteButtonText}&gt;Delete&lt;/Text&gt;
      &lt;/TouchableOpacity&gt;
    &lt;/View&gt;
  )
}

export function ListDetail({ listId }: ListDetailProps) {
  const [newItemText, setNewItemText] = useState(``)
  const { itemActions } = useShopping()

  // Get items for this list
  const itemsResult = useLiveQuery((q) =&gt;
    q
      .from({ item: itemsCollection })
      .where(({ item }) =&gt; eq(item.listId, listId))
      .orderBy(({ item }) =&gt; item.createdAt, `asc`),
  )
  const items = itemsResult.data as Array&lt;ListItemRow&gt;

  const handleAddItem = async () =&gt; {
    if (!newItemText.trim() || !itemActions.addItem) return
    await itemActions.addItem({ listId, text: newItemText })
    setNewItemText(``)
  }

  const handleToggleItem = (id: string) =&gt; {
    itemActions.toggleItem?.(id)
  }

  const handleDeleteItem = (id: string) =&gt; {
    itemActions.deleteItem?.(id)
  }

  const checkedCount = items.filter((i) =&gt; i.checked).length

  return (
    &lt;View style={styles.container}&gt;
      {/* Summary */}
      &lt;Text style={styles.summary}&gt;
        {checkedCount}/{items.length} items checked
      &lt;/Text&gt;

      {/* Add item input */}
      &lt;View style={styles.inputRow}&gt;
        &lt;TextInput
          style={styles.input}
          value={newItemText}
          onChangeText={setNewItemText}
          placeholder=&quot;Add item...&quot;
          onSubmitEditing={handleAddItem}
        /&gt;
        &lt;TouchableOpacity
          style={[
            styles.addButton,
            !newItemText.trim() &amp;&amp; styles.addButtonDisabled,
          ]}
          onPress={handleAddItem}
          disabled={!newItemText.trim()}
        &gt;
          &lt;Text style={styles.addButtonText}&gt;Add&lt;/Text&gt;
        &lt;/TouchableOpacity&gt;
      &lt;/View&gt;

      {/* Items */}
      {items.length === 0 ? (
        &lt;View style={styles.centered}&gt;
          &lt;Text style={styles.emptyText}&gt;No items yet. Add one above!&lt;/Text&gt;
        &lt;/View&gt;
      ) : (
        &lt;FlatList
          data={items}
          keyExtractor={(item) =&gt; item.id}
          style={styles.list}
          renderItem={({ item }) =&gt; (
            &lt;ItemRow
              item={item}
              onToggle={() =&gt; handleToggleItem(item.id)}
              onDelete={() =&gt; handleDeleteItem(item.id)}
            /&gt;
          )}
        /&gt;
      )}
    &lt;/View&gt;
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: `#f5f5f5`,
  },
  centered: {
    flex: 1,
    justifyContent: `center`,
    alignItems: `center`,
  },
  summary: {
    fontSize: 14,
    color: `#666`,
    marginBottom: 12,
  },
  inputRow: {
    flexDirection: `row`,
    gap: 8,
    marginBottom: 16,
  },
  input: {
    flex: 1,
    backgroundColor: `#fff`,
    borderWidth: 1,
    borderColor: `#d1d5db`,
    borderRadius: 8,
    paddingHorizontal: 12,
    paddingVertical: 10,
    fontSize: 16,
  },
  addButton: {
    backgroundColor: `#3b82f6`,
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 8,
    justifyContent: `center`,
  },
  addButtonDisabled: {
    opacity: 0.5,
  },
  addButtonText: {
    color: `#fff`,
    fontWeight: `600`,
    fontSize: 16,
  },
  emptyText: {
    color: `#666`,
    fontSize: 16,
  },
  list: {
    flex: 1,
  },
  itemRow: {
    flexDirection: `row`,
    alignItems: `center`,
    backgroundColor: `#fff`,
    borderWidth: 1,
    borderColor: `#e5e5e5`,
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
    gap: 12,
  },
  checkbox: {
    width: 24,
    height: 24,
    borderWidth: 2,
    borderColor: `#d1d5db`,
    borderRadius: 4,
    justifyContent: `center`,
    alignItems: `center`,
  },
  checkboxChecked: {
    backgroundColor: `#22c55e`,
    borderColor: `#22c55e`,
  },
  checkmark: {
    color: `#fff`,
    fontSize: 14,
    fontWeight: `bold`,
  },
  itemText: {
    flex: 1,
    fontSize: 16,
    color: `#111`,
  },
  itemTextChecked: {
    textDecorationLine: `line-through`,
    color: `#999`,
  },
  deleteButton: {
    paddingHorizontal: 12,
    paddingVertical: 6,
  },
  savingBadge: {
    backgroundColor: `#fef3c7`,
    borderRadius: 999,
    paddingHorizontal: 8,
    paddingVertical: 2,
  },
  savingBadgeText: {
    color: `#92400e`,
    fontSize: 11,
    fontWeight: `700`,
  },
  deleteButtonText: {
    color: `#dc2626`,
    fontSize: 14,
  },
})
">
<input type="hidden" name="project[files][src/components/ListsScreen.tsx]" value="import React, { useEffect, useState } from &#39;react&#39;
import {
  ActivityIndicator,
  Alert,
  FlatList,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from &#39;react-native&#39;
import { useRouter } from &#39;expo-router&#39;
import { count, eq, useLiveQuery } from &#39;@tanstack/react-db&#39;
import { itemsCollection, listsCollection } from &#39;../db/collections&#39;
import { useShopping } from &#39;../db/ShoppingContext&#39;

// Subcomponent that subscribes to the child aggregate collections for reactive counts
function ListCard({
  list,
  onPress,
  onDelete,
}: {
  list: {
    id: string
    name: string
    totalItems: any
    checkedItems: any
    uncheckedPreview: any
    $synced?: boolean
  }
  onPress: () =&gt; void
  onDelete: () =&gt; void
}) {
  // Subscribe to the child collections — this is the &quot;includes&quot; pattern for React
  const { data: totalData } = useLiveQuery(list.totalItems)
  const { data: checkedData } = useLiveQuery(list.checkedItems)
  const { data: uncheckedPreviewData } = useLiveQuery(list.uncheckedPreview)
  const totalCount = (totalData as any)?.[0]?.n ?? 0
  const checkedCount = (checkedData as any)?.[0]?.n ?? 0
  const uncheckedPreview =
    (uncheckedPreviewData as any as Array&lt;{ text: string }&gt; | undefined) ?? []
  const uncheckedCount = Math.max(0, totalCount - checkedCount)
  const remainingCount = Math.max(0, uncheckedCount - uncheckedPreview.length)
  const previewText = uncheckedPreview
    .map((item) =&gt; item.text.trim())
    .filter((text) =&gt; text.length &gt; 0)
    .join(`, `)
  const [showSavingBadge, setShowSavingBadge] = useState(false)

  useEffect(() =&gt; {
    if (list.$synced !== false) {
      setShowSavingBadge(false)
      return
    }

    setShowSavingBadge(false)
    const timer = setTimeout(() =&gt; {
      setShowSavingBadge(true)
    }, 200)
    return () =&gt; {
      clearTimeout(timer)
    }
  }, [list.id, list.$synced])

  return (
    &lt;TouchableOpacity
      style={styles.listCard}
      onPress={onPress}
      activeOpacity={0.7}
    &gt;
      &lt;View style={styles.listContent}&gt;
        &lt;View style={styles.listHeaderRow}&gt;
          &lt;Text style={styles.listName}&gt;{list.name}&lt;/Text&gt;
          {showSavingBadge ? (
            &lt;View style={styles.savingBadge}&gt;
              &lt;Text style={styles.savingBadgeText}&gt;Saving&lt;/Text&gt;
            &lt;/View&gt;
          ) : null}
        &lt;/View&gt;
        &lt;Text style={styles.listCount}&gt;
          {checkedCount}/{totalCount} items
        &lt;/Text&gt;
        {uncheckedCount &gt; 0 &amp;&amp; previewText.length &gt; 0 ? (
          &lt;Text style={styles.previewText}&gt;
            {previewText}
            {remainingCount &gt; 0 ? ` and ${remainingCount} more` : ``}
          &lt;/Text&gt;
        ) : totalCount &gt; 0 ? (
          &lt;Text style={styles.allDoneText}&gt;All items checked&lt;/Text&gt;
        ) : null}
      &lt;/View&gt;
      &lt;TouchableOpacity
        style={styles.deleteButton}
        onPress={onDelete}
        hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
      &gt;
        &lt;Text style={styles.deleteButtonText}&gt;Delete&lt;/Text&gt;
      &lt;/TouchableOpacity&gt;
    &lt;/TouchableOpacity&gt;
  )
}

export function ListsScreen() {
  const router = useRouter()
  const [newListName, setNewListName] = useState(``)
  const { listActions, isInitialized, initError } = useShopping()

  // ★ Includes query with aggregate subqueries: each list gets child collections
  // with computed counts. ListCard subscribes to them via useLiveQuery.
  const queryResult = useLiveQuery((q) =&gt;
    q
      .from({ list: listsCollection })
      .select(({ list }) =&gt; ({
        id: list.id,
        name: list.name,
        createdAt: list.createdAt,
        $synced: list.$synced,
        totalItems: q
          .from({ item: itemsCollection })
          .where(({ item }) =&gt; eq(item.listId, list.id))
          .select(({ item }) =&gt; ({ n: count(item.id) })),
        uncheckedPreview: q
          .from({ item: itemsCollection })
          .where(({ item }) =&gt; eq(item.listId, list.id))
          .where(({ item }) =&gt; eq(item.checked, false))
          .select(({ item }) =&gt; ({
            id: item.id,
            text: item.text,
            createdAt: item.createdAt,
          }))
          .orderBy(({ item }) =&gt; item.createdAt, `asc`)
          .limit(3),
        checkedItems: q
          .from({ item: itemsCollection })
          .where(({ item }) =&gt; eq(item.listId, list.id))
          .where(({ item }) =&gt; eq(item.checked, true))
          .select(({ item }) =&gt; ({ n: count(item.id) })),
      }))
      .orderBy(({ list }) =&gt; list.createdAt, `desc`),
  )
  const lists = queryResult.data as unknown as Array&lt;{
    id: string
    name: string
    createdAt: string
    $synced?: boolean
    totalItems: any
    uncheckedPreview: any
    checkedItems: any
  }&gt;

  const handleAddList = async () =&gt; {
    if (!newListName.trim() || !listActions.addList) return
    await listActions.addList(newListName)
    setNewListName(``)
  }

  const handleDeleteList = (id: string, name: string) =&gt; {
    Alert.alert(`Delete &quot;${name}&quot;?`, `This will also delete all items.`, [
      { text: `Cancel`, style: `cancel` },
      {
        text: `Delete`,
        style: `destructive`,
        onPress: () =&gt; listActions.deleteList?.(id),
      },
    ])
  }

  if (initError) {
    return (
      &lt;View style={styles.container}&gt;
        &lt;View style={styles.errorBox}&gt;
          &lt;Text style={styles.errorText}&gt;{initError}&lt;/Text&gt;
        &lt;/View&gt;
      &lt;/View&gt;
    )
  }

  if (!isInitialized) {
    return (
      &lt;View style={styles.centered}&gt;
        &lt;ActivityIndicator size=&quot;large&quot; color=&quot;#3b82f6&quot; /&gt;
        &lt;Text style={styles.loadingText}&gt;Initializing...&lt;/Text&gt;
      &lt;/View&gt;
    )
  }

  return (
    &lt;View style={styles.container}&gt;
      {/* Add list input */}
      &lt;View style={styles.inputRow}&gt;
        &lt;TextInput
          style={styles.input}
          value={newListName}
          onChangeText={setNewListName}
          placeholder=&quot;New list name...&quot;
          onSubmitEditing={handleAddList}
        /&gt;
        &lt;TouchableOpacity
          style={[
            styles.addButton,
            !newListName.trim() &amp;&amp; styles.addButtonDisabled,
          ]}
          onPress={handleAddList}
          disabled={!newListName.trim()}
        &gt;
          &lt;Text style={styles.addButtonText}&gt;Add&lt;/Text&gt;
        &lt;/TouchableOpacity&gt;
      &lt;/View&gt;

      {/* Lists */}
      {lists.length === 0 ? (
        &lt;View style={styles.centered}&gt;
          &lt;Text style={styles.emptyText}&gt;No lists yet&lt;/Text&gt;
        &lt;/View&gt;
      ) : (
        &lt;FlatList
          data={lists}
          keyExtractor={(item) =&gt; item.id}
          style={styles.list}
          renderItem={({ item: list }) =&gt; (
            &lt;ListCard
              list={list}
              onPress={() =&gt; router.push(`/list/${list.id}`)}
              onDelete={() =&gt; handleDeleteList(list.id, list.name)}
            /&gt;
          )}
        /&gt;
      )}

      {/* Instructions */}
      &lt;View style={styles.instructions}&gt;
        &lt;Text style={styles.instructionsTitle}&gt;Features showcased:&lt;/Text&gt;
        &lt;Text style={styles.instructionsText}&gt;
          1. Includes — item counts from nested child queries
        &lt;/Text&gt;
        &lt;Text style={styles.instructionsText}&gt;
          2. Electric sync — real-time replication via shape streams
        &lt;/Text&gt;
        &lt;Text style={styles.instructionsText}&gt;
          3. Offline transactions — works without network
        &lt;/Text&gt;
        &lt;Text style={styles.instructionsText}&gt;
          4. SQLite persistence — data survives app restart
        &lt;/Text&gt;
      &lt;/View&gt;
    &lt;/View&gt;
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: `#f5f5f5`,
  },
  centered: {
    flex: 1,
    justifyContent: `center`,
    alignItems: `center`,
    gap: 12,
  },
  errorBox: {
    backgroundColor: `#fee2e2`,
    borderWidth: 1,
    borderColor: `#fca5a5`,
    borderRadius: 8,
    padding: 12,
    marginBottom: 16,
  },
  errorText: {
    color: `#dc2626`,
    fontSize: 14,
  },
  inputRow: {
    flexDirection: `row`,
    gap: 8,
    marginBottom: 16,
  },
  input: {
    flex: 1,
    backgroundColor: `#fff`,
    borderWidth: 1,
    borderColor: `#d1d5db`,
    borderRadius: 8,
    paddingHorizontal: 12,
    paddingVertical: 10,
    fontSize: 16,
  },
  addButton: {
    backgroundColor: `#3b82f6`,
    paddingHorizontal: 20,
    paddingVertical: 10,
    borderRadius: 8,
    justifyContent: `center`,
  },
  addButtonDisabled: {
    opacity: 0.5,
  },
  addButtonText: {
    color: `#fff`,
    fontWeight: `600`,
    fontSize: 16,
  },
  loadingText: {
    color: `#666`,
    fontSize: 14,
  },
  emptyText: {
    color: `#666`,
    fontSize: 16,
  },
  list: {
    flex: 1,
  },
  listCard: {
    flexDirection: `row`,
    alignItems: `center`,
    backgroundColor: `#fff`,
    borderWidth: 1,
    borderColor: `#e5e5e5`,
    borderRadius: 12,
    padding: 16,
    marginBottom: 8,
  },
  listContent: {
    flex: 1,
  },
  listHeaderRow: {
    flexDirection: `row`,
    alignItems: `center`,
    gap: 8,
  },
  listName: {
    fontSize: 18,
    fontWeight: `600`,
    color: `#111`,
  },
  savingBadge: {
    backgroundColor: `#fef3c7`,
    borderRadius: 999,
    paddingHorizontal: 8,
    paddingVertical: 2,
  },
  savingBadgeText: {
    color: `#92400e`,
    fontSize: 11,
    fontWeight: `700`,
  },
  listCount: {
    fontSize: 14,
    color: `#666`,
    marginTop: 4,
  },
  previewText: {
    marginTop: 4,
    color: `#374151`,
    fontSize: 13,
  },
  allDoneText: {
    marginTop: 4,
    color: `#15803d`,
    fontSize: 12,
    fontWeight: `600`,
  },
  deleteButton: {
    paddingHorizontal: 12,
    paddingVertical: 6,
  },
  deleteButtonText: {
    color: `#dc2626`,
    fontSize: 14,
  },
  instructions: {
    backgroundColor: `#f0f0f0`,
    borderRadius: 8,
    padding: 16,
    marginTop: 16,
  },
  instructionsTitle: {
    fontWeight: `600`,
    color: `#111`,
    marginBottom: 8,
  },
  instructionsText: {
    color: `#666`,
    fontSize: 13,
    marginBottom: 2,
  },
})
">
<input type="hidden" name="project[files][src/db/AsyncStorageAdapter.ts]" value="import AsyncStorage from &#39;@react-native-async-storage/async-storage&#39;
import type { StorageAdapter } from &#39;@tanstack/offline-transactions&#39;

export class AsyncStorageAdapter implements StorageAdapter {
  private prefix: string

  constructor(prefix = `offline-tx:`) {
    this.prefix = prefix
  }

  private getKey(key: string): string {
    return `${this.prefix}${key}`
  }

  async get(key: string): Promise&lt;string | null&gt; {
    return AsyncStorage.getItem(this.getKey(key))
  }

  async set(key: string, value: string): Promise&lt;void&gt; {
    await AsyncStorage.setItem(this.getKey(key), value)
  }

  async delete(key: string): Promise&lt;void&gt; {
    await AsyncStorage.removeItem(this.getKey(key))
  }

  async keys(): Promise&lt;Array&lt;string&gt;&gt; {
    const allKeys = await AsyncStorage.getAllKeys()
    return allKeys
      .filter((k) =&gt; k.startsWith(this.prefix))
      .map((k) =&gt; k.slice(this.prefix.length))
  }

  async clear(): Promise&lt;void&gt; {
    const keys = await this.keys()
    const prefixedKeys = keys.map((k) =&gt; this.getKey(k))
    if (prefixedKeys.length &gt; 0) {
      await AsyncStorage.multiRemove(prefixedKeys)
    }
  }
}
">
<input type="hidden" name="project[files][src/db/ShoppingContext.tsx]" value="import React, {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from &#39;react&#39;
import NetInfo from &#39;@react-native-community/netinfo&#39;
import {
  hydrateSimulatedOffline,
  isSimulatedOffline,
  setSimulatedOffline,
  subscribeSimulatedOffline,
} from &#39;../network/simulatedOffline&#39;
import {
  clearLocalState as clearCollectionsLocalState,
  createItemActions,
  createListActions,
  createOfflineExecutor,
} from &#39;./collections&#39;

type OfflineExecutor = ReturnType&lt;typeof createOfflineExecutor&gt;

interface ShoppingContextValue {
  offline: OfflineExecutor | null
  listActions: ReturnType&lt;typeof createListActions&gt;
  itemActions: ReturnType&lt;typeof createItemActions&gt;
  isNetworkOnline: boolean
  isSimulatedOffline: boolean
  isOnline: boolean
  setSimulateOffline: (enabled: boolean) =&gt; Promise&lt;void&gt;
  clearLocalState: () =&gt; Promise&lt;void&gt;
  pendingCount: number
  isInitialized: boolean
  initError: string | null
}

const ShoppingContext = createContext&lt;ShoppingContextValue | null&gt;(null)

export function ShoppingProvider({ children }: { children: React.ReactNode }) {
  const [offline, setOffline] = useState&lt;OfflineExecutor | null&gt;(null)
  const [isNetworkOnline, setIsNetworkOnline] = useState(true)
  const [isSimulatedOfflineState, setIsSimulatedOfflineState] =
    useState(isSimulatedOffline())
  const [pendingCount, setPendingCount] = useState(0)
  const [isInitialized, setIsInitialized] = useState(false)
  const [initError, setInitError] = useState&lt;string | null&gt;(null)

  // Initialize offline executor
  useEffect(() =&gt; {
    try {
      const executor = createOfflineExecutor()
      setOffline(executor)
      setIsInitialized(true)
      return () =&gt; {
        executor.dispose()
      }
    } catch (err) {
      console.error(`[Shopping] Failed to create executor:`, err)
      setInitError(err instanceof Error ? err.message : `Failed to initialize`)
      setIsInitialized(true)
    }
  }, [])

  // Monitor network status (for UI display only —
  // ReactNativeOnlineDetector in the executor handles retry automatically)
  useEffect(() =&gt; {
    void hydrateSimulatedOffline().catch((err) =&gt; {
      console.warn(`[Shopping] Failed to hydrate simulated offline state`, err)
    })
    return subscribeSimulatedOffline(() =&gt; {
      setIsSimulatedOfflineState(isSimulatedOffline())
    })
  }, [])

  useEffect(() =&gt; {
    const unsubscribe = NetInfo.addEventListener((state) =&gt; {
      const connected =
        state.isConnected === true &amp;&amp; state.isInternetReachable !== false
      setIsNetworkOnline(connected)
    })
    return () =&gt; unsubscribe()
  }, [])

  // Monitor pending transactions
  useEffect(() =&gt; {
    if (!offline) return
    const interval = setInterval(() =&gt; {
      setPendingCount(offline.getPendingCount())
    }, 100)
    return () =&gt; clearInterval(interval)
  }, [offline])

  const listActions = useMemo(() =&gt; createListActions(offline), [offline])
  const itemActions = useMemo(() =&gt; createItemActions(offline), [offline])
  const setSimulateOffline = useCallback((enabled: boolean) =&gt; {
    return setSimulatedOffline(enabled)
  }, [])
  const clearLocalState = useCallback(async () =&gt; {
    await setSimulatedOffline(false)
    await clearCollectionsLocalState(offline)
  }, [offline])
  const isOnline = isNetworkOnline &amp;&amp; !isSimulatedOfflineState

  const value = useMemo(
    () =&gt; ({
      offline,
      listActions,
      itemActions,
      isNetworkOnline,
      isSimulatedOffline: isSimulatedOfflineState,
      isOnline,
      setSimulateOffline,
      clearLocalState,
      pendingCount,
      isInitialized,
      initError,
    }),
    [
      offline,
      listActions,
      itemActions,
      isNetworkOnline,
      isSimulatedOfflineState,
      isOnline,
      setSimulateOffline,
      clearLocalState,
      pendingCount,
      isInitialized,
      initError,
    ],
  )

  return (
    &lt;ShoppingContext.Provider value={value}&gt;
      {children}
    &lt;/ShoppingContext.Provider&gt;
  )
}

export function useShopping(): ShoppingContextValue {
  const ctx = useContext(ShoppingContext)
  if (!ctx) {
    throw new Error(`useShopping must be used within ShoppingProvider`)
  }
  return ctx
}
">
<input type="hidden" name="project[files][src/db/collections.ts]" value="import { open } from &#39;@op-engineering/op-sqlite&#39;
import { createCollection } from &#39;@tanstack/react-db&#39;
import { electricCollectionOptions } from &#39;@tanstack/electric-db-collection&#39;
import {
  createReactNativeSQLitePersistence,
  persistedCollectionOptions,
} from &#39;@tanstack/react-native-db-sqlite-persistence&#39;
import { startOfflineExecutor } from &#39;@tanstack/offline-transactions/react-native&#39;
import { API_URL, itemsApi, listsApi } from &#39;../utils/api&#39;
import { createOfflineAwareFetch } from &#39;../network/simulatedOffline&#39;
import { simulatedOnlineDetector } from &#39;../network/SimulatedOnlineDetector&#39;
import { AsyncStorageAdapter } from &#39;./AsyncStorageAdapter&#39;
import type { PendingMutation } from &#39;@tanstack/db&#39;
import type { OpSQLiteDatabaseLike } from &#39;@tanstack/react-native-db-sqlite-persistence&#39;
import type { ElectricCollectionUtils } from &#39;@tanstack/electric-db-collection&#39;

export type ShoppingList = {
  id: string
  name: string
  createdAt: string
}

export type ShoppingItem = {
  id: string
  listId: string
  text: string
  checked: boolean
  createdAt: string
}

const database = open({
  name: `shopping-list.sqlite`,
  location: `default`,
}) as unknown as OpSQLiteDatabaseLike

const sharedPersistence = createReactNativeSQLitePersistence({
  database,
}) as any
const offlineStorage = new AsyncStorageAdapter(`shopping-offline:`)

type SQLiteResultWithRows = {
  rows?: {
    _array?: Array&lt;Record&lt;string, unknown&gt;&gt;
    length?: unknown
    item?: unknown
  }
  resultRows?: Array&lt;Record&lt;string, unknown&gt;&gt;
  results?: Array&lt;SQLiteResultWithRows&gt;
}

function getExecuteMethod(db: OpSQLiteDatabaseLike) {
  return db.executeAsync ?? db.execute ?? db.executeRaw ?? db.execAsync
}

function extractRows(result: unknown): Array&lt;Record&lt;string, unknown&gt;&gt; {
  const fromRowsObject = (
    rows: SQLiteResultWithRows[`rows`],
  ): Array&lt;Record&lt;string, unknown&gt;&gt; | null =&gt; {
    if (!rows) return null
    if (Array.isArray(rows._array)) {
      return rows._array
    }
    if (typeof rows.length === `number` &amp;&amp; typeof rows.item === `function`) {
      const item = rows.item as (index: number) =&gt; unknown
      const extracted: Array&lt;Record&lt;string, unknown&gt;&gt; = []
      for (let i = 0; i &lt; rows.length; i++) {
        const row = item(i)
        if (row &amp;&amp; typeof row === `object`) {
          extracted.push(row as Record&lt;string, unknown&gt;)
        }
      }
      return extracted
    }
    return null
  }

  if (Array.isArray(result)) {
    if (result.length === 0) return []
    const first = result[0] as SQLiteResultWithRows
    if (Array.isArray(first.resultRows)) {
      return first.resultRows
    }
    const fromFirstRows = fromRowsObject(first.rows)
    if (fromFirstRows) {
      return fromFirstRows
    }
    if (Array.isArray(first.results) &amp;&amp; first.results.length &gt; 0) {
      return extractRows(first.results[0])
    }
    return result as Array&lt;Record&lt;string, unknown&gt;&gt;
  }
  const maybe = result as SQLiteResultWithRows
  if (Array.isArray(maybe.resultRows)) {
    return maybe.resultRows
  }
  const fromDirectRows = fromRowsObject(maybe.rows)
  if (fromDirectRows) {
    return fromDirectRows
  }
  if (Array.isArray(maybe.results) &amp;&amp; maybe.results.length &gt; 0) {
    return extractRows(maybe.results[0])
  }
  return []
}

async function executeSql(
  sql: string,
  params: ReadonlyArray&lt;unknown&gt; = [],
): Promise&lt;unknown&gt; {
  const execute = getExecuteMethod(database)
  if (!execute) {
    throw new Error(`No execute method available for op-sqlite database`)
  }
  return Promise.resolve(
    execute.call(database, sql, params.length ? params : undefined),
  )
}

export async function clearLocalState(
  offline: ReturnType&lt;typeof createOfflineExecutor&gt; | null,
): Promise&lt;void&gt; {
  if (offline) {
    await offline.clearOutbox()
  } else {
    await offlineStorage.clear()
  }

  const rows = extractRows(
    await executeSql(
      `SELECT name FROM sqlite_master WHERE type = &#39;table&#39; AND name NOT LIKE &#39;sqlite_%&#39;`,
    ),
  )

  await executeSql(`PRAGMA foreign_keys = OFF`)
  try {
    for (const row of rows) {
      const tableName = row.name
      if (typeof tableName === `string`) {
        await executeSql(
          `DROP TABLE IF EXISTS &quot;${tableName.replace(/&quot;/g, `&quot;&quot;`)}&quot;`,
        )
      }
    }
  } finally {
    await executeSql(`PRAGMA foreign_keys = ON`)
  }
}

export const listsCollection = createCollection(
  persistedCollectionOptions&lt;
    ShoppingList,
    string | number,
    never,
    ElectricCollectionUtils&lt;ShoppingList&gt;
  &gt;({
    ...electricCollectionOptions&lt;ShoppingList&gt;({
      id: `lists-collection`,
      shapeOptions: {
        url: `${API_URL}/api/shapes/lists`,
        fetchClient: createOfflineAwareFetch(fetch),
        onError: (error) =&gt; {
          console.error(`[Electric] lists shape error`, error)
        },
      },
      getKey: (item) =&gt; item.id,
    }),
    persistence: sharedPersistence,
    schemaVersion: 1,
  }),
)

export const itemsCollection = createCollection(
  persistedCollectionOptions&lt;
    ShoppingItem,
    string | number,
    never,
    ElectricCollectionUtils&lt;ShoppingItem&gt;
  &gt;({
    ...electricCollectionOptions&lt;ShoppingItem&gt;({
      id: `items-collection`,
      shapeOptions: {
        url: `${API_URL}/api/shapes/items`,
        fetchClient: createOfflineAwareFetch(fetch),
        onError: (error) =&gt; {
          console.error(`[Electric] items shape error`, error)
        },
      },
      getKey: (item) =&gt; item.id,
    }),
    persistence: sharedPersistence,
    schemaVersion: 1,
  }),
)

async function syncLists({
  transaction,
}: {
  transaction: { mutations: Array&lt;PendingMutation&gt; }
  idempotencyKey: string
}) {
  for (const mutation of transaction.mutations) {
    const data = mutation.modified as ShoppingList
    switch (mutation.type) {
      case `insert`: {
        const created = await listsApi.create({
          id: data.id,
          name: data.name,
          createdAt: data.createdAt,
        })
        await listsCollection.utils.awaitTxId(created.txid)
        break
      }
      case `update`: {
        const updated = await listsApi.update(data.id, { name: data.name })
        if (updated) {
          await listsCollection.utils.awaitTxId(updated.txid)
        }
        break
      }
      case `delete`: {
        const deleted = await listsApi.delete(
          (mutation.original as ShoppingList).id,
        )
        if (deleted) {
          await listsCollection.utils.awaitTxId(deleted.txid)
        }
        break
      }
    }
  }
}

async function syncItems({
  transaction,
}: {
  transaction: { mutations: Array&lt;PendingMutation&gt; }
  idempotencyKey: string
}) {
  for (const mutation of transaction.mutations) {
    const data = mutation.modified as ShoppingItem
    switch (mutation.type) {
      case `insert`: {
        const created = await itemsApi.create({
          id: data.id,
          listId: data.listId,
          text: data.text,
          checked: data.checked,
          createdAt: data.createdAt,
        })
        await itemsCollection.utils.awaitTxId(created.txid)
        break
      }
      case `update`: {
        const updated = await itemsApi.update(data.id, {
          text: data.text,
          checked: data.checked,
        })
        if (updated) {
          await itemsCollection.utils.awaitTxId(updated.txid)
        }
        break
      }
      case `delete`: {
        const deleted = await itemsApi.delete(
          (mutation.original as ShoppingItem).id,
        )
        if (deleted) {
          await itemsCollection.utils.awaitTxId(deleted.txid)
        }
        break
      }
    }
  }
}

export function createOfflineExecutor() {
  return startOfflineExecutor({
    collections: {
      lists: listsCollection,
      items: itemsCollection,
    },
    storage: offlineStorage,
    mutationFns: {
      syncLists,
      syncItems,
    },
    onlineDetector: simulatedOnlineDetector,
    onLeadershipChange: (isLeader) =&gt; {
      console.log(`[Offline] Leadership changed:`, isLeader)
    },
    onStorageFailure: (diagnostic) =&gt; {
      console.warn(`[Offline] Storage failure:`, diagnostic)
    },
  })
}

export function createListActions(
  offline: ReturnType&lt;typeof createOfflineExecutor&gt; | null,
) {
  if (!offline) {
    return { addList: null, deleteList: null }
  }

  const addList = offline.createOfflineAction({
    mutationFnName: `syncLists`,
    onMutate: (name: string) =&gt; {
      const newList: ShoppingList = {
        id: crypto.randomUUID(),
        name: name.trim(),
        createdAt: new Date().toISOString(),
      }
      listsCollection.insert(newList)
      return newList
    },
  })

  const deleteList = offline.createOfflineAction({
    mutationFnName: `syncLists`,
    onMutate: (id: string) =&gt; {
      const list = listsCollection.get(id)
      if (list) {
        listsCollection.delete(id)
      }
      return list
    },
  })

  return { addList, deleteList }
}

export function createItemActions(
  offline: ReturnType&lt;typeof createOfflineExecutor&gt; | null,
) {
  if (!offline) {
    return { addItem: null, toggleItem: null, deleteItem: null }
  }

  const addItem = offline.createOfflineAction({
    mutationFnName: `syncItems`,
    onMutate: ({ listId, text }: { listId: string; text: string }) =&gt; {
      const newItem: ShoppingItem = {
        id: crypto.randomUUID(),
        listId,
        text: text.trim(),
        checked: false,
        createdAt: new Date().toISOString(),
      }
      itemsCollection.insert(newItem)
      return newItem
    },
  })

  const toggleItem = offline.createOfflineAction({
    mutationFnName: `syncItems`,
    onMutate: (id: string) =&gt; {
      const item = itemsCollection.get(id) as ShoppingItem | undefined
      if (!item) return
      itemsCollection.update(id, (draft) =&gt; {
        draft.checked = !draft.checked
      })
      return item
    },
  })

  const deleteItem = offline.createOfflineAction({
    mutationFnName: `syncItems`,
    onMutate: (id: string) =&gt; {
      const item = itemsCollection.get(id)
      if (item) {
        itemsCollection.delete(item.id)
      }
      return item
    },
  })

  return { addItem, toggleItem, deleteItem }
}
">
<input type="hidden" name="project[files][src/network/SimulatedOnlineDetector.ts]" value="import { ReactNativeOnlineDetector } from &#39;@tanstack/offline-transactions/react-native&#39;
import {
  isSimulatedOffline,
  subscribeSimulatedOffline,
} from &#39;./simulatedOffline&#39;
import type { OnlineDetector } from &#39;@tanstack/offline-transactions/react-native&#39;

class SimulatedOnlineDetector implements OnlineDetector {
  private readonly baseDetector = new ReactNativeOnlineDetector()

  subscribe(callback: () =&gt; void): () =&gt; void {
    const unsubscribeBase = this.baseDetector.subscribe(callback)
    const unsubscribeSimulated = subscribeSimulatedOffline(callback)
    return () =&gt; {
      unsubscribeBase()
      unsubscribeSimulated()
    }
  }

  isOnline(): boolean {
    return this.baseDetector.isOnline() &amp;&amp; !isSimulatedOffline()
  }

  notifyOnline(): void {
    this.baseDetector.notifyOnline()
  }

  dispose(): void {
    this.baseDetector.dispose()
  }
}

export const simulatedOnlineDetector: OnlineDetector =
  new SimulatedOnlineDetector()
">
<input type="hidden" name="project[files][src/network/simulatedOffline.ts]" value="import AsyncStorage from &#39;@react-native-async-storage/async-storage&#39;

const STORAGE_KEY = `shopping-list:simulate-offline`

let forcedOffline = false
const listeners = new Set&lt;() =&gt; void&gt;()

function notifyListeners() {
  for (const listener of listeners) {
    listener()
  }
}

export function isSimulatedOffline(): boolean {
  return forcedOffline
}

export async function hydrateSimulatedOffline(): Promise&lt;void&gt; {
  const stored = await AsyncStorage.getItem(STORAGE_KEY)
  forcedOffline = stored === `true`
  notifyListeners()
}

export async function setSimulatedOffline(value: boolean): Promise&lt;void&gt; {
  forcedOffline = value
  await AsyncStorage.setItem(STORAGE_KEY, value ? `true` : `false`)
  notifyListeners()
}

export function subscribeSimulatedOffline(listener: () =&gt; void): () =&gt; void {
  listeners.add(listener)
  return () =&gt; {
    listeners.delete(listener)
  }
}

export function createOfflineAwareFetch(baseFetch: typeof fetch): typeof fetch {
  return async (input: RequestInfo | URL, init?: RequestInit) =&gt; {
    if (isSimulatedOffline()) {
      throw new TypeError(`Network request blocked by simulated offline mode`)
    }
    return baseFetch(input, init)
  }
}
">
<input type="hidden" name="project[files][src/utils/api.ts]" value="import { Platform } from &#39;react-native&#39;
import { createOfflineAwareFetch } from &#39;../network/simulatedOffline&#39;

const SERVER_PORT = 3001
export const API_URL = Platform.select({
  android: `http://10.0.2.2:${SERVER_PORT}`,
  ios: `http://localhost:${SERVER_PORT}`,
  default: `http://localhost:${SERVER_PORT}`,
})
const offlineAwareFetch = createOfflineAwareFetch(fetch)

// ─── Types ──────────────────────────────────────────────

export interface ShoppingList {
  id: string
  name: string
  createdAt: string
}

export interface ShoppingItem {
  id: string
  listId: string
  text: string
  checked: boolean
  createdAt: string
}

type ApiTxResult&lt;T&gt; = { txid: number } &amp; T

// ─── Lists API ──────────────────────────────────────────

export const listsApi = {
  async getAll(): Promise&lt;Array&lt;ShoppingList&gt;&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/lists`)
    if (!response.ok) {
      throw new Error(`Failed to fetch lists: ${response.status}`)
    }
    return response.json()
  },

  async create(data: {
    id?: string
    name: string
    createdAt?: string
  }): Promise&lt;ApiTxResult&lt;{ list: ShoppingList }&gt;&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/lists`, {
      method: &#39;POST&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify(data),
    })
    if (!response.ok) {
      throw new Error(`Failed to create list: ${response.status}`)
    }
    return response.json()
  },

  async update(
    id: string,
    data: { name?: string },
  ): Promise&lt;ApiTxResult&lt;{ list: ShoppingList }&gt; | null&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/lists/${id}`, {
      method: &#39;PUT&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify(data),
    })
    if (response.status === 404) return null
    if (!response.ok) {
      throw new Error(`Failed to update list: ${response.status}`)
    }
    return response.json()
  },

  async delete(id: string): Promise&lt;ApiTxResult&lt;{ success: boolean }&gt; | null&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/lists/${id}`, {
      method: &#39;DELETE&#39;,
    })
    if (response.status === 404) return null
    if (!response.ok) {
      throw new Error(`Failed to delete list: ${response.status}`)
    }
    return response.json()
  },
}

// ─── Items API ──────────────────────────────────────────

export const itemsApi = {
  async getAll(): Promise&lt;Array&lt;ShoppingItem&gt;&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/items`)
    if (!response.ok) {
      throw new Error(`Failed to fetch items: ${response.status}`)
    }
    return response.json()
  },

  async create(data: {
    id?: string
    listId: string
    text: string
    checked?: boolean
    createdAt?: string
  }): Promise&lt;ApiTxResult&lt;{ item: ShoppingItem }&gt;&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/items`, {
      method: &#39;POST&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify(data),
    })
    if (!response.ok) {
      throw new Error(`Failed to create item: ${response.status}`)
    }
    return response.json()
  },

  async update(
    id: string,
    data: { text?: string; checked?: boolean },
  ): Promise&lt;ApiTxResult&lt;{ item: ShoppingItem }&gt; | null&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/items/${id}`, {
      method: &#39;PUT&#39;,
      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
      body: JSON.stringify(data),
    })
    if (response.status === 404) return null
    if (!response.ok) {
      throw new Error(`Failed to update item: ${response.status}`)
    }
    return response.json()
  },

  async delete(id: string): Promise&lt;ApiTxResult&lt;{ success: boolean }&gt; | null&gt; {
    const response = await offlineAwareFetch(`${API_URL}/api/items/${id}`, {
      method: &#39;DELETE&#39;,
    })
    if (response.status === 404) return null
    if (!response.ok) {
      throw new Error(`Failed to delete item: ${response.status}`)
    }
    return response.json()
  },
}
">
<input type="hidden" name="project[files][src/utils/queryClient.ts]" value="import { QueryClient } from &#39;@tanstack/react-query&#39;

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      retry: 3,
    },
  },
})
">
<input type="hidden" name="project[files][android/gradle/wrapper/gradle-wrapper.jar]" value="https://pkg.pr.new/template/da1dc6af-8098-4d63-adf5-691437f51c66">
<input type="hidden" name="project[files][android/gradle/wrapper/gradle-wrapper.properties]" value="distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
">
<input type="hidden" name="project[files][android/app/src/debug/AndroidManifest.xml]" value="&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;

    &lt;uses-permission android:name=&quot;android.permission.SYSTEM_ALERT_WINDOW&quot;/&gt;

    &lt;application android:usesCleartextTraffic=&quot;true&quot; tools:targetApi=&quot;28&quot; tools:ignore=&quot;GoogleAppIndexingWarning&quot; tools:replace=&quot;android:usesCleartextTraffic&quot; /&gt;
&lt;/manifest&gt;
">
<input type="hidden" name="project[files][android/app/src/main/AndroidManifest.xml]" value="&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
  &lt;uses-permission android:name=&quot;android.permission.INTERNET&quot;/&gt;
  &lt;uses-permission android:name=&quot;android.permission.READ_EXTERNAL_STORAGE&quot;/&gt;
  &lt;uses-permission android:name=&quot;android.permission.SYSTEM_ALERT_WINDOW&quot;/&gt;
  &lt;uses-permission android:name=&quot;android.permission.VIBRATE&quot;/&gt;
  &lt;uses-permission android:name=&quot;android.permission.WRITE_EXTERNAL_STORAGE&quot;/&gt;
  &lt;queries&gt;
    &lt;intent&gt;
      &lt;action android:name=&quot;android.intent.action.VIEW&quot;/&gt;
      &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot;/&gt;
      &lt;data android:scheme=&quot;https&quot;/&gt;
    &lt;/intent&gt;
  &lt;/queries&gt;
  &lt;application android:name=&quot;.MainApplication&quot; android:label=&quot;@string/app_name&quot; android:icon=&quot;@mipmap/ic_launcher&quot; android:roundIcon=&quot;@mipmap/ic_launcher_round&quot; android:allowBackup=&quot;true&quot; android:theme=&quot;@style/AppTheme&quot; android:supportsRtl=&quot;true&quot;&gt;
    &lt;meta-data android:name=&quot;expo.modules.updates.ENABLED&quot; android:value=&quot;false&quot;/&gt;
    &lt;meta-data android:name=&quot;expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH&quot; android:value=&quot;ALWAYS&quot;/&gt;
    &lt;meta-data android:name=&quot;expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS&quot; android:value=&quot;0&quot;/&gt;
    &lt;activity android:name=&quot;.MainActivity&quot; android:configChanges=&quot;keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode&quot; android:launchMode=&quot;singleTask&quot; android:windowSoftInputMode=&quot;adjustResize&quot; android:theme=&quot;@style/Theme.App.SplashScreen&quot; android:exported=&quot;true&quot; android:screenOrientation=&quot;portrait&quot;&gt;
      &lt;intent-filter&gt;
        &lt;action android:name=&quot;android.intent.action.MAIN&quot;/&gt;
        &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot;/&gt;
      &lt;/intent-filter&gt;
      &lt;intent-filter&gt;
        &lt;action android:name=&quot;android.intent.action.VIEW&quot;/&gt;
        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot;/&gt;
        &lt;category android:name=&quot;android.intent.category.BROWSABLE&quot;/&gt;
        &lt;data android:scheme=&quot;shopping-list&quot;/&gt;
      &lt;/intent-filter&gt;
    &lt;/activity&gt;
  &lt;/application&gt;
&lt;/manifest&gt;">
<input type="hidden" name="project[files][android/app/src/main/res/drawable-hdpi/splashscreen_logo.png]" value="https://pkg.pr.new/template/7e5d079c-8c95-4b49-a005-02ae06d042ed">
<input type="hidden" name="project[files][android/app/src/main/res/drawable/ic_launcher_background.xml]" value="&lt;layer-list xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
  &lt;item android:drawable=&quot;@color/splashscreen_background&quot;/&gt;
  &lt;item&gt;
    &lt;bitmap android:gravity=&quot;center&quot; android:src=&quot;@drawable/splashscreen_logo&quot;/&gt;
  &lt;/item&gt;
&lt;/layer-list&gt;">
<input type="hidden" name="project[files][android/app/src/main/res/drawable/rn_edit_text_material.xml]" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;!-- Copyright (C) 2014 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
--&gt;
&lt;inset xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
       android:insetLeft=&quot;@dimen/abc_edit_text_inset_horizontal_material&quot;
       android:insetRight=&quot;@dimen/abc_edit_text_inset_horizontal_material&quot;
       android:insetTop=&quot;@dimen/abc_edit_text_inset_top_material&quot;
       android:insetBottom=&quot;@dimen/abc_edit_text_inset_bottom_material&quot;
       &gt;

    &lt;selector&gt;
        &lt;!--
          This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
          The item below with state_pressed=&quot;false&quot; and state_focused=&quot;false&quot; causes a NullPointerException.
          NullPointerException:tempt to invoke virtual method &#39;android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)&#39;

          &lt;item android:state_pressed=&quot;false&quot; android:state_focused=&quot;false&quot; android:drawable=&quot;@drawable/abc_textfield_default_mtrl_alpha&quot;/&gt;

          For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
        --&gt;
        &lt;item android:state_enabled=&quot;false&quot; android:drawable=&quot;@drawable/abc_textfield_default_mtrl_alpha&quot;/&gt;
        &lt;item android:drawable=&quot;@drawable/abc_textfield_activated_mtrl_alpha&quot;/&gt;
    &lt;/selector&gt;

&lt;/inset&gt;
">
<input type="hidden" name="project[files][android/app/src/main/res/drawable-mdpi/splashscreen_logo.png]" value="https://pkg.pr.new/template/432dd19b-82ad-43d5-a8ee-1ea3d0a3708c">
<input type="hidden" name="project[files][android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png]" value="https://pkg.pr.new/template/ceb13000-f0a0-44ca-a1df-ca7b9fc98d1c">
<input type="hidden" name="project[files][android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png]" value="https://pkg.pr.new/template/ab08731f-0201-4b58-9da7-bb80805accee">
<input type="hidden" name="project[files][android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png]" value="https://pkg.pr.new/template/80ce1917-7184-4112-bfb7-45c67a653f72">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-hdpi/ic_launcher.webp]" value="https://pkg.pr.new/template/2a5f8700-4fee-4975-a2c0-cc073241bd5d">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp]" value="https://pkg.pr.new/template/34ceabb3-1617-4d4b-b9b7-7ef9a3b4eb83">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-mdpi/ic_launcher.webp]" value="https://pkg.pr.new/template/8a354a00-730e-4099-ad87-3fb6f494a68b">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp]" value="https://pkg.pr.new/template/b01c853c-1935-42b1-a52e-f429be62ede5">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp]" value="https://pkg.pr.new/template/3710e294-1ead-45e5-a376-1eaaf380e4f3">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp]" value="https://pkg.pr.new/template/c1c528cc-a865-4281-8e0d-a13302c81a82">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp]" value="https://pkg.pr.new/template/fd31202c-54bc-4ae3-b629-ab590d33bd1c">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp]" value="https://pkg.pr.new/template/fc855e40-6e6b-4fd2-ada3-d4f5215df119">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp]" value="https://pkg.pr.new/template/104bb1ae-3848-4815-9c0a-24f8cf839054">
<input type="hidden" name="project[files][android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp]" value="https://pkg.pr.new/template/b84a6d8e-e70c-4cab-be1e-5a20e26ef87a">
<input type="hidden" name="project[files][android/app/src/main/res/values/colors.xml]" value="&lt;resources&gt;
  &lt;color name=&quot;splashscreen_background&quot;&gt;#FFFFFF&lt;/color&gt;
  &lt;color name=&quot;colorPrimary&quot;&gt;#023c69&lt;/color&gt;
  &lt;color name=&quot;colorPrimaryDark&quot;&gt;#ffffff&lt;/color&gt;
&lt;/resources&gt;">
<input type="hidden" name="project[files][android/app/src/main/res/values/strings.xml]" value="&lt;resources&gt;
  &lt;string name=&quot;app_name&quot;&gt;Shopping List Demo&lt;/string&gt;
&lt;/resources&gt;">
<input type="hidden" name="project[files][android/app/src/main/res/values/styles.xml]" value="&lt;resources xmlns:tools=&quot;http://schemas.android.com/tools&quot;&gt;
  &lt;style name=&quot;AppTheme&quot; parent=&quot;Theme.AppCompat.DayNight.NoActionBar&quot;&gt;
    &lt;item name=&quot;android:editTextBackground&quot;&gt;@drawable/rn_edit_text_material&lt;/item&gt;
    &lt;item name=&quot;android:windowOptOutEdgeToEdgeEnforcement&quot; tools:targetApi=&quot;35&quot;&gt;true&lt;/item&gt;
    &lt;item name=&quot;colorPrimary&quot;&gt;@color/colorPrimary&lt;/item&gt;
    &lt;item name=&quot;android:statusBarColor&quot;&gt;#ffffff&lt;/item&gt;
  &lt;/style&gt;
  &lt;style name=&quot;Theme.App.SplashScreen&quot; parent=&quot;AppTheme&quot;&gt;
    &lt;item name=&quot;android:windowBackground&quot;&gt;@drawable/ic_launcher_background&lt;/item&gt;
  &lt;/style&gt;
&lt;/resources&gt;">
<input type="hidden" name="project[files][android/app/src/main/res/values-night/colors.xml]" value="&lt;resources/&gt;">
<input type="hidden" name="project[files][android/app/src/main/java/com/tanstack/shoppinglist/MainActivity.kt]" value="package com.tanstack.shoppinglist

import android.os.Build
import android.os.Bundle

import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate

import expo.modules.ReactActivityDelegateWrapper

class MainActivity : ReactActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    // Set the theme to AppTheme BEFORE onCreate to support
    // coloring the background, status bar, and navigation bar.
    // This is required for expo-splash-screen.
    setTheme(R.style.AppTheme);
    super.onCreate(null)
  }

  /**
   * Returns the name of the main component registered from JavaScript. This is used to schedule
   * rendering of the component.
   */
  override fun getMainComponentName(): String = &quot;main&quot;

  /**
   * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
   * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
   */
  override fun createReactActivityDelegate(): ReactActivityDelegate {
    return ReactActivityDelegateWrapper(
          this,
          BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
          object : DefaultReactActivityDelegate(
              this,
              mainComponentName,
              fabricEnabled
          ){})
  }

  /**
    * Align the back button behavior with Android S
    * where moving root activities to background instead of finishing activities.
    * @see &lt;a href=&quot;https://developer.android.com/reference/android/app/Activity#onBackPressed()&quot;&gt;onBackPressed&lt;/a&gt;
    */
  override fun invokeDefaultOnBackPressed() {
      if (Build.VERSION.SDK_INT &lt;= Build.VERSION_CODES.R) {
          if (!moveTaskToBack(false)) {
              // For non-root activities, use the default implementation to finish them.
              super.invokeDefaultOnBackPressed()
          }
          return
      }

      // Use the default back button implementation on Android S
      // because it&#39;s doing more than [Activity.moveTaskToBack] in fact.
      super.invokeDefaultOnBackPressed()
  }
}
">
<input type="hidden" name="project[files][android/app/src/main/java/com/tanstack/shoppinglist/MainApplication.kt]" value="package com.tanstack.shoppinglist

import android.app.Application
import android.content.res.Configuration

import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader

import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper

class MainApplication : Application(), ReactApplication {

  override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
        this,
        object : DefaultReactNativeHost(this) {
          override fun getPackages(): List&lt;ReactPackage&gt; {
            val packages = PackageList(this).packages
            // Packages that cannot be autolinked yet can be added manually here, for example:
            // packages.add(MyReactNativePackage())
            return packages
          }

          override fun getJSMainModuleName(): String = &quot;.expo/.virtual-metro-entry&quot;

          override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG

          override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
          override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
      }
  )

  override val reactHost: ReactHost
    get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)

  override fun onCreate() {
    super.onCreate()
    SoLoader.init(this, OpenSourceMergedSoMapping)
    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
      // If you opted-in for the New Architecture, we load the native entry point for this app.
      load()
    }
    ApplicationLifecycleDispatcher.onApplicationCreate(this)
  }

  override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
  }
}
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="shopping-list-react-native">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>