Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/android-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Android PR validation

on:
pull_request:
paths:
- .github/workflows/android-apk.yml
- android/**
- capacitor.config.ts
- index.html
- package.json
- pnpm-lock.yaml
- public/**
- shared/**
- src/**

permissions:
contents: read

jobs:
build-debug-apk:
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- name: Check out repository
uses: actions/checkout@v7
- name: Set up pnpm
uses: pnpm/action-setup@v6
with:
version: 11.24.0
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- name: Set up Java
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '21'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build and sync Android assets
run: pnpm android:sync
- name: Validate native compilation and signing configuration
working-directory: android
env:
ORG_GRADLE_PROJECT_itemlyPreview: 'true'
ORG_GRADLE_PROJECT_itemlyDurableSigning: 'true'
ITEMLY_SIGNING_STORE: ${{ runner.temp }}/itemly-test.p12
ITEMLY_SIGNING_ALIAS: itemly-preview
shell: bash
run: |
set -euo pipefail
umask 077
export ITEMLY_SIGNING_PASSWORD="$(openssl rand -hex 32)"
export ITEMLY_SIGNING_KEY_PASSWORD="$ITEMLY_SIGNING_PASSWORD"
keytool -genkeypair -keystore "$ITEMLY_SIGNING_STORE" -storetype PKCS12 -storepass:env ITEMLY_SIGNING_PASSWORD -keypass:env ITEMLY_SIGNING_KEY_PASSWORD -alias "$ITEMLY_SIGNING_ALIAS" -keyalg RSA -keysize 3072 -validity 2 -dname 'CN=Disposable CI fixture'
bash ./gradlew --no-daemon :app:assembleDebug
- name: Remove throwaway test key
if: always()
shell: bash
run: rm -f -- "$RUNNER_TEMP/itemly-test.p12"
# Compilation is automatic, distribution is not. Only the manually
# requested durable workflow uploads an APK intended for installation.
79 changes: 79 additions & 0 deletions .github/workflows/android-signed-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Android installable update APK

# Explicit opt-in only. Never expose signing credentials to PR workflows or
# pull_request_target, and never fall back to a runner-generated debug key.
on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: itemly-durable-android
cancel-in-progress: false

jobs:
build-update:
if: github.repository == 'Kamitoad/itemly' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- name: Check out explicitly selected trusted branch
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up pnpm
uses: pnpm/action-setup@v6
with:
version: 11.24.0
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- name: Set up Java
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '21'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check application and tests
run: pnpm check
- name: Build and sync Android assets
run: pnpm android:sync
- name: Build with durable preview identity
working-directory: android
env:
KEYSTORE_BASE64: ${{ secrets.ITEMLY_ANDROID_KEYSTORE_BASE64 }}
ITEMLY_SIGNING_PASSWORD: ${{ secrets.ITEMLY_ANDROID_KEYSTORE_PASSWORD }}
ITEMLY_SIGNING_KEY_PASSWORD: ${{ secrets.ITEMLY_ANDROID_KEYSTORE_PASSWORD }}
ITEMLY_SIGNING_ALIAS: itemly-preview
ITEMLY_SIGNING_STORE: ${{ runner.temp }}/itemly-preview.p12
ORG_GRADLE_PROJECT_itemlyPreview: 'true'
ORG_GRADLE_PROJECT_itemlyDurableSigning: 'true'
shell: bash
run: |
set -euo pipefail
test -n "$KEYSTORE_BASE64" && test -n "$ITEMLY_SIGNING_PASSWORD" || { echo 'Configure the two ITEMLY_ANDROID signing secrets first.'; exit 1; }
umask 077
printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$ITEMLY_SIGNING_STORE"
unset KEYSTORE_BASE64
export ORG_GRADLE_PROJECT_itemlyVersionCode="$(( $(date -u +%s) - 1704067200 ))"
export ORG_GRADLE_PROJECT_itemlyVersionName="0.1.$ORG_GRADLE_PROJECT_itemlyVersionCode"
bash ./gradlew --no-daemon :app:assembleDebug
mkdir -p ../update-apk
cp app/build/outputs/apk/debug/app-debug.apk "../update-apk/itemly-update-${ORG_GRADLE_PROJECT_itemlyVersionCode}.apk"
printf 'App ID: com.kamitoad.itemly.preview\nVersion code: %s\nCommit: %s\n' "$ORG_GRADLE_PROJECT_itemlyVersionCode" "$GITHUB_SHA" > ../update-apk/build-info.txt
keytool -exportcert -keystore "$ITEMLY_SIGNING_STORE" -storetype PKCS12 -storepass:env ITEMLY_SIGNING_PASSWORD -alias "$ITEMLY_SIGNING_ALIAS" | sha256sum >> ../update-apk/build-info.txt
- name: Remove temporary signing material
if: always()
shell: bash
run: rm -f -- "$RUNNER_TEMP/itemly-preview.p12"
- name: Upload installable update
uses: actions/upload-artifact@v7
with:
name: itemly-installable-update-${{ github.run_id }}
path: update-apk/*
if-no-files-found: error
retention-days: 30
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ data/
.DS_Store
.idea/
coverage/
.local/
*.p12
*.jks
*.keystore
*.pem
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- Android user-initiated full backup export via the system document picker, including receipt images, original extraction data, drafts, trash, audit history, and preferences.
- Validated, empty-database-only Android restore with a confirmation preview, image checksums, transactional writes, rollback, and failure cleanup.
- A manually dispatched durable-signed preview APK workflow and protected local signing setup, allowing later APKs to install as updates without deleting receipts.
- Backup corruption, restore failure, document cancellation, preference failure, and signing-boundary tests. Physical-device update/restore acceptance is still pending.

### Changed

- Automatic Android PR jobs now validate native compilation/signing with a throwaway key but do not distribute APKs; installable update artifacts are manually requested only.
- The receipt overview is now the home screen and lists purchases newest first.
- A floating “New receipt” button starts capture from anywhere in the overview.
- Cards, date display, empty states, and responsive spacing were refined for more intuitive mobile navigation.
Expand Down
10 changes: 7 additions & 3 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ Itemly may store:
- Payment method, card brand, and at most the visible last four card digits
- Review status, uncertainties, technical source details, and personal notes

By default, this data is stored in the local `./data` directory. Structured data is stored in SQLite; images are stored in `receipts/`. The browser stores only the chosen appearance and the installable PWA shell.
In the web runtime, this data is stored in the local `./data` directory. Structured data is stored in SQLite; images are stored in `receipts/`. The browser stores only the chosen appearance and the installable PWA shell.

The experimental Android runtime instead stores receipts and original images in app-private storage on the phone. It does not call the PC API for core operations, and Android system cloud backup is disabled. Device-to-device transfer behavior can vary by Android manufacturer; explicit exclusion rules are also configured. Existing web-server data is not automatically migrated to Android. Uninstalling the app deletes its private data. The unreleased Android version includes user-initiated full export and empty-database restore; see [Android backup instructions](docs/ANDROID.md) and complete the real-device acceptance checks before relying on it.

## AI processing

Expand All @@ -28,11 +30,13 @@ If an operator configures an OpenAI-compatible provider, Itemly sends a selected

## Sharing and tracking

Itemly contains no ads, telemetry, or built-in analytics. Without a configured AI provider, Itemly does not transmit purchase data to third parties.
Itemly contains no ads, telemetry, or built-in analytics. Without a configured AI provider, Itemly does not automatically transmit purchase data to third parties. A user may explicitly choose a cloud-backed document provider when exporting an Android backup; that provider receives the selected backup under its own terms. Itemly does not silently connect a cloud account.

## Backups and deletion

Backups contain the complete SQLite database and available receipt images. Treat them as confidential as the local data directory. The operator is responsible for deleting or moving local data and backups. Version 0.1.0 does not yet offer deletion in the UI.
Web-server backups contain the complete SQLite database and available receipt images. Android backups use a separate, versioned logical JSON format and include stored drafts, confirmed receipts, trash, images, original extraction data, audit history, and relevant appearance/currency preferences. They are unencrypted and can contain sensitive purchase/payment details. Save them outside app-private storage if they must survive uninstall, and protect access to the exported file. Selected external document access is explicit; no broad storage permission is required. Temporary private copies are removed on ordinary completion; an OS interruption may leave cache files until Android clears them.

The unreleased UI supports recoverable receipt trash, not permanent erasure. Trash and previous values in audit history are still included in backups. Deleting a receipt from the normal history does not erase these copies. The user/operator remains responsible for deleting exported backup files and protecting the device/local data directory. Until physical-device update and backup/restore acceptance passes, do not use the Android prototype as the only copy of important data.

## Network use

Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

Itemly is a mobile-first, self-hosted web app for structured purchase data. Receipt images and data stay on your own computer by default. You can enter receipts manually, use a free copy-and-paste workflow with a regular ChatGPT chat, or optionally configure an OpenAI-compatible API provider.

An experimental Android package is now being developed for phone-local use without a running computer. It is not yet a verified release; see [Android prototype](docs/ANDROID.md) before installing or relying on it.

Android now includes **Daten & Sicherung** for user-initiated full export/restore and a separate **Android installable update APK** workflow for durable-signed updates. Signing setup and device acceptance are required; do not alternate durable update APKs with disposable PR artifacts. Android and webserver backups currently use different formats.

![Itemly in dark mode](docs/images/itemly-home.jpg)

## Why Itemly?
Expand All @@ -24,7 +28,9 @@ Many receipt apps require an account, cloud storage, or a paid AI service. Iteml
- Camera and gallery selection with preview and explicit consent before external processing
- Validated JSON import from a regular ChatGPT chat
- A fully manual workflow without AI access
- Editing of merchant, item, payment, and total details during review
- Editing of merchant, item, payment, and total details during review and after saving
- Item creation, editing, and removal within an existing receipt
- Receipt trash and restoration without losing original images or extraction evidence
- Uncertainty markers for extracted items and fields
- Arithmetic checks for line items, discounts, taxes, deposits, and fees
- Drafts and atomic, idempotent saves of balanced purchases
Expand Down Expand Up @@ -62,6 +68,10 @@ The complete app is then served at `http://localhost:8787`.

Itemly v0.1.0 has no user accounts or access control. Run the server only on a private computer or trusted home network unless you add suitable access protection. Do not expose it directly to the public internet.

## Experimental Android build

The Android project is in `android/`. It bundles the React UI and routes receipt capture, manual entry, ChatGPT JSON import, saves, history, and detail views to app-private storage rather than the PC API. After installing Android Studio and its SDK, run `pnpm android:sync`, open the project with `pnpm android:open`, and deploy a debug build to a device. The [Android prototype guide](docs/ANDROID.md) lists the required real-device tests and current limitations. Existing receipts in the PC database are **not** automatically copied to the phone. Phone backup/restore is not implemented yet; do not treat the prototype as the only copy of important receipts.

## Capture receipts without an API key

1. Select **ChatGPT-JSON importieren** in Itemly.
Expand Down
9 changes: 9 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ Where possible, include the affected version, reproduction steps, potential impa
- Original values and arithmetic discrepancies remain visible.

These limits describe the current design. They do not imply that public multi-user hosting is supported safely.

## Unreleased Android backup and signing boundaries

- Backup imports are untrusted data, not SQL or instructions. They are validated and restored only to an empty local database; image digests detect corruption, not authenticity. Import only files you trust.
- Backups contain personal data in unencrypted JSON, including trash and audit history. Keep exported documents confidential and outside app-private storage if they must survive uninstall.
- The durable preview signing key/password are local ignored recovery files plus GitHub Actions secrets, never repository source. Protect both copies. Do not rotate the key to fix an installation error.
- Durable signing runs only in the manually dispatched trusted workflow. PR validation uses a throwaway test key without repository credentials and distributes no APK.
- Repository write access is trusted; protect the signing environment with approval requirements before onboarding untrusted contributors. Fork isolation alone does not protect secrets from same-repository workflow changes.
- Preview APKs are still debuggable development builds, not hardened production releases. Physical-device update and restore acceptance is pending.
103 changes: 103 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore

# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

# Signing keys must never be committed.
*.jks
*.keystore
*.p12
*.pem

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# Google Services (e.g. APIs or Firebase)
# Firebase configuration is not used by this prototype.
google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/

# Android Profiling
*.hprof

# Cordova plugins for Capacitor
capacitor-cordova-android-plugins

# Copied web assets
app/src/main/assets/public

# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
2 changes: 2 additions & 0 deletions android/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build/*
!/build/.npmkeep
Loading
Loading