Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## 1.2.0

### 2026-08-30
- **Feature: transient authenticated Wayfarer routing (#260)**
- Discovers and confirms server-owned routing profiles before requesting provider-neutral routes
- Keeps provider credentials server-side and never contacts a routing provider directly
- Preserves saved Segment geometry priority with Direct guidance for unavailable, rejected, cancelled, or stale work
- Retains safe server-returned attribution and hosted provenance only with the active route
- Keeps hosted routes and profile selections session-only; offline retention remains future work in #261

### 2026-06-20
- **Feature: Search private trips (#228, PR #231)**
- Added local, case-insensitive trip-name search to the My Trips tab
Expand Down
15 changes: 9 additions & 6 deletions docs/03-Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ From the main map, you can add your current location to the loaded trip as a new

## Navigation

Navigate with saved Trip Segment geometry or honest straight-line Direct guidance. Mobile does not contact a public routing provider.
Navigate with saved Trip Segment geometry, transient authenticated Wayfarer-hosted routing, or honest straight-line Direct guidance. Mobile contacts only the configured Wayfarer server; that server may use its selected routing provider.

### Navigation Contexts

Expand All @@ -310,8 +310,8 @@ The app supports navigation in different contexts:
| Context | Started From | Features |
|---------|--------------|----------|
| **Trip Navigation** | Trip sidebar → place | Uses trip segments, full route priority |
| **Group Navigation** | Groups → member | Direct guidance to member location |
| **Map Navigation** | Long-press on map | Direct guidance to any point |
| **Group Navigation** | Groups → member | Hosted routing when available, otherwise Direct |
| **Map Navigation** | Long-press on map | Hosted routing when available, otherwise Direct |

### Starting Trip Navigation

Expand Down Expand Up @@ -343,12 +343,14 @@ Route calculation differs based on navigation context:
| Priority | Source | When Used |
|----------|--------|-----------|
| 1 | **User Segments** | Trip has pre-defined route geometry |
| 2 | **Direct Route** | Saved geometry is unavailable or invalid |
| 2 | **Wayfarer hosted route** | Saved geometry is unavailable and authenticated routing is available |
| 3 | **Direct Route** | Hosted routing is not selected, unavailable, or rejected |

**Ad-Hoc Navigation** (groups, map locations):
| Priority | Source | When Used |
|----------|--------|-----------|
| 1 | **Direct Route** | Always; ad-hoc targets have no saved Segment geometry |
| 1 | **Wayfarer hosted route** | Authenticated routing is available and selected |
| 2 | **Direct Route** | Hosted routing is not selected, unavailable, or rejected |

> **Note**: Ad-hoc navigation does not have saved Segment geometry because there is no Trip context.

Expand All @@ -359,7 +361,8 @@ Route calculation differs based on navigation context:
- Distance to destination
- Bearing-based heading

Direct is not road-aware or hosted turn-by-turn routing. Authenticated Wayfarer-hosted routing is planned separately and is not implemented yet.
Direct is not road-aware. Hosted route geometry, attribution, and safe provider/profile provenance remain only with
the active route and are not retained for offline use; replacement or stop clears them. #261 owns offline retention.

### External Maps Integration

Expand Down
8 changes: 8 additions & 0 deletions docs/07-Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,14 @@ For detailed troubleshooting:
3. **Check destination**: Place must have valid coordinates
4. **Try different place**: Some places may have issues

### Hosted Routing Falls Back to Direct

Direct remains usable when the configured Wayfarer server is old, routing is disabled, no provider is available,
authentication authority changes, or live location/target/profile state no longer matches a delayed response. Confirm
the server supports the Mobile routing endpoints and that routing is enabled for your account. Provider credentials
are configured only on the server and are never entered in Mobile. A hosted route and its safe provenance are
session-only; offline retention is deferred to #261.

### Off-Route Constantly

**Symptoms:**
Expand Down
12 changes: 9 additions & 3 deletions docs/11-Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,15 @@ services.AddHttpClient("WayfarerApi", client =>
The `TripNavigationService` calculates routes with the following priority:

1. **Saved Segment geometry**: Trip-defined geometry (always preferred when valid)
2. **Direct guidance**: Straight line with bearing and distance

Mobile does not contact a public routing provider. Authenticated Wayfarer-hosted routing is future work and is not part of the current architecture.
2. **Authenticated Wayfarer route**: Fresh provider-neutral, session-only geometry
3. **Direct guidance**: Straight line with bearing and distance

Mobile contacts only its configured Wayfarer server. Routing identity uses a non-secret, process-local authentication
session revision rather than the bearer token. In the final synchronous UI callback, the coordinator rebuilds current
authority from the settings owner, live device location, current Trip Place/Segment data or member owner, and the
hosted selection owner. It compares generation, normalized server, target and Segment identity, profile/authority,
choice, and canonical origin/anchors/destination immediately before installation, with no await or dispatch gap.
Provider credentials and provider-specific endpoints remain server-side.

### Navigation Graph

Expand Down
32 changes: 28 additions & 4 deletions docs/12-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@ Manages the dropped pin marker for map long-press interactions. Stateless render

**Source**: `src/WayfarerMobile/Services/TripNavigationService.cs`

Provides navigation with route calculation and progress tracking. Mobile makes no direct routing-provider request.
Provides navigation installation and progress tracking. A separate hosted-routing owner uses the authenticated
Wayfarer server; Mobile never contacts a routing provider directly.

### Navigation Modes

Expand All @@ -497,13 +498,15 @@ Provides navigation with route calculation and progress tracking. Mobile makes n
- Has access to user-defined segments and trip context
- Route priority:
1. Valid saved Segment geometry (trip-defined routes)
2. Direct Route (straight-line fallback)
2. A freshly requested, transient Wayfarer-hosted route
3. Direct Route (straight-line fallback)

**Ad-Hoc Navigation** (`CalculateRouteToCoordinatesAsync`):
- Used for groups, map locations, any coordinates
- No trip context available
- Route priority:
1. Direct Route
1. A freshly requested, transient Wayfarer-hosted route
2. Direct Route

```csharp
// Trip navigation - uses full route priority chain
Expand Down Expand Up @@ -548,7 +551,28 @@ public NavigationRoute? CalculateRouteToPlace(
}
```

Direct guidance is not road-aware or hosted turn-by-turn routing. Authenticated Wayfarer-hosted routing remains future work.
Hosted routes are authenticated, provider-neutral, session-only results. Provider credentials and provider selection
remain on Wayfarer. The active route retains linked attribution plus safe transient provenance: selected transport
profile and authority identities, provider and provider-configuration identities, mapping identity, storage mode, and
the normalized backend generation timestamp. It contains no bearer token, credentials, or provider endpoint and
clears through normal replacement or stop. Old servers, disabled routing, rejected requests, cancellation,
malformed/stale responses, and provider
unavailability remain routing-local and retain Direct guidance without affecting authentication or synchronization.
Valid saved Segment geometry is never replaced automatically. Mobile does not persist generated geometry, selection,
attribution, or authority identities; offline retention of hosted routes belongs to #261.

Chooser entries are scoped to the exact discovery catalog displayed. Mobile submits that catalog identity with the
chosen profile; a `catalog-changed` capability response makes no route request, rediscovers once, and requires a
fresh choice from the refreshed labels or retains Direct when dismissed. Once capability succeeds, unrelated later
catalog changes do not invalidate the confirmed route.

`TransportProfileId` is the Segment's current planning profile identity. Current hosted selection state remains
separate from the immutable provenance retained on a successfully published route; neither rewrites the Segment nor
becomes a durable current-profile setting. The settings owner advances a non-secret, memory-only authentication
session revision whenever the effective server or token authority changes, including logout/reset, so routing never
copies or compares the token. At actual publication the coordinator rereads live location, exact Trip Place/Segment
profile/ordered anchors by stable IDs, or the current member location from its owner. That state is compared beside
the synchronous route/provenance copy.

### Navigation State

Expand Down
20 changes: 19 additions & 1 deletion docs/13-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,25 @@ public async Task<ApiResult<T>> SendAsync<T>(HttpRequestMessage request)

## Mobile Routing Boundary

Mobile does not contact a public or commercial routing provider. Valid downloaded Trip Segment geometry remains available offline; otherwise navigation uses Direct straight-line distance and bearing guidance. Direct is not hosted turn-by-turn routing. Authenticated provider-neutral Wayfarer routing is future work and is not implemented yet.
Mobile never contacts a public or commercial routing provider. It discovers eligible profiles with authenticated
`GET /api/mobile/routing/profiles`, confirms a selected profile with
`GET /api/mobile/routing/capability/{transportProfileId}`, and requests a transient route with
`POST /api/mobile/routing/route`. The discovery catalog identity scopes only pre-capability selection; the selected
profile authority identity fences route execution and publication. Bearer credentials remain bound to the configured
Wayfarer server, provider credentials stay server-side, and returned attribution is displayed as supplied. Mobile
uses only a non-secret process-local authentication revision plus the normalized server for in-flight publication
identity; it never copies or hashes the bearer token into routing state.

A chooser selection carries the discovery identity of the catalog the user actually saw into capability. A
`catalog-changed` response causes one bounded rediscovery and refreshed presentation; cancellation retains Direct and
no route request is sent. Catalog drift after successful capability is outside chooser authority and does not by
itself invalidate the confirmed selected profile.

Valid downloaded Trip Segment geometry remains higher authority. Hosted failures, old-server 404 responses, disabled
providers, cancellation, and stale results fall back to Direct straight-line guidance without changing the general
session. Hosted route output and profile choices are never persisted; offline hosted-route retention belongs to #261.
Safe provider/profile provenance remains attached only to a successfully published active route and clears on normal
replacement or stop.

## JSON Serialization

Expand Down
11 changes: 11 additions & 0 deletions docs/15-Security.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ The QR code for app configuration contains only:
- Tokens are not included in crash reports
- Tokens are cleared on logout

### Hosted Routing Disclosure

When a user requests hosted routing, Mobile sends the selected profile identity and the route's origin, destination,
and approved ordered anchors to the configured Wayfarer backend. Wayfarer may disclose those coordinates to its
selected routing provider. Provider credentials, provider endpoints, and native provider modes remain server-side.
Mobile uses a non-secret, process-local authentication revision to invalidate in-flight work when the effective
server/token authority changes; the bearer token is never copied, hashed, logged, or persisted as routing identity.
At publication it rereads the live origin and the exact current Trip or member target owner. The active hosted route
retains only safe provider/profile provenance and linked attribution, which clear on replacement or stop. #261 owns
any future offline retention policy.

## Secure Storage

### MAUI SecureStorage
Expand Down
3 changes: 3 additions & 0 deletions src/WayfarerMobile.Core/Interfaces/IDialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ public interface IDialogService
/// <param name="message">The error message.</param>
/// <param name="retryAction">Optional retry action.</param>
Task ShowErrorWithRetryAsync(string title, string message, Func<Task>? retryAction = null);

/// <summary>Shows a focused transient choice and returns null when dismissed.</summary>
Task<string?> SelectAsync(string title, IReadOnlyList<string> choices, string cancel = "Cancel");
}
5 changes: 5 additions & 0 deletions src/WayfarerMobile.Core/Interfaces/ISettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public interface ISettingsService
/// </summary>
string? ApiToken { get; set; }

/// <summary>
/// Gets the non-secret in-memory revision of the effective authentication authority.
/// </summary>
long AuthenticationSessionRevision { get; }

/// <summary>
/// Gets or sets the minimum time between logged locations (from server).
/// </summary>
Expand Down
19 changes: 19 additions & 0 deletions src/WayfarerMobile.Core/Models/NavigationRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,27 @@ public class NavigationRoute
/// Gets or sets the initial bearing for direct routes (degrees from north).
/// </summary>
public double InitialBearing { get; set; }

/// <summary>Gets transient linked attribution for the active hosted route.</summary>
public List<HostedRouteAttribution> Attribution { get; set; } = new();

/// <summary>Gets or sets safe transient provenance for the active hosted route.</summary>
public HostedRouteProvenance? HostedProvenance { get; set; }
}

/// <summary>Contains one safe linked attribution displayed only with an active hosted route.</summary>
public sealed record HostedRouteAttribution(string Text, string Url);

/// <summary>Safe memory-only provenance retained with an active hosted route.</summary>
public sealed record HostedRouteProvenance(
Guid TransportProfileId,
string SelectedProfileAuthorityIdentity,
string Provider,
Guid ProviderConfigurationId,
string MappingIdentity,
string StorageMode,
DateTimeOffset GeneratedAt);

/// <summary>
/// A single turn-by-turn instruction in the navigation route.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/WayfarerMobile/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ private static void ConfigureServices(IServiceCollection services)
services.AddSingleton<ApiClient>();
services.AddSingleton<IApiClient>(sp => sp.GetRequiredService<ApiClient>());
services.AddSingleton<IVisitApiClient>(sp => sp.GetRequiredService<ApiClient>());
services.AddSingleton<IHostedRoutingApiClient, HostedRoutingApiClient>();
services.AddSingleton<HostedRoutingService>();
services.AddSingleton<QueueDrainService>(); // Drains offline queue via check-in endpoint
services.AddSingleton<IPlaceOperationsHandler, PlaceOperationsHandler>();
services.AddSingleton<IRegionOperationsHandler, RegionOperationsHandler>();
Expand Down
24 changes: 16 additions & 8 deletions src/WayfarerMobile/Services/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
Expand Down Expand Up @@ -32,15 +33,22 @@ public class ApiClient : IApiClient, IVisitApiClient
/// </summary>
private readonly CircuitBreakerState _circuitBreaker = new(threshold: 3, cooldown: TimeSpan.FromSeconds(30));

private static readonly JsonSerializerOptions JsonOptions = new()
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();

private static JsonSerializerOptions CreateJsonOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
// Use relaxed encoding to prevent HTML characters (<, >) from being escaped to \u003C, \u003E
// This is needed for notes HTML content to be stored correctly on the server
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters = { new UtcDateTimeConverter() }
};
var resolver = new DefaultJsonTypeInfoResolver();
resolver.Modifiers.Add(HostedSegmentProfileIdentity.Configure);
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
TypeInfoResolver = resolver,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
options.Converters.Add(new UtcDateTimeConverter());
return options;
}

/// <summary>
/// HTTP status codes that are considered transient and should be retried.
Expand Down
7 changes: 7 additions & 0 deletions src/WayfarerMobile/Services/DialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ public async Task ShowErrorWithRetryAsync(string title, string message, Func<Tas
}
}

/// <inheritdoc />
public async Task<string?> SelectAsync(string title, IReadOnlyList<string> choices, string cancel = "Cancel")
{
var page = GetCurrentPage();
return page == null ? null : await page.DisplayActionSheetAsync(title, cancel, null, choices.ToArray());
}

private static Page? GetCurrentPage()
{
if (Application.Current?.Windows.Count > 0)
Expand Down
Loading
Loading