Skip to content
Open
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
51 changes: 51 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Android app
on:
# Feature-branch pushes are covered by the pull_request run for the same head.
push:
branches: [main]
paths: ['android/**', 'ios/Sources/Rupert/Resources/path-configuration.json', '.github/workflows/android.yml']
pull_request:
paths: ['android/**', 'ios/Sources/Rupert/Resources/path-configuration.json', '.github/workflows/android.yml']
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
name: Android build, tests and lint
runs-on: ubuntu-latest
# Typically 6-10 minutes (most of it the first Gradle and Robolectric downloads).
timeout-minutes: 30
defaults:
run:
working-directory: android
env:
# The Play build number; Play requires it to grow with every upload.
RUPERT_VERSION_CODE: ${{ github.run_number }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: '21'
- uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1
with:
# Pull requests read the cache that main writes; they never write it.
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Unit and Robolectric tests, lint
run: ./gradlew --no-daemon testDebugUnitTest lintDebug
- name: Debug APK and unsigned release bundle
# No signing secrets in CI: the release bundle is signed by the owner (android/README.md).
run: ./gradlew --no-daemon assembleDebug bundleRelease
- name: Keep the APK, bundle and reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rupert-android-${{ github.sha }}
path: |
android/app/build/outputs/apk/debug/*.apk
android/app/build/outputs/bundle/release/*.aab
android/app/build/reports/
if-no-files-found: warn
retention-days: 14
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ Square ───┼── sync engine ──▶ SQLite DB ──▶ GUI
| Path | Purpose |
| --- | --- |
| `web/` | The Rails 8.1 app: sync engine services, the dashboard GUI (`app/assets/tailwind/application.css` holds the palette; `bin/check-palette-contrast` verifies it), JSON APIs |
| `mobile/` | Staff iPhone/Android app (see `mobile/README.md`) |
| `ios/` | Rupert for iPhone and iPad: Hotwire Native shell around a Rupert instance (see `ios/README.md`) |
| `android/` | Rupert for Android: the same app for Android phones and tablets (see `android/README.md`) |
| `mobile/` | Staff timesheet app for iPhone/Android, Expo (see `mobile/README.md`) |
| `docs/` | Docs site, plans, audits and research (see `docs/README.md`) |
| `deploy/` | SER8 (Incus) systemd units (web, jobs, backup) and host-side watcher/backup scripts (see Production and deployment) |
| `bin/` | Host scripts (`backup.sh`) |
Expand Down
3 changes: 3 additions & 0 deletions android/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
gradlew text eol=lf
*.bat text eol=crlf
*.jar binary
15 changes: 15 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.gradle/
.kotlin/
build/
local.properties
captures/
*.iml
.idea/
.cxx/
# Signing material never belongs in this public repository.
*.jks
*.keystore
keystore.properties
*.p12
*.pem
play-service-account*.json
47 changes: 47 additions & 0 deletions android/PlayStore/checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Google Play: what the owner does

Everything here happens in the owner's Google accounts; nothing has been created or uploaded. **Done**
= in this repo or the build; **Owner** = a click in Play Console or a secret only the owner has.

| # | Item | Status | Where |
|---|---|---|---|
| 1 | Google Play developer account (one-time US$25). Choose **Organization** (needs a D-U-N-S number) so the app skips the personal-account rule of a 12-tester, 14-day closed test before production. | Owner | play.google.com/console/signup |
| 2 | Identity and developer verification, contact email `sdolbec@rupertpos.com`, website `https://rupertpos.com` | Owner | Play Console → Account details |
| 3 | Create app: name `Rupert: Shop Back Office`, default language English (US), App, Free | Owner | Play Console → Create app |
| 4 | Upload key (`keytool`, kept outside the repo) and a signed bundle | Owner, steps in `../README.md` → Release signing | `./gradlew bundleRelease` |
| 5 | Play App Signing: accept Google-managed app signing key on the first upload | Owner | Test and release → App integrity |
| 6 | Internal testing release with the bundle; add the owner's own Google account as tester | Owner | Test and release → Internal testing |
| 7 | App Links: put the App signing key SHA-256 in `RUPERT_ANDROID_CERT_SHA256` on the server and deploy | Owner | `../README.md` → App Links |
| 8 | Store listing: texts, icon, feature graphic, phone screenshots | Done (files) · Owner pastes and uploads | `listing.md`, `icon-512.png`, `feature-graphic.png`, `screenshots/` |
| 9 | Privacy policy URL `https://pos.rupertpos.com/privacy` | Done (page) | App content → Privacy policy |
| 10 | App access: sign-in needed, review login | Notes done · Owner runs the demo task and types the password | `review-notes.md` |
| 11 | Ads: **No ads** | Owner clicks | App content → Ads |
| 12 | Content rating questionnaire (IARC) | Owner clicks, answers in `data-safety.md` | App content → Content rating |
| 13 | Target audience: **18 and over** only; not designed for children | Owner clicks | App content → Target audience |
| 14 | Data safety form | Owner clicks, answers in `data-safety.md` | App content → Data safety |
| 15 | Account deletion URL `https://pos.rupertpos.com/account/delete` (web, after signing in) and in-app path | Done | App content → Data safety → Data deletion |
| 16 | Financial features: **My app doesn't provide any financial features** (no payments in the app) | Owner clicks | App content → Financial features |
| 17 | Government app, health, news, COVID: No | Owner clicks | App content |
| 18 | Category **Business**; contact email `sdolbec@rupertpos.com` | Owner clicks | Store settings |
| 19 | Countries: United States (or all) | Owner clicks | Production → Countries |
| 20 | Production release: promote the tested build, **managed publishing on** so nothing goes live until you press Publish | Owner only | Publishing overview |

## Before each upload

- `./gradlew testDebugUnitTest lintDebug` and the CI job are green.
- Install the release build on a phone (`bundletool` or an internal testing link) and sign in with
email and with Google, open a workspace if you have one, scan a barcode on an inventory count,
turn the lock on and off.
- Raise `RUPERT_VERSION_CODE`.

## Policies to keep in mind

- **Payments:** the app sells nothing; billing and sign-up are hidden inside the app
(`hotwire_native_app?`), as on iOS. Keep it that way, or Play Billing rules apply.
- **Webview apps:** Play accepts apps built around a website when they add app functionality. This
one adds the native scanner, lock, tabs, settings, shortcuts and server picker; the review notes
list them.
- **Ask Rupert / @rupert** send the person's question and business data to a cloud AI provider that
processes it for Rupert. A provider acting for you is not "sharing" on the Data safety form, but the
privacy policy must say it (it does today). Decide before submitting whether that holds for the
provider you use.
52 changes: 52 additions & 0 deletions android/PlayStore/data-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Data safety and content rating answers

## Data safety (Play Console → App content → Data safety)

Google counts what Rupert's server collects through the app, not only the app's own code. The app
itself adds nothing: no analytics, ads, crash reporting or tracking SDKs, and no runtime permissions.
The barcode scanner runs on the phone inside Google Play services and sends no images to Rupert.

**Does your app collect or share any of the required user data types?** Yes.
**Is all of the user data collected by your app encrypted in transit?** Yes (HTTPS only; plain http
only to a server on the person's own device or local network, which they choose).
**Do you provide a way for users to request that their data is deleted?** Yes: in the app (App
settings → Delete account) and on the web at `https://pos.rupertpos.com/account/delete`.

| Category → type | Collected | Shared | Optional? | Purpose |
|---|---|---|---|---|
| Personal info → Name | Yes | No | Required | App functionality, Account management |
| Personal info → Email address | Yes | No | Required | App functionality, Account management |
| Personal info → User IDs | Yes | No | Required | App functionality, Account management |
| Photos and videos → Photos, Videos (attachments) | Yes | No | Optional | App functionality |
| Files and docs (attachments, receipts) | Yes | No | Optional | App functionality |
| Messages → Other in-app messages (Team chat) | Yes | No | Optional | App functionality |
| App activity → Other user-generated content (Ask Rupert questions, business records) | Yes | No | Optional | App functionality |
| App info and performance → Diagnostics (page details sent with Report a problem) | Yes | No | Optional | App functionality |

Collected data is processed ephemerally? **No** (it is stored in the business's Rupert).
Not collected: location, financial info of the person, health, contacts, calendar, audio, web
browsing, installed apps, device or other IDs, crash logs.

## Content rating (IARC questionnaire)

Category: **Reference, News, or Educational**? No: choose **All Other App Types**.

| Question | Answer |
|---|---|
| Violence, fear, sexuality, language, controlled substances, gambling | No |
| Users can interact or exchange content | Yes (private team chat within a business) |
| Shares the user's current location with other users | No |
| Allows purchases of digital goods | No |
| Unrestricted internet access (a browser) | No (only Rupert's own sites; other links open in the browser) |

Merchant catalogs (for example a shop selling vapes or hemp products) can show product names in
the app; if a reviewer asks, the app is a business tool for the shop's own staff, not a store.

## Google Play services (barcode scanner)

The camera scanner is Google's code scanner, which runs inside Google Play services, not in Rupert.
Images stay on the phone. Google's ML Kit documentation says Play services may send Google
diagnostics about the scanner API itself (API usage and performance, no images or codes). Check
Google's current "ML Kit data disclosure" page when filling the form; if it still lists that, add
**App info and performance → Diagnostics** as collected for analytics by a third party (Google) as
the page directs.
Binary file added android/PlayStore/feature-graphic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/PlayStore/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions android/PlayStore/listing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Rupert: Google Play listing (en-US)

Paste into Play Console → Grow users → Store presence → Main store listing. Limits in brackets.

| Field | Value |
|---|---|
| App name [30] | `Rupert: Shop Back Office` |
| Short description [80] | `Sales, stock counts and your team, in one place for independent shops.` |
| App icon (512 × 512 PNG) | `icon-512.png` |
| Feature graphic (1024 × 500) | `feature-graphic.png` |
| Phone screenshots (2–8) | `screenshots/*.png`, in file-name order |
| Category | Business |
| Tags | Business, Productivity, Inventory |
| Contact email | `sdolbec@rupertpos.com` |
| Website | https://rupertpos.com |
| Privacy policy | https://pos.rupertpos.com/privacy |

**Full description [4000]**

```
Rupert is the back office for independent shops. It brings your sales, inventory, people and bookkeeping into one place, and this app puts it in your pocket.

TODAY AT A GLANCE
• Your start page shows today's sales, what needs attention and what your team is working on.
• Notifications tell you when a count, an order or a teammate needs you.

COUNT STOCK WITH YOUR CAMERA
• Open an inventory count and scan barcodes with the phone's camera. Each scan saves straight to the count.
• Works alongside USB and Bluetooth scanners at the counter.

TEAM CHAT AND ASK RUPERT
• Talk with your team in Team chat, with photos and files.
• Ask Rupert questions about your business in plain words: "How did we do last Saturday?" or "What's running low?"

EVERYTHING IN ONE PLACE
• Sales and customers from the systems your shop already uses, such as Shopify and Square.
• Inventory, purchasing and counts across locations.
• Staff schedules, time clock and timesheets.
• Bookkeeping, cash flow and reports.

BUILT FOR THE SHOP FLOOR
• Lock the app with your fingerprint, face or screen lock, so a shared phone does not show your numbers.
• Long-press the app icon to jump straight to Team chat, Ask Rupert, Notifications or Search.
• Follows your workspace's colours and appearance, in light and dark.
• Works with pos.rupertpos.com or your business's own Rupert server.

Rupert is for businesses that use Rupert. Sign in with the account your business gave you, with Google or with email and password.
```

## Graphics

`icon-512.png` is the iPhone app's icon. `feature-graphic.png` is generated by
`screenshots/make-feature-graphic.sh`. Screenshots come from the emulator (Pixel 8, 1080 × 2400)
against the fictional review demo shop; remake them with `adb exec-out screencap -p > file.png`.
28 changes: 28 additions & 0 deletions android/PlayStore/review-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Play review: App access

Play Console → App content → App access → "All or some functionality is restricted" → add
instructions. Use the same fictional demo shop as App Review (`REVIEW_PASSWORD=… bin/rails
rupert:app_review_demo` on the server; see `ios/AppStore/review-notes.md`).

| Field | Value |
|---|---|
| Name | Rupert demo shop |
| Username | the review account's email (from `rupert:app_review_demo`) |
| Password | (the owner sets it) |
| Any other information | paste the notes below |

```
Rupert is back-office software for independent retail shops (sales, inventory, staff, bookkeeping). Accounts are created by a business, so we made a demo shop filled with sample data. No real customer data is in it.

To sign in: on the first screen, tap "Sign in with email and password" and use the username and password above. (Google sign-in is for business Google accounts.)

Native features to try:
1. Barcode scanning with the camera (Google code scanner): Home → menu → Inventory → Counts → open the draft count → Scan → "Scan with camera". The code is saved to the count.
2. App lock: tap the person icon at the top right of the Home tab → App settings → "Require screen lock". Leave and return to the app; it asks for your fingerprint, face or screen lock and hides its pages in recent apps.
3. Launcher shortcuts: long-press the Rupert icon → Team Chat, Ask Rupert, Notifications or Search.
4. Native tab bar with a separate back stack per tab, full-screen forms, pull to refresh, and a server picker (App settings → Rupert server) for businesses that run their own Rupert.

Account deletion: person icon on the Home tab → App settings → "Delete account". Please do not delete the review account.

The app sells nothing. Businesses subscribe on our website; the app shows no prices, plans or purchase links.
```
13 changes: 13 additions & 0 deletions android/PlayStore/screenshots/make-feature-graphic.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Makes the Play feature graphic (1024 × 500) from the app icon, with ImageMagick 7.
# android/PlayStore/screenshots/make-feature-graphic.sh [font]
set -euo pipefail
here=$(cd "$(dirname "$0")/.." && pwd)
font=${1:-/Library/Fonts/Roboto-Bold.ttf} # any TTF; Roboto is Android's own face
magick -size 1024x500 xc:'#C2D4CD' \
\( "$here/icon-512.png" -resize 360x360 \) -gravity west -geometry +70+0 -composite \
-font "$font" -fill '#17353A' -gravity west \
-pointsize 84 -annotate +480-60 'Rupert' \
-pointsize 34 -fill '#0E6F72' -annotate +484+30 'Shop back office' \
-pointsize 26 -fill '#17353A' -annotate +484+80 'Sales, stock and team, together' \
-strip "$here/feature-graphic.png"
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading