diff --git a/AGENTS.md b/AGENTS.md index 51f6d292..c362d99f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,3 +20,7 @@ Full details, incident history, and F-Droid notes: `doc/android-firebase-gms.md` ## Android Build — MLKit Barcode Scanner `@capacitor-mlkit/barcode-scanning` depends on `com.google.android.gms:play-services-code-scanner`, which merges `com.google.android.gms.version` into the APK manifest. This is a known long-term issue for strict FOSS/F-Droid builds. For now, the dependency is accepted; barcode scanning simply will not work on GMS-less devices (it fails gracefully at scan time, not at startup). Do not add additional GMS/Firebase dependencies without explicitly acknowledging this trade-off. + +## Android Build — Capacitor Geolocation + +`@capacitor/geolocation` depends on `com.google.android.gms:play-services-location`, an accepted second GMS dependency. On GMS-less devices "Use my location" fails and the interests prompt falls back to picking a point on a map. See `doc/android-firebase-gms.md`. diff --git a/CHANGELOG.md b/CHANGELOG.md index abea2c9a..448cf36b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.11] - 2026 +### Added +- The first visit to the feed asks for interests (and an optional location) instead of showing the three welcome pages, then shows the people and projects closest in meaning, nearest first. +- After the matches, a registered person sees a red dot on the profile tab, a one-time banner, and a card on the Profile page to save the interests as their public profile; on Android and iOS the card also offers a text-message or in-app alert. +### Changed +- The three welcome pages open only from Help. + + ## [1.4.10] - 2026.10.17 ### Changed - New Activities were heavily refactored and enhanced to match alertSearch endpoints with more results, cursors, etc. diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 68734a4b..787643c9 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -15,6 +15,7 @@ dependencies { implementation project(':capacitor-camera') implementation project(':capacitor-clipboard') implementation project(':capacitor-filesystem') + implementation project(':capacitor-geolocation') implementation project(':capacitor-preferences') implementation project(':capacitor-push-notifications') implementation project(':capacitor-share') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fea4baa4..812795ff 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -99,6 +99,8 @@ + + diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index 28240b47..941a658d 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -23,6 +23,10 @@ "pkg": "@capacitor/filesystem", "classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin" }, + { + "pkg": "@capacitor/geolocation", + "classpath": "com.capacitorjs.plugins.geolocation.GeolocationPlugin" + }, { "pkg": "@capacitor/preferences", "classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin" diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 6fd65f99..e9a1410c 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -20,6 +20,9 @@ project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacito include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') +include ':capacitor-geolocation' +project(':capacitor-geolocation').projectDir = new File('../node_modules/@capacitor/geolocation/android') + include ':capacitor-preferences' project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') diff --git a/doc/CHANGES/PLAN-onboarding-by-interests.md b/doc/CHANGES/PLAN-onboarding-by-interests.md index d8cb1fd8..0a3337b4 100644 --- a/doc/CHANGES/PLAN-onboarding-by-interests.md +++ b/doc/CHANGES/PLAN-onboarding-by-interests.md @@ -1,41 +1,41 @@ -# Plan: Onboarding by Interests (mobile) +# Plan: Onboarding by Interests ## Goal -A person who installs the phone app and opens it to the Home feed is asked one -thing before anything else: what they are interested in, in their own words, +A person who opens the app to the Home feed for the first time (phone, web, +or desktop) is asked one thing before anything else: what they are interested in, in their own words, with an optional "use my location". The app then shows them the people and projects whose descriptions are closest in meaning to what they typed, and closest in distance when they gave a location. After they have seen those results, and only once they are registered, a red dot on the profile tab (and -a one-time banner) points them to the follow-up: save the interests and location to their public profile, and -turn on a daily alert, text message first, so they hear when someone with -matching interests joins or posts. +a one-time banner) points them to the follow-up: save the interests and +location to their public profile, and, on the phone apps, turn on a daily +alert, text message first, so they hear when someone with matching interests +joins or posts. Web and desktop have no New Activity alerts, so there the +follow-up is the profile save alone. In scope: -1. The interests prompt replaces the three-page welcome dialog on native - Android and iOS builds. - - The Help page should now show a link to enter their interests again, - which triggers this interest prompt again. - This link goes above the current onboarding-welcome dialog link. - - The current onboarding explanations can be triggered by the Help page, - but that's the only way. On mobile they are not part of the onboarding - process. +1. The interests prompt replaces the three-page welcome dialog on every + platform. + - The Help page has a link to enter interests again, which opens the + interests prompt. It sits above the link to the welcome pages. + - The welcome pages open only from that Help link; they are not part of + onboarding. 2. The interests and location are saved on the phone as soon as the person enters them, registered or not, so they can be put in a public profile whenever the person chooses, including long after onboarding. 3. A results screen for interest matches, reachable again later from Discover. 4. A "next step" nudge that does not block the screen: a red dot on the profile tab, a banner that animates in once, and a card at the top of the - Profile page that holds the two follow-up actions. It appears only for - registered identities, since both actions need registration. + Profile page that holds the follow-up actions: the profile save, and on + the phone apps the alert. It appears only for registered identities, + since every action needs registration. 5. The follow-up actions reuse what the Profile page already does: save a profile with description and location, verify a phone and authorize SMS New Activity alerts, or authorize in-app New Activity alerts. -Out of scope: the web and Electron builds keep the welcome dialog as their -onboarding; on-device embedding; changes to what the alerts contain; +Out of scope: alerts on web and desktop; on-device embedding; changes to what the alerts contain; registration changes beyond text search on `/similar` (profile save and both alert channels keep requiring registration). @@ -65,7 +65,7 @@ alert channels keep requiring registration). `minSimilarity`, and `limit` (default 25, max 50). It returns `{ subjectType, subjectId, similarity, record }` best first, with DIDs hidden the way the profile and plan searches hide them. A `text:` source - needs a JWT from a **registered** DID (`ClaimService.getRateLimits`) and is + needs a JWT from a **registered** DID (`mainService.isMember`) and is limited to `EMBEDDING_QUERY_MAX_PER_MINUTE` queries per DID (default 20). The query text is embedded in memory and not stored. - `GET /api/partner/userProfile` (the people list Discover uses) and @@ -117,8 +117,8 @@ alert channels keep requiring registration). - Settings are one row per account (`src/db/tables/settings.ts`); booleans and JSON columns are listed in `PlatformServiceMixin._mapColumnsToValues`, and a column is added with a migration in `src/db-sql/migration.ts`. -- Platform detection: `this.capabilities().isNativeApp` and `isMobile` through - `PlatformServiceMixin`; the Capacitor service reports both true. +- Platform detection: `this.capabilities.isNativeApp` and `isMobile` through + `PlatformServiceMixin` (a computed property); the Capacitor service reports both true. ## Design @@ -146,11 +146,10 @@ the nudge is on) → `done`. A person who taps "Not now" on the prompt goes to `done` with `declinedAt` set. `finishedOnboarding` is set true whenever the stage becomes `done`, so everything that reads it keeps working. -On mobile the three welcome pages are not part of onboarding at any stage. -`HomeView`, `DiscoverView`, and `ProjectsView` open `OnboardingDialog` on -mobile only while `welcomeTour` is true. The Help page's "page-by-page help" -link sets `welcomeTour` on mobile (on web it keeps setting `finishedOnboarding` -false), and finishing or closing the dialog clears it. On mobile, a +The three welcome pages are not part of onboarding at any stage. +`HomeView`, `DiscoverView`, and `ProjectsView` open `OnboardingDialog` only +while `welcomeTour` is true. The Help page's "page-by-page help" link sets +`welcomeTour`, and finishing or closing the dialog clears it. A `finishedOnboarding` of false, which a fresh install has until the stage reaches `done`, opens nothing by itself. @@ -179,7 +178,7 @@ profile, and they outlive the onboarding stages: registers them. - When a registered person has no saved profile, the Profile page's "Public Profile" editor (shown only to registered identities) pre-fills its textarea and map point from them and shows "Saved only - on this phone. Not public until you save your profile." with a "Forget" + on this device. Not public until you save your profile." with a "Forget" link. This is the path for a person who was unregistered during onboarding, and for anyone who declined the card. - Saving the profile clears them, since the server then holds the text. @@ -190,9 +189,10 @@ profile, and they outlive the onboarding stages: ### Where and when the prompt opens -Mobile only: `capabilities().isNativeApp && capabilities().isMobile`. Web and -Electron keep `OnboardingDialog` unchanged, so the Playwright web suite -is untouched. +Every platform. HomeView sets `data-onboarding-checked` on `#Content` once +the prompt has opened or was not due, and the Playwright helper +`closeOnboardingIfShown` waits for it and closes the prompt only when it +opened: test users with saved profile text never see it. The prompt opens from `HomeView` only, in the same place `OnboardingDialog` opens (end of `initializeIdentity`, so the feed is loading underneath), when @@ -223,7 +223,7 @@ One screen, replacing the three. Content: link. On denial or failure, show "Pick on map" which opens a small map (the Leaflet setup from the Profile page) to tap a point. Location is optional; the search runs without it. -- The privacy line, in full: "Your interests stay on this phone. Searching +- The privacy line, in full: "Your interests stay on this device. Searching sends them to the server to find matches, but the server does not store them. You can choose to add them to your public profile afterward." (See Decisions: the request's "does not leave the device" wording is not true of @@ -246,7 +246,7 @@ projects/people by anywhere/nearby/mapped and would gain a fourth axis. - Calls `GET /api/partner/similar` with `from=text:`, `to=both`, `limit=50`, and, when there is a point, a bounding box of about 50 km around it (±0.45° latitude; longitude scaled by `cos(lat)`). The box, not the point, - is what leaves the phone. + is what leaves the device. - Shows two sections, People and Projects, best match first, each row with the description excerpt, the similarity as a short label ("strong", "good", "some"), and a tap-through to `user-profile` or `project`. When the box @@ -274,8 +274,9 @@ finds `isRegistered` true (HomeView re-checks registration on each load) makes them appear, with the banner's one-time animation still unspent. The kept interests wait on the phone for that moment. -1. **Red dot on the profile tab.** `QuickNav.vue` reads the stage on mount (the - settings read is cached) and renders a small red circle at the top-right of +1. **Red dot on the profile tab.** `QuickNav.vue` reads the stage once per + launch into a shared reactive state (`src/utils/onboardingNudge.ts`), which + the pages that change the stage or registration update, and renders a small red circle at the top-right of the `circle-user` icon. Every page shows QuickNav, so the pointer is everywhere and in nobody's way. 2. **One-time banner.** A `NextStepBanner.vue` component, placed under @@ -283,7 +284,8 @@ kept interests wait on the phone for that moment. `InterestMatchesView`. The first time any of those pages renders while the stage is `save` and `bannerShownAt` is null, it slides down from the top: "Want to hear when new matches appear? Save your interests and pick an - alert →", holds for a few seconds while the profile tab's dot pulses (a CSS + alert →" on the phone apps, and "Let people with interests like yours find + you: save your interests to your profile →" elsewhere. It holds for a few seconds while the profile tab's dot pulses (a CSS keyframe on the dot, triggered by a shared reactive flag), then collapses toward the tab. Tapping it goes to `account`. It sets `bannerShownAt` and never animates again; the dot stays. @@ -297,7 +299,9 @@ kept interests wait on the phone for that moment. to the same box the search used when no search box exists yet, so Nearby works. Match alerts depend on this step; the card says so: "Alerts about matches use your saved profile." - - **Step 2, "Get alerted."** Two choices, in this order: + - **Step 2, "Get alerted."** On the phone apps only + (`offersAlerts(capabilities)`: native and mobile), since web and desktop + have no Notifications section. Two choices, in this order: 1. **Text message** (labelled "most reliable"): runs the existing `toggleSmsEnabled` → `SmsVerificationDialog` → time prompt → `authorizeSmsAlertSearch` sequence. Shown disabled with the reason when @@ -306,17 +310,17 @@ kept interests wait on the phone for that moment. note "may arrive late on iPhone". A link to `help-notification-types` sits under both. - "Done" and "No thanks" at the bottom; both set the stage to `done`, which - clears the dot and the card. Completing both steps also sets `done`. + clears the dot and the card. Completing every step shown (the save, plus + an alert on the phone apps) also sets `done`. The card is a thin layer over methods `AccountViewView` already has; the notification section below it is unchanged, so a person who ignores the card finds the same toggles where they always were. ### Welcome text -`OnboardingDialog.vue` keeps its three pages unchanged. On web and Electron it -remains the onboarding. On mobile it opens only from the Help page link, as -described under State, and walks the same Home → Discover → Projects sequence -while `welcomeTour` is set. +`OnboardingDialog.vue` keeps its three pages unchanged. It opens only from the +Help page link, as described under State, and walks the Home → Discover → +Projects sequence while `welcomeTour` is set. ### Location @@ -392,89 +396,96 @@ its registration gate. ### endorser-ch -- [ ] `/similar`: drop the registration check for `text:`; per-DID limit 20 +- [x] `/similar`: drop the registration check for `text:`; per-DID limit 20 registered / 5 unregistered. -- [ ] Per-IP text-query limit (IPv4 address or IPv6 /64), with `TRUST_PROXY` +- [x] Per-IP text-query limit (IPv4 address or IPv6 /64), with `TRUST_PROXY` feeding Express `trust proxy`; document the setting for each deployment. -- [ ] Query-vector LRU cache keyed by preprocessed-text hash. -- [ ] Separate, capped text-query queue behind profile and sweep work; 503 +- [x] Query-vector LRU cache keyed by preprocessed-text hash. +- [x] Separate, capped text-query queue behind profile and sweep work; 503 when full. -- [ ] Tests in `test/controller-partner-9-similar.js`: unregistered text query +- [x] Tests in `test/controller-partner-9-similar.js`: unregistered text query succeeds; per-IP limit with a spoofed `X-Forwarded-For` ignored when the proxy is not trusted; cache hit skips inference; full queue returns 503. - [ ] Deploy to the test server before the app's results view depends on it. ### This app: state and gating -- [ ] Migration adding `onboardingState TEXT` to `settings`; add it to the JSON - list in `PlatformServiceMixin._mapColumnsToValues` and to the `Settings` - type. -- [ ] `src/libs/onboarding.ts`: the state type, stage transitions, the +- [x] Migration adding `onboardingState TEXT` to `settings`, and the column in + the `Settings` type as a JSON string read with `parseOnboardingState`, like + `newActivityCursors` (not in `_mapColumnsToValues`). +- [x] `src/libs/onboarding.ts`: the state type, stage transitions, the bounding-box helper (point + radius → `minLocLat` … ), the rounding helper, - and `isMobileOnboarding(capabilities)`. Unit tests in `src/test/`. -- [ ] `HomeView.initializeIdentity`: on mobile with stage `interests`, check - for an existing server profile, then open `InterestsPromptDialog`; otherwise - keep the `OnboardingDialog` call for web. -- [ ] `HomeView`, `DiscoverView`, `ProjectsView`: on mobile, open - `OnboardingDialog` only while `welcomeTour` is set; clear it when the dialog - finishes or closes. -- [ ] `HelpView.vue`: an "enter your interests again" link above the welcome - link, opening `InterestsPromptDialog` pre-filled; on mobile, the welcome - link sets `welcomeTour` instead of only unsetting `finishedOnboarding`. + and `offersAlerts(capabilities)`. Unit tests in `src/test/`. +- [x] `HomeView.initializeIdentity`: with stage `interests`, check for an + existing server profile, then open `InterestsPromptDialog`; open + `OnboardingDialog` instead during a welcome tour. +- [x] `HomeView`, `DiscoverView`, `ProjectsView`: open `OnboardingDialog` only + while `welcomeTour` is set; clear it when the dialog finishes or closes. +- [x] `HelpView.vue`: an "enter your interests again" link above the welcome + link, opening `InterestsPromptDialog` pre-filled; the welcome link sets + `welcomeTour`. +- [x] Web and desktop: the same prompt, results, and nudge, with the card's + alert step and the banner's alert wording only on the phone apps. ### This app: prompt and results -- [ ] `InterestsPromptDialog.vue`: textarea, location button with permission +- [x] `InterestsPromptDialog.vue`: textarea, location button with permission flow and map fallback, privacy line, "Find matches", "Not now". -- [ ] `@capacitor/geolocation`: install, `npx cap sync`, Android permissions +- [x] `@capacitor/geolocation`: install, Android permissions (`ACCESS_COARSE_LOCATION`, `ACCESS_FINE_LOCATION`), iOS - `NSLocationWhenInUseUsageDescription`; note the Play Services dependency in - `doc/android-firebase-gms.md`. -- [ ] `src/libs/partnerServer.ts`: `fetchSimilar(params)` typed client for + `NSLocationWhenInUseUsageDescription`, Podfile entry; note the Play Services + dependency in `doc/android-firebase-gms.md`. +- [ ] iOS `pod install` for `CapacitorGeolocation` (updates `Podfile.lock`; + `scripts/build-ios.sh` runs it through `cap sync`) — next: run on a machine + with CocoaPods (Trent) +- [x] `src/libs/partnerServer.ts`: `fetchSimilar(params)` typed client for `/api/partner/similar`, with the 429, 503, and network cases mapped to a small result type. -- [ ] `InterestMatchesView.vue` + route `interest-matches` (+ deep-link schema +- [x] `InterestMatchesView.vue` + route `interest-matches` (+ deep-link schema entry with optional `text`, `lat`, `lon`): two sections, "Farther away" fill, error states, stage → `save` on first render. -- [ ] `DiscoverView.vue`: a "Search by interests" link to the view, so the +- [x] `DiscoverView.vue`: a "Search by interests" link to the view, so the feature is reachable after onboarding. ### This app: nudge -- [ ] `QuickNav.vue`: red dot on the profile tab while the stage is `save` +- [x] `QuickNav.vue`: red dot on the profile tab while the stage is `save` and the identity is registered; pulse class driven by a shared reactive flag. -- [ ] `NextStepBanner.vue`: one-time animated banner, registered identities +- [x] `NextStepBanner.vue`: one-time animated banner, registered identities only; placed in Home, Discover, Projects, and the results view; sets `bannerShownAt`. -- [ ] `NextStepCard.vue` in `AccountViewView.vue`: Step 1 save (text + +- [x] `NextStepCard.vue` in `AccountViewView.vue`: Step 1 save (text + rounded point + `searchBoxes[0]`), Step 2 alerts with SMS first and in-app second, reasons when disabled, "Done" / "No thanks"; rendered only for registered identities; clears interests from `onboardingState` only on a successful save. -- [ ] `AccountViewView.vue` "Public Profile": with no saved profile, pre-fill - from the kept interests and point, with the "Saved only on this phone" note +- [x] `AccountViewView.vue` "Public Profile": with no saved profile, pre-fill + from the kept interests and point, with the "Saved only on this device" note and "Forget". -- [ ] Default time pre-fill in `PushNotificationPermission` when opened from +- [x] Default time pre-fill in `PushNotificationPermission` when opened from the card (per Decision 6). ### This app: text, docs, tests -- [ ] `doc/usage-guide.md` and `HelpNotificationTypesView.vue`: mention that +- [x] `doc/usage-guide.md` and `HelpNotificationTypesView.vue`: mention that match alerts come from the saved profile text. -- [ ] Unit tests: stage transitions, kept-interests lifetime (kept on `done`, +- [x] Unit tests: stage transitions, kept-interests lifetime (kept on `done`, "Not now", and while unregistered; cleared on save and "Forget"), bbox and rounding helpers, `fetchSimilar` error mapping. -- [ ] Playwright web: confirm `importUserAndCloseOnboarding` passes - (web is unchanged). -- [ ] Mobile manual script in `doc/`: fresh install → prompt → deny location → +- [x] Playwright web: `closeOnboardingIfShown` (waits for + `data-onboarding-checked`, closes the prompt only if it opened) in place of + direct `closeOnboardingAndFinish` clicks; no close on Discover, where + nothing opens. +- [x] Manual script in `doc/onboarding-by-interests-manual-test.md`, phones + and web: fresh install → prompt → deny location → map pick → matches → banner → dot → card → SMS path on a registered account; invite deep link on a fresh install → no prompt until Home; unregistered account → matches by meaning → no dot, banner, or card → register → dot and banner appear → card offers "Save" → profile saved with those interests; "No thanks" on the card → Profile editor pre-filled later; Help → welcome pages walk Home, Discover, Projects once and stop. -- [ ] CHANGELOG entry; bump with `-beta`. +- [x] CHANGELOG entry; bump with `-beta`. ## Rejected @@ -509,6 +520,13 @@ available for this release. The request rules this out: the person is in a search flow and the follow-up must wait at the edge of the screen until they choose it. +### Keeping the welcome pages as the web and desktop onboarding + +The first release of this plan limited the interests prompt to the phone apps +and left web and Electron on the welcome pages. Closed 2026-10-31: every +platform gets the interests prompt, and only the alert step stays phone-only, +because web and desktop have no Notifications section. + ### Dropping the three welcome pages entirely Their content is the only in-app explanation of the feed, Discover, and diff --git a/doc/CHANGES/PLAN.md b/doc/CHANGES/PLAN.md index b744ab36..548fb00c 100644 --- a/doc/CHANGES/PLAN.md +++ b/doc/CHANGES/PLAN.md @@ -1,7 +1,7 @@ ## Tasks -- [ ] Onboarding by interests on mobile — next: state and gating work (`onboardingState` migration, `src/libs/onboarding.ts`); see [PLAN-onboarding-by-interests.md](PLAN-onboarding-by-interests.md) (Trent) +- [ ] Onboarding by interests on mobile — next: deploy the endorser-ch `/similar` change to the test server, run iOS `pod install`, then the manual script `doc/onboarding-by-interests-manual-test.md` on devices; see [PLAN-onboarding-by-interests.md](PLAN-onboarding-by-interests.md) (Trent) - [ ] On-device New Activity notifications through alertSearch — next: plugin request-job runner (`configureRequestJob`), then the app's job builder; see [PLAN-device-alertsearch-notifications.md](PLAN-device-alertsearch-notifications.md) (Trent) - [ ] Default `apiServer` in every settings fallback. `$accountSettings()` (`src/utils/PlatformServiceMixin.ts`) returns the caller's `defaults` (often diff --git a/doc/android-firebase-gms.md b/doc/android-firebase-gms.md index c3aa14f3..85cc977f 100644 --- a/doc/android-firebase-gms.md +++ b/doc/android-firebase-gms.md @@ -68,6 +68,14 @@ Practical impact: This is an accepted trade-off. Removing it would require either forking the plugin or introducing a `foss` product flavor that excludes the MLKit plugin entirely — work to undertake if/when F-Droid submission is planned. +## Known GMS dependency: Capacitor Geolocation + +`@capacitor/geolocation` (the interests prompt's "Use my location" on the phone apps) depends on `com.google.android.gms:play-services-location` on Android. Like the MLKit scanner, it is present regardless of `-PfirebaseEnabled`. + +On a device without Google Play Services the location request fails, and the prompt falls back to "Pick on map", where the person taps a point on a Leaflet map. Location is optional: the search runs without one. Only a point rounded to two decimals (about 1 km) is kept, and only a box of about 50 km around it is sent to the server. + +The same trade-off applies as for MLKit: removing it would mean a `foss` flavor that drops the plugin and keeps only the map. + ## Incident: June 2026 Jose Olarte III's `notify-api` branch placed a production `google-services.json` in `android/` to test Firebase Cloud Messaging. The branch was never merged to `master`, but because the file is gitignored it persisted on disk after switching branches. At the time, the Gradle conditional activated Firebase based on file presence alone (no opt-in flag), so all subsequent local builds embedded Firebase and required Google Play Services. This silently broke APK/Aurora/Zapstore distribution. diff --git a/doc/onboarding-by-interests-manual-test.md b/doc/onboarding-by-interests-manual-test.md new file mode 100644 index 00000000..784c6503 --- /dev/null +++ b/doc/onboarding-by-interests-manual-test.md @@ -0,0 +1,113 @@ +# Manual Test: Onboarding by Interests + +The app asks for interests on the first visit to the feed, shows matches +from the partner server's `/api/partner/similar`, and then nudges a +registered person to save a profile and, on Android and iOS, to pick an +alert. Sections 1-7 run on the phone apps; section 8 covers web and desktop. +Design: `doc/CHANGES/PLAN-onboarding-by-interests.md`. The Playwright suite +covers the web dismissal path (`closeOnboardingIfShown`). + +## Setup + +- A partner server that allows text search on `/similar` for unregistered + identities, with some profiles and projects that have descriptions. +- A build pointed at that server (Profile → Advanced → Partner Server). +- "Fresh install" means uninstalling the app first, so settings are empty. +- A registered identity to import for the registered runs, and a US phone + number that can receive the verification text. + +## 1. Fresh install, registered, denied location, SMS + +1. Install, open, and import the registered identity (or have it registered + from another device) and go to the feed. +2. The prompt "What would you like to find?" opens over the feed; the three + welcome pages do not. +3. Type interests. Tap "Use my location" and deny permission. "Pick on map" + appears with a map; tap a point. "Near , " shows two decimals. +4. Tap "Find matches". The results view shows People and Projects, each row + with a "strong", "good", or "some" label. With fewer than five nearby, + "Farther away" appears. +5. Go to the feed. The banner slides down once, the profile tab's red dot + pulses, and the banner collapses. Visit Discover and Projects: no banner; + the dot stays. +6. Tap the profile tab. The "Hear about new matches" card is at the top with + the interests filled in and "Include location" checked. +7. Tap "Save". The profile saves; "Public Profile" below shows the text and + the point. Profile → Search Area shows a box around the point if there was + none before. +8. Tap "Text message". Enter the phone number, enter the code, and accept the + time prompt, which starts at 9:00 AM. The card closes, and the dot is gone + on every page. + +## 2. Invite deep link on a fresh install + +1. Fresh install. Open an invite link (`invite-one-accept`) on the phone. +2. The invite flow runs to the contacts page without the prompt. +3. Tap the feed tab. The prompt opens now. + +## 3. Unregistered identity, then registration + +1. Fresh install with a new, unregistered identity. Go to the feed. +2. Enter interests with "Use my location" allowed; "Find matches" shows + matches the same way as for a registered identity. +3. No banner, no dot, and no card on the Profile page. "Public Profile" is + not shown (it needs registration). +4. Have someone register the identity. Open the feed: after the + registration check, the banner animates and the dot appears. +5. The card on the Profile page offers "Save" with the interests typed in + step 2. Save; the profile holds those interests. + +## 4. "No thanks", then the profile editor + +1. With the card showing on a registered identity that has no saved profile, + tap "No thanks". The card and dot go away. +2. Reload the Profile page. "Public Profile" is pre-filled with the + interests and point, with "Saved only on this phone. Not public until you + save your profile." and "Forget". +3. "Forget" empties the editor and the note goes away; reloading the page + keeps it empty. + +## 5. Close, "Not now", and Help + +1. Fresh install. When the prompt opens, close it with the "x". Visit + another tab and return to the feed: no prompt. Force-quit and reopen the + app: the prompt opens again. +2. Type something and tap "Not now". The prompt does not return on later + cold starts. +3. Help → "enter your interests again" opens the prompt with the text kept + from step 2. "Find matches" runs the flow again (results, then banner and + dot for a registered identity). +4. Help → "see the page-by-page help again" opens the welcome on the feed. + "Show me more!" goes to Discover with its page, then Projects with its + page. "Let's go!" closes it. Visiting those pages again opens nothing. + +## 6. Existing profile on a reinstall + +1. Fresh install, then import an identity that already has a saved public + profile with text. +2. Go to the feed. No prompt opens (the app found the server profile). + +## 7. Server limits + +1. Search many times within a minute from the results view ("Change", then + "Find matches"). After the limit, "Too many searches right now; try again + in a minute." shows with "Try again". +2. With the server's `EMBEDDING_QUERY_MAX_QUEUED=0`, a new uncached text + gives "Matching is busy; try again shortly." +3. In airplane mode, the results view shows the connection message and + "Try again"; the interests are still in Help's prompt afterward. + +## 8. Web and desktop + +1. In a fresh browser profile (or a private window), open the app. The + prompt opens over the feed with no keyboard-microphone tip, and the + privacy line says "this device". +2. "Use my location": the browser asks for permission. Deny it, and "Pick on + map" appears. +3. With a registered identity, "Find matches", then return to the feed: the + banner reads "Let people with interests like yours find you…", and the dot + appears. +4. The Profile card has only "Save to your public profile", with no alert + step. "Save" closes the card and the dot. +5. Help → "enter your interests again" is present, above the page-by-page + help link, and opens the prompt pre-filled. diff --git a/doc/usage-guide.md b/doc/usage-guide.md index 596da455..83e3e1e3 100644 --- a/doc/usage-guide.md +++ b/doc/usage-guide.md @@ -74,6 +74,13 @@ Companion videos will also describe end-to-end workflows for the end-user. This section covers application usage for people who will use TimeSafari as intended. It is a simplified guide illustrating how to gain value from using TimeSafari. +The first visit to the feed asks what you are interested in, in your own words, with an optional +location. The app shows the people and projects whose descriptions are closest in meaning, nearest +first. The interests stay on your device until you save them to your public profile. On the +Android and iOS apps, a daily New Activity alert (in-app or text message) can report new matches; +those matches come only from the saved public profile text, so an alert about them needs a saved +profile. + \pagebreak # 2 - Software Developers diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 3af5843f..497eb3c9 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -44,6 +44,8 @@ Time Safari allows you to take photos, and also scan QR codes from contacts. NSPhotoLibraryUsageDescription Time Safari allows you to upload photos. + NSLocationWhenInUseUsageDescription + Time Safari uses your approximate location to find people and projects near you. Only a rounded area leaves your phone. NSUserNotificationAlertStyle alert UIBackgroundModes diff --git a/ios/App/Podfile b/ios/App/Podfile index 58202730..f3ad93dc 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -17,6 +17,7 @@ def capacitor_pods pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera' pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' + pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation' pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' diff --git a/package-lock.json b/package-lock.json index a923d691..b3afbf61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@capacitor/clipboard": "^7.0.4", "@capacitor/core": "^7.6.4", "@capacitor/filesystem": "^7.1.8", + "@capacitor/geolocation": "^7.1.8", "@capacitor/ios": "^7.6.4", "@capacitor/preferences": "^7.0.4", "@capacitor/push-notifications": "^7.0.7", @@ -1935,6 +1936,18 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/geolocation": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-7.1.8.tgz", + "integrity": "sha512-x/rJieD2N2zlGvXFRD3ru4ArJWTwXAIYmyhfo5zg4F1DKGos7mCCOvIOSVMdfQmt14SCsmJeO9Qe8EtDGNGCrw==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.3" + }, + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "7.6.4", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.6.4.tgz", diff --git a/package.json b/package.json index 1ad87551..32de2904 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "@capacitor/clipboard": "^7.0.4", "@capacitor/core": "^7.6.4", "@capacitor/filesystem": "^7.1.8", + "@capacitor/geolocation": "^7.1.8", "@capacitor/ios": "^7.6.4", "@capacitor/preferences": "^7.0.4", "@capacitor/push-notifications": "^7.0.7", diff --git a/src/components/InterestsPromptDialog.vue b/src/components/InterestsPromptDialog.vue new file mode 100644 index 00000000..bf9803a9 --- /dev/null +++ b/src/components/InterestsPromptDialog.vue @@ -0,0 +1,269 @@ + + + + diff --git a/src/components/NextStepBanner.vue b/src/components/NextStepBanner.vue new file mode 100644 index 00000000..ddba2d95 --- /dev/null +++ b/src/components/NextStepBanner.vue @@ -0,0 +1,117 @@ + + + + + + diff --git a/src/components/NextStepCard.vue b/src/components/NextStepCard.vue new file mode 100644 index 00000000..89415de4 --- /dev/null +++ b/src/components/NextStepCard.vue @@ -0,0 +1,178 @@ + + + + diff --git a/src/components/OnboardingDialog.vue b/src/components/OnboardingDialog.vue index af70fda0..7a3608f5 100644 --- a/src/components/OnboardingDialog.vue +++ b/src/components/OnboardingDialog.vue @@ -83,7 +83,9 @@

To see these instructions and more, click above on - Help + + +

@@ -200,6 +202,11 @@ import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import { AppString, NotificationIface } from "../constants/app"; +import { + onboardingSettingsUpdate, + parseOnboardingState, + withWelcomeTour, +} from "../libs/onboarding"; import { OnboardPage } from "../libs/util"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; @@ -262,10 +269,10 @@ export default class OnboardingDialog extends Vue { } /** - * CSS classes for help badge styling + * CSS classes for the help badge: the same circle as the Help button */ get helpBadgeClasses() { - return "ml-1 mr-1 text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"; + return "ml-1 mr-1 inline-block text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"; } /** @@ -294,12 +301,23 @@ export default class OnboardingDialog extends Vue { this.visible = true; if (this.page === OnboardPage.Create) { // we'll assume that they've been through all the other pages - await this.$updateSettings({ - finishedOnboarding: true, - }); + await this.finishTour(); } } + /** + * Ends the tour. The pages open only as a tour started from Help; the + * interests prompt is the onboarding, so `finishedOnboarding` is its to set. + */ + private async finishTour() { + const settings = await this.$accountSettings(); + await this.$saveSettings( + onboardingSettingsUpdate( + withWelcomeTour(parseOnboardingState(settings.onboardingState), false), + ), + ); + } + /** * Closes the onboarding dialog with optional completion actions * @@ -309,9 +327,7 @@ export default class OnboardingDialog extends Vue { async onClickClose(done?: boolean, goHome?: boolean) { this.visible = false; if (done) { - await this.$updateSettings({ - finishedOnboarding: true, - }); + await this.finishTour(); if (goHome) { this.$router.push({ name: "home" }); } diff --git a/src/components/PushNotificationPermission.vue b/src/components/PushNotificationPermission.vue index 18d0583c..b39bdf33 100644 --- a/src/components/PushNotificationPermission.vue +++ b/src/components/PushNotificationPermission.vue @@ -185,9 +185,14 @@ export default class PushNotificationPermission extends Vue { skipSchedule?: boolean; rolloverIntervalMinutes?: number; timeOnly?: boolean; + /** "HH:mm" (24-hour) to start the clock at, which the person can change */ + defaultTime?: string; }, ) { this.callback = callback || this.callback; + if (options?.defaultTime) { + this.timeValue = options.defaultTime; + } this.isVisible = true; this.pushType = pushType; this.skipScheduleForOpen = options?.skipSchedule ?? false; diff --git a/src/components/QuickNav.vue b/src/components/QuickNav.vue index 133b1553..30aff29a 100644 --- a/src/components/QuickNav.vue +++ b/src/components/QuickNav.vue @@ -92,7 +92,17 @@ class="block text-center py-2 px-1" >
- + + + + + + + +

+ Saved only on this device. Not public until you save your profile. + Forget +