Remote sync of webdav
This commit is contained in:
parent
f01186df79
commit
483fbeafb6
44 changed files with 4842 additions and 975 deletions
329
README.md
329
README.md
|
|
@ -4,28 +4,48 @@ A Flutter app (Android + iOS) for logging fuel purchases per vehicle. Snap a
|
|||
photo of a gas receipt, it OCRs the gallons/price-per-gallon/total on-device,
|
||||
you confirm or correct the numbers, and it's saved alongside the receipt
|
||||
photo. Vehicles and fuel entries live in a local SQLite database that's kept
|
||||
in sync with a shared Google Drive folder, so multiple people can log fuel
|
||||
against the same pool of vehicles from their own phones, offline or online.
|
||||
in sync with a shared folder on **Google Drive, Dropbox, OneDrive, or your
|
||||
own WebDAV server** (your choice), so multiple people can log fuel against
|
||||
the same pool of vehicles from their own phones, offline or online.
|
||||
|
||||
## Features
|
||||
|
||||
- Manage a list of vehicles (make, model, color, license plate).
|
||||
- Capture a fuel receipt photo per vehicle and OCR it on-device with Google
|
||||
ML Kit — no internet connection or API key required for the OCR itself.
|
||||
- Review/edit the parsed gallons, price per gallon, and total cost before
|
||||
saving (OCR on printed receipts is usually good but not perfect).
|
||||
- Manage a list of vehicles, identified by **VIN** (required, and unique
|
||||
across active vehicles — but editable, e.g. to fix a typo from a misread
|
||||
scan; scan it from a photo of the door-jamb sticker or dashboard plate
|
||||
instead of typing all 17 characters). An optional **nickname** ("Mom's
|
||||
Car", "Red Ford F-150") is what's shown as the primary label everywhere;
|
||||
without one, the VIN is shown instead.
|
||||
- Capture a fuel receipt photo per vehicle — from the camera or an existing
|
||||
photo in your library — and OCR it on-device with Google ML Kit — no
|
||||
internet connection or API key required for the OCR itself.
|
||||
- If a receipt's address shows a state other than Missouri, a warning
|
||||
explains that the fuel tax refund only covers Missouri purchases and lets
|
||||
you choose whether to log the entry anyway.
|
||||
- Review/edit the parsed gallons, price per gallon, total cost, *and*
|
||||
date/time before saving (OCR on printed receipts is usually good but not
|
||||
perfect; the date/time picker is always available as a manual fallback
|
||||
when a date can't be found on the receipt).
|
||||
- Per-vehicle fuel log with running gallons total, tap-to-zoom receipt
|
||||
photos, and delete.
|
||||
- Connect Google Drive and pick any folder you have access to — including
|
||||
one someone else created and shared with you — as the shared storage
|
||||
location. The app looks for (or creates) a `MO-Fuel-Tax-Back` subfolder
|
||||
there, so anyone pointed at the same shared parent converges on the same
|
||||
data automatically.
|
||||
- Connect **Google Drive, Dropbox, OneDrive, or a self-hosted WebDAV
|
||||
server** (Nextcloud, ownCloud, a Synology NAS, or any generic WebDAV
|
||||
endpoint) and pick any folder you have access to — including one someone
|
||||
else created and shared with you — as the shared storage location. The
|
||||
app looks for (or creates) a `MO-Fuel-Tax-Back` subfolder there, so
|
||||
anyone pointed at the same shared parent converges on the same data
|
||||
automatically, regardless of which of the four providers each person is
|
||||
using. WebDAV is the only one of the four that needs no developer-console
|
||||
setup at all — just a server URL, username, and password.
|
||||
- Works fully offline: writes always land in the local SQLite database
|
||||
first; a background sync pushes changes to Drive and pulls others'
|
||||
first; a background sync pushes changes to the cloud and pulls others'
|
||||
changes down once online.
|
||||
- Once a receipt photo is uploaded to Drive, the local copy is deleted;
|
||||
viewing it later downloads it fresh from Drive on demand.
|
||||
- Settings lets you choose whether a receipt photo's local copy is deleted
|
||||
once it's safely uploaded to the cloud (the default — keeps the phone's
|
||||
storage footprint small) or kept on the phone for offline viewing.
|
||||
- Settings → Advanced lets you tune the sync lock's staleness timeout (how
|
||||
long before another device's abandoned lock is cleared so sync isn't
|
||||
stuck waiting forever), from 1–60 minutes.
|
||||
|
||||
## Project layout
|
||||
|
||||
|
|
@ -33,9 +53,14 @@ against the same pool of vehicles from their own phones, offline or online.
|
|||
lib/
|
||||
models/ Vehicle, FuelEntry — plain data classes with SQLite row (de)serialization
|
||||
(toMap/fromMap). Both carry `updatedAt` (merge conflict resolution) and
|
||||
`deletedAt` (a soft-delete tombstone, so deletions sync too). FuelEntry
|
||||
carries either a local receiptImagePath (not yet uploaded) or a
|
||||
receiptDriveFileId (uploaded, local copy removed).
|
||||
`deletedAt` (a soft-delete tombstone, so deletions sync too). Vehicle's
|
||||
`id` is a hidden, generated, immutable primary key — never shown in the
|
||||
UI — that FuelEntry.vehicleId references and sync merges on; `vin` is a
|
||||
required, unique-among-active-vehicles, but user-editable field, and
|
||||
`nickname` is optional. FuelEntry can hold a local `receiptImagePath`, a
|
||||
`receiptDriveFileId`, or both at once if "keep photos on this phone" is
|
||||
on — see `needsReceiptUpload` vs. `isReceiptUploadedToDrive` for the
|
||||
distinction.
|
||||
services/
|
||||
app_state.dart In-memory read cache + CRUD, exposed via Provider. Stamps
|
||||
updatedAt on mutations and triggers background sync.
|
||||
|
|
@ -44,28 +69,59 @@ lib/
|
|||
ApplicationDocumentsDirectory/FuelTaxTracker.
|
||||
db_schema.dart CREATE TABLE statements and the merge SQL — shared between
|
||||
the app and its tests so they can never drift apart.
|
||||
drive_auth_service.dart Google sign-in + builds an authenticated http.Client
|
||||
for the Drive API.
|
||||
drive_service.dart Raw Drive API calls: browse folders, find-or-create
|
||||
the MO-Fuel-Tax-Back folder, upload/download files,
|
||||
lock file operations, cheap md5-based change detection.
|
||||
drive_sync_service.dart Orchestrates one sync round: acquire the cross-device
|
||||
lock, ATTACH + merge the remote database into the local
|
||||
one, upload pending receipt photos, push the local
|
||||
database back up, release the lock.
|
||||
cloud/
|
||||
cloud_storage_provider.dart The CloudStorageProvider/CloudStorageSession
|
||||
interface every backend implements (auth, folder
|
||||
browsing, file upload/download, lock file ops) —
|
||||
this is what cloud_sync_service.dart and the rest
|
||||
of the app talk to; they never know which of the
|
||||
three providers below is actually active.
|
||||
google_drive_provider.dart Google sign-in + the googleapis DriveApi client.
|
||||
dropbox_provider.dart Hand-rolled OAuth2 PKCE + Dropbox's path-addressed
|
||||
REST API (files/list_folder, files/upload, etc).
|
||||
onedrive_provider.dart Hand-rolled OAuth2 PKCE + Microsoft Graph
|
||||
(/me/drive/...), ID-addressed like Drive.
|
||||
webdav_provider.dart Generic WebDAV (PROPFIND/MKCOL/PUT/GET/DELETE)
|
||||
over HTTP Basic Auth — no OAuth, no developer
|
||||
console, just a server URL + username + password.
|
||||
For self-hosted servers (Nextcloud, ownCloud, a
|
||||
Synology NAS, etc). Path-addressed like Dropbox;
|
||||
uses the resource's ETag as its versionTag.
|
||||
oauth_pkce.dart PKCE code_verifier/code_challenge generation, shared
|
||||
by the Dropbox and OneDrive providers (Google's own
|
||||
SDK handles its OAuth flow itself, and WebDAV uses
|
||||
plain Basic Auth, so neither needs this).
|
||||
cloud_oauth_config.dart Fill in your OAuth client IDs/keys here for whichever
|
||||
provider(s) you want to use — see setup below.
|
||||
cloud_sync_service.dart Orchestrates one sync round against whichever
|
||||
CloudStorageProvider is active: acquire the
|
||||
cross-device lock, ATTACH + merge the remote database
|
||||
into the local one, upload pending receipt photos,
|
||||
push the local database back up, release the lock.
|
||||
lock_coordinator.dart The ticket-based lock-file mutex, as pure injectable
|
||||
logic (independently unit-tested without real Drive).
|
||||
drive_oauth_config.dart Fill in your OAuth client IDs here — see setup below.
|
||||
logic (independently unit-tested without a real backend).
|
||||
ocr_service.dart Thin wrapper around google_mlkit_text_recognition.
|
||||
receipt_parser.dart Regex-based extraction of gallons/price/total from OCR text.
|
||||
receipt_parser.dart Regex-based extraction of gallons/price/total/date from OCR
|
||||
text — falls back to manual entry (date picker, or the
|
||||
gallons*price derivation) for whatever isn't found.
|
||||
screens/ One file per screen (vehicle list, add/edit vehicle, vehicle detail,
|
||||
confirm fuel entry, receipt viewer, settings, Drive folder browser).
|
||||
confirm fuel entry, receipt viewer, settings, cloud folder browser).
|
||||
```
|
||||
|
||||
## Manual setup required (Google Cloud Console)
|
||||
## Manual setup required (cloud storage)
|
||||
|
||||
This app needs OAuth credentials you create yourself — I can't provision
|
||||
cloud resources on your behalf. In [Google Cloud Console](https://console.cloud.google.com/):
|
||||
You only need to complete setup for whichever provider(s) you actually want
|
||||
to offer in the app. Google Drive, Dropbox, and OneDrive all follow the same
|
||||
shape: I can't provision cloud resources on your behalf, so each needs an
|
||||
app/OAuth client you create yourself in that provider's own developer
|
||||
console, with the resulting IDs pasted into
|
||||
`lib/services/cloud_oauth_config.dart` — Settings only shows a "Connect"
|
||||
button for one of these three once its config fields are filled in with
|
||||
real values. **WebDAV needs none of this** — see its section below.
|
||||
|
||||
### Google Drive (Google Cloud Console)
|
||||
|
||||
In [Google Cloud Console](https://console.cloud.google.com/):
|
||||
|
||||
1. Create/select a project, enable the **Google Drive API** (APIs & Services
|
||||
→ Library).
|
||||
|
|
@ -89,12 +145,84 @@ cloud resources on your behalf. In [Google Cloud Console](https://console.cloud.
|
|||
`serverClientId`, even for a mobile app).
|
||||
- **iOS**: bundle ID matching the Xcode project. Copy its **Client ID**
|
||||
and note the reversed form (`com.googleusercontent.apps.<...>`).
|
||||
5. Fill in `lib/services/drive_oauth_config.dart`:
|
||||
- `androidServerClientId` ← the **Web application** client's ID.
|
||||
- `iosClientId` ← the **iOS** client's ID.
|
||||
5. Fill in `lib/services/cloud_oauth_config.dart`:
|
||||
- `googleAndroidServerClientId` ← the **Web application** client's ID.
|
||||
- `googleIosClientId` ← the **iOS** client's ID.
|
||||
6. Replace the placeholder in `ios/Runner/Info.plist`'s `CFBundleURLTypes` →
|
||||
`CFBundleURLSchemes` with your iOS client's reversed ID.
|
||||
|
||||
### Dropbox (Dropbox App Console)
|
||||
|
||||
Dropbox has no official Flutter sign-in SDK, so this uses a hand-built
|
||||
OAuth2 Authorization Code + PKCE flow (no client secret needed — safe for a
|
||||
public/mobile app) via `flutter_web_auth_2`, which opens the system browser
|
||||
and catches the redirect through a custom URL scheme already registered in
|
||||
`AndroidManifest.xml` / `Info.plist`.
|
||||
|
||||
1. Create an app at [dropbox.com/developers/apps](https://www.dropbox.com/developers/apps).
|
||||
2. Access type: **Full Dropbox** (not "App folder") — needed because
|
||||
browsing to and reusing a folder someone *else* created and shared
|
||||
requires seeing the whole account, the same reasoning as Google's full
|
||||
`drive` scope above.
|
||||
3. Under **OAuth 2** → **Redirect URIs**, add
|
||||
`mofueltaxback-dropbox://oauth2redirect` (this exact scheme is already
|
||||
wired up in the Android manifest and iOS Info.plist; use a different one
|
||||
only if you also update those two files to match).
|
||||
4. Copy the app's **App key** from the Settings tab.
|
||||
5. Fill in `lib/services/cloud_oauth_config.dart`:
|
||||
- `dropboxAppKey` ← the App key.
|
||||
- `dropboxRedirectUri` ← `mofueltaxback-dropbox://oauth2redirect`.
|
||||
6. While the app is in **Development** status, only your own Dropbox
|
||||
account can sign in; add teammates under the app's **Permissions** /
|
||||
member-access settings, or apply for **Production** status, once you're
|
||||
ready to share it with others.
|
||||
|
||||
### OneDrive (Azure Portal / Microsoft Graph)
|
||||
|
||||
Same PKCE approach as Dropbox, against the Microsoft identity platform and
|
||||
Microsoft Graph.
|
||||
|
||||
1. In [Azure Portal](https://portal.azure.com/) → **Microsoft Entra ID** →
|
||||
**App registrations** → **New registration**.
|
||||
2. Supported account types: **Personal Microsoft accounts only** (or "any
|
||||
organizational directory and personal Microsoft accounts" if you also
|
||||
want work/school accounts to be able to sign in — those may additionally
|
||||
need their tenant admin's consent for the scopes below).
|
||||
3. Under **Authentication** → **Add a platform** → **Mobile and desktop
|
||||
applications**, add the redirect URI `mofueltaxback-onedrive://auth`
|
||||
(this exact scheme is already wired up in the Android manifest and iOS
|
||||
Info.plist; use a different one only if you also update those two files
|
||||
to match).
|
||||
4. Under **API permissions**, add Microsoft Graph **delegated** permissions
|
||||
`Files.ReadWrite.All` and `offline_access` (the latter is required to get
|
||||
a refresh token back from the token endpoint).
|
||||
5. Copy the **Application (client) ID** from the Overview page.
|
||||
6. Fill in `lib/services/cloud_oauth_config.dart`:
|
||||
- `oneDriveClientId` ← the Application (client) ID.
|
||||
- `oneDriveRedirectUri` ← `mofueltaxback-onedrive://auth`.
|
||||
|
||||
### WebDAV (self-hosted — no developer console needed)
|
||||
|
||||
This is the option for a personal cloud server you already run — Nextcloud,
|
||||
ownCloud, a Synology/QNAP NAS's built-in WebDAV support, or a bare
|
||||
Apache/nginx WebDAV endpoint. There's no OAuth app to register anywhere;
|
||||
tapping "Connect WebDAV" in Settings just opens a form asking for:
|
||||
|
||||
- **Server URL** — the full WebDAV endpoint, e.g.
|
||||
`https://cloud.example.com/remote.php/dav/files/yourusername/` for
|
||||
Nextcloud/ownCloud, or whatever your NAS's WebDAV documentation gives you.
|
||||
`https://` is assumed if you omit the scheme.
|
||||
- **Username** and **Password** — for servers that support app-specific
|
||||
passwords (Nextcloud: Settings → Security → "Create new app password"),
|
||||
use one of those instead of your real account password, so this app can
|
||||
be revoked independently later.
|
||||
|
||||
The app verifies the URL and credentials with a harmless `PROPFIND` request
|
||||
before treating you as signed in, so a typo or wrong password fails
|
||||
immediately with a clear error rather than surfacing later during sync.
|
||||
Nothing needs to be filled in `cloud_oauth_config.dart` or the
|
||||
Android/iOS manifest files for this provider.
|
||||
|
||||
## Running it
|
||||
|
||||
```
|
||||
|
|
@ -111,19 +239,27 @@ flutter build ios --release # iOS (requires a full Xcode install + signing
|
|||
|
||||
This was scaffolded and verified with Flutter 3.44.9. `flutter analyze` and
|
||||
`flutter test` are clean, and `flutter build apk --debug` has been confirmed
|
||||
to produce a working APK. The Drive sign-in/sync path needs the manual
|
||||
OAuth setup above before it can be exercised end-to-end.
|
||||
to produce a working APK. None of the three providers' sign-in/sync paths
|
||||
can be exercised end-to-end until you've done that provider's manual OAuth
|
||||
setup above.
|
||||
|
||||
## Permissions
|
||||
|
||||
- **Camera**: `NSCameraUsageDescription` (iOS, `ios/Runner/Info.plist`) and
|
||||
`android.permission.CAMERA` (Android, `AndroidManifest.xml`) are already
|
||||
set up for receipt capture.
|
||||
- **Network/Drive**: `INTERNET` and `ACCESS_NETWORK_STATE` are declared in
|
||||
the main Android manifest (needed for release builds; debug builds get
|
||||
`INTERNET` for free). No manifest changes are needed for `google_sign_in`
|
||||
itself when not using `google-services.json` — see the manual setup
|
||||
section above instead.
|
||||
- **Network**: `INTERNET` and `ACCESS_NETWORK_STATE` are declared in the
|
||||
main Android manifest (needed for release builds; debug builds get
|
||||
`INTERNET` for free).
|
||||
- **OAuth redirects**: no manifest changes are needed for `google_sign_in`
|
||||
itself when not using `google-services.json` — see the Google setup
|
||||
section above instead. Dropbox and OneDrive's browser-based OAuth redirect
|
||||
is already wired up via a `flutter_web_auth_2` callback activity
|
||||
(Android) / extra `CFBundleURLTypes` entries (iOS) for the
|
||||
`mofueltaxback-dropbox://` and `mofueltaxback-onedrive://` schemes — you
|
||||
only need to register the matching redirect URI in each provider's own
|
||||
console (see setup above), not touch these files, unless you deliberately
|
||||
choose different scheme names.
|
||||
|
||||
## How sync works
|
||||
|
||||
|
|
@ -134,29 +270,37 @@ Every row carries `updated_at` (for merge resolution), `deleted_at` (a
|
|||
soft-delete tombstone — see below), and a local-only `dirty` flag (pending
|
||||
push to Drive, never itself treated as meaningful sync data). Sync:
|
||||
|
||||
1. Creates a lock file `{email}-{utcEpochMillis}.lock` in the shared Drive
|
||||
1. Creates a lock file `{email}-{utcEpochMillis}.lock` in the shared cloud
|
||||
folder, then waits until no *other* lock file older than its own remains
|
||||
(polling every second, deleting any it finds older than 10 minutes as
|
||||
orphaned/stale) — a ticket-based mutex using the Drive folder itself as
|
||||
(polling every second, deleting any it finds older than the configured
|
||||
staleness timeout — Settings → Advanced, 1–60 minutes, default 10 — as
|
||||
orphaned/stale) — a ticket-based mutex using the cloud folder itself as
|
||||
the coordination point, so two devices never overwrite each other's
|
||||
edits to the shared database mid-write.
|
||||
2. Checks the remote database file's md5 checksum against the last one seen
|
||||
(a metadata-only call, no content download) to decide if a pull is even
|
||||
needed. If it changed: downloads it to a temp file, `ATTACH`es it to the
|
||||
local database, and runs one `INSERT OR REPLACE ... SELECT ... WHERE
|
||||
local.id IS NULL OR remote.updated_at > local.updated_at` per table.
|
||||
Rows that statement doesn't match — including this device's own
|
||||
not-yet-pushed edits — are left untouched, so no separate "keep local"
|
||||
step is needed.
|
||||
3. Uploads any locally-pending receipt photos (rows still holding a local
|
||||
file path), clearing that path and recording the Drive file ID instead.
|
||||
If any upload fails, the push step below is skipped entirely this cycle
|
||||
— a row is never pushed while it still holds a local-only path.
|
||||
edits to the shared database mid-write. This logic (`lock_coordinator.dart`)
|
||||
is identical regardless of which of the four providers is active.
|
||||
2. Checks the remote database file's change-detection tag (Drive's
|
||||
`md5Checksum`, Dropbox's `content_hash`, OneDrive's `cTag`, or a WebDAV
|
||||
resource's `ETag` — an opaque `versionTag` as far as the sync engine is
|
||||
concerned) against the last one seen (a metadata-only call, no content
|
||||
download) to decide if a pull is even needed. If it changed: downloads
|
||||
it to a temp file, `ATTACH`es it to
|
||||
the local database, and runs one `INSERT OR REPLACE ... SELECT ... WHERE
|
||||
local.<key> IS NULL OR remote.updated_at > local.updated_at` per table
|
||||
(`id` for both tables — vehicles' hidden generated id, not the
|
||||
user-editable VIN). Rows that statement doesn't match — including this
|
||||
device's own not-yet-pushed edits — are left untouched, so no separate
|
||||
"keep local" step is needed.
|
||||
3. Uploads any receipt photos still needing it (a local path but no cloud
|
||||
file ID yet — see `FuelEntry.needsReceiptUpload`), recording the cloud
|
||||
file ID. The local copy is then deleted or kept depending on the
|
||||
Settings "keep photos on this phone" toggle. If any upload fails, the
|
||||
push step below is skipped entirely this cycle — a row is never pushed
|
||||
while it still holds a local-only, not-yet-uploaded path.
|
||||
4. Reads the local database file directly (sqflite's default journal mode
|
||||
isn't WAL, so the file is complete and consistent as soon as the last
|
||||
write's `Future` resolves; `PRAGMA wal_checkpoint` runs first anyway as
|
||||
cheap insurance) and uploads it as the new remote copy, then clears the
|
||||
`dirty` flag on every row now that local matches what's on Drive.
|
||||
`dirty` flag on every row now that local matches what's on the cloud.
|
||||
5. Releases the lock.
|
||||
|
||||
**Deletions propagate correctly**, unlike a naive "union records" merge:
|
||||
|
|
@ -169,24 +313,71 @@ newest-wins rule as any edit — no separate deletion-handling logic needed.
|
|||
- **Field-level conflicts aren't merged.** Two people editing the *same*
|
||||
record at the same time: whole record, newer `updated_at` wins — not a
|
||||
field-by-field merge.
|
||||
- **Lock acquisition has no hard timeout** beyond the 10-minute staleness
|
||||
reap. Fine at the scale this is built for (a handful of people); could in
|
||||
theory spin under many simultaneous contenders.
|
||||
- **Lock acquisition has no hard timeout** beyond the configurable
|
||||
staleness reap (Settings → Advanced). Fine at the scale this is built for
|
||||
(a handful of people); could in theory spin under many simultaneous
|
||||
contenders.
|
||||
- **VIN uniqueness is checked, but not race-proof across devices.** Editing
|
||||
or adding a vehicle checks for an existing active vehicle with the same
|
||||
VIN before saving, but two offline devices could still each independently
|
||||
create (or rename into) the same VIN before either has seen the other's
|
||||
change — same category of accepted limitation as "field-level conflicts
|
||||
aren't merged" above. `mergeVehiclesSql` merges on the hidden `id`, not
|
||||
`vin`, specifically so this doesn't corrupt the merge itself if it
|
||||
happens — you'd just end up with two vehicle rows sharing a VIN until
|
||||
someone notices and fixes it by hand.
|
||||
- **`google_sign_in` v7's Android path requires a *Web* OAuth client ID**
|
||||
(`serverClientId`), not just the Android client's SHA-1 registration —
|
||||
see the manual setup section. This is a quirk of the Credential
|
||||
Manager-based implementation and easy to miss if you're used to older
|
||||
`google_sign_in` versions.
|
||||
- **Dropbox and OneDrive's sign-in isn't restored across a cold app
|
||||
restart.** Their OAuth refresh tokens are kept in memory only for now
|
||||
(Google's own SDK handles its own persistent session separately, and
|
||||
WebDAV persists its credentials to OS-encrypted storage — see below — so
|
||||
this only affects these two hand-built OAuth providers) —
|
||||
`attemptSilentSignIn()` always returns false for them, so you'll need to
|
||||
reconnect once per app launch until refresh-token persistence is added.
|
||||
- **WebDAV credentials are stored via `flutter_secure_storage`** (Keystore
|
||||
on Android, Keychain on iOS) so `attemptSilentSignIn()` can restore the
|
||||
session after a cold restart — it re-verifies them with the same PROPFIND
|
||||
check `signInWithCredentials` uses rather than trusting the stored values
|
||||
blindly, since the server or password could have changed since. A wrong
|
||||
or revoked password just silently fails the restore (same as no
|
||||
connection ever having been made); there's no proactive UI nudge to
|
||||
reconnect beyond the sync error that shows up on the next sync attempt.
|
||||
- **No automatic retry-on-401 for Dropbox/OneDrive.** Each session checks
|
||||
the access token's expiry before every call and refreshes proactively,
|
||||
but a token revoked or invalidated out-of-band (e.g. from that provider's
|
||||
own "manage app access" page) surfaces as a failed sync (visible in
|
||||
Settings as "Last sync failed") rather than prompting a fresh sign-in
|
||||
automatically.
|
||||
- **Dropbox has no distinct "Shared with me" tab** in the folder browser
|
||||
(the interface's `sharedWithMe` parameter is a no-op for
|
||||
`DropboxProvider`) — Dropbox auto-mounts *accepted* shares directly into
|
||||
the account's normal folder tree, so they already show up under "My
|
||||
Files" in the common case. Revisit if an unmounted/pending share needs to
|
||||
be browsable directly.
|
||||
- **WebDAV has no "Shared with me" concept at all** (it's not part of the
|
||||
base WebDAV protocol) — `WebDavProvider.supportsSharedWithMe` is false,
|
||||
same as Dropbox, and it's up to the server/user to point the app at
|
||||
whatever path a share is mounted under. WebDAV credentials are also sent
|
||||
as HTTP Basic Auth on every request, so an **HTTPS server URL is
|
||||
effectively required** — the app doesn't block a plain `http://` URL, but
|
||||
it would send the password in the clear.
|
||||
- **Receipt parsing is best-effort regex matching** on the OCR'd text
|
||||
(`lib/services/receipt_parser.dart`), tuned against common receipt phrasing
|
||||
("GALLONS", "PRICE/GAL", "PPG", "TOTAL", etc.). Unusual receipt layouts may
|
||||
parse partially or not at all — the confirm screen always lets you fill in
|
||||
or correct whatever wasn't found.
|
||||
- No automated tests exercise the OCR, camera, or real Drive API calls
|
||||
(those need a real device/emulator and live credentials); `receipt_parser_test.dart`,
|
||||
`lock_coordinator_test.dart`, and `db_merge_test.dart` (using
|
||||
`sqflite_common_ffi` to run the real merge SQL against temp SQLite files
|
||||
on the Dart VM) cover the pure logic pieces against fixed inputs.
|
||||
- No automated tests exercise the OCR, camera/gallery picker, or real Drive
|
||||
API calls (those need a real device/emulator and live credentials);
|
||||
`receipt_parser_test.dart`, `lock_coordinator_test.dart`,
|
||||
`db_merge_test.dart`, and `pending_receipt_upload_test.dart` (the latter
|
||||
two using `sqflite_common_ffi` to run real SQL against temp SQLite files
|
||||
on the Dart VM) cover the pure logic pieces against fixed inputs —
|
||||
`receipt_parser_test.dart` in particular is transcribed from 13 real
|
||||
photographed receipts across different gas station chains.
|
||||
- No migration path exists from the earlier JSON-file storage format —
|
||||
not needed since no real data had accumulated under it yet, but flag it
|
||||
if that's no longer true for you.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue