diff --git a/README.md b/README.md
index e23dc0f..9a0d45f 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,7 @@ The backend also includes persistence-mode switching, relay execution/orchestrat
### Product-facing surfaces
-The user-facing app has real route structure and state modules, but several flows still rely on demo fixtures, safe fallbacks, or placeholder actions while the backend docking continues. In practice, the product app is ahead on screen coverage and interaction scaffolding, but not every surface is fully wired to production data end-to-end.
+The user-facing app now resolves auth session bootstrap, marketplace preview, provider discovery, and provider profile/detail from platform API contracts by default. Fallback fixtures remain only for true request failures and some non-critical UX paths still contain placeholder actions while backend docking continues.
### Backend and operator logic
diff --git a/apps/product-app/app/provider-profile.js b/apps/product-app/app/provider-profile.js
index 9a09f9c..bf56a29 100644
--- a/apps/product-app/app/provider-profile.js
+++ b/apps/product-app/app/provider-profile.js
@@ -1,5 +1,98 @@
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import { useEffect, useState } from 'react';
+import { ActivityIndicator, Pressable, Text, View } from 'react-native';
+
+import { loadProviderDetail } from '../src/features/discovery/provider-detail-actions';
import { ProviderProfile } from '../src/features/marketplace/provider-profile-screen';
+import { productAppShell } from '../src/shared/app-shell';
export default function ProviderProfileRoute() {
- return ;
+ const router = useRouter();
+ const { providerUserId } = useLocalSearchParams();
+ const [provider, setProvider] = useState(undefined);
+ const [errorMessage, setErrorMessage] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const normalizedProviderUserId = Array.isArray(providerUserId) ? providerUserId[0] : providerUserId;
+
+ useEffect(() => {
+ if (!normalizedProviderUserId || typeof normalizedProviderUserId !== 'string') {
+ setErrorMessage('Missing provider id. Open this route from discovery.');
+ setIsLoading(false);
+ return;
+ }
+
+ setIsLoading(true);
+ setErrorMessage(undefined);
+ setProvider(undefined);
+
+ loadProviderDetail(normalizedProviderUserId)
+ .then((result) => {
+ if (result.notFound) {
+ setErrorMessage('Provider not found.');
+ return;
+ }
+
+ if (result.errorMessage) {
+ setErrorMessage(result.errorMessage);
+ return;
+ }
+
+ setProvider({
+ name: result.provider.displayName,
+ title:
+ result.provider.tradeCategories.length > 0
+ ? result.provider.tradeCategories.join(' / ')
+ : 'Verified provider',
+ bio: result.provider.bio,
+ });
+ })
+ .catch((error) => {
+ setErrorMessage(error instanceof Error ? error.message : 'Unexpected error loading provider.');
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }, [normalizedProviderUserId]);
+
+ if (isLoading) {
+ return (
+
+
+ Loading provider profile…
+
+ );
+ }
+
+ if (errorMessage) {
+ return (
+
+ {errorMessage}
+ router.replace('/discovery')}
+ style={{ marginTop: 10 }}
+ >
+ Back to discovery
+
+
+ );
+ }
+
+ return (
+ router.replace('/discovery')}
+ onRequest={() =>
+ router.push({
+ pathname: '/booking-wizard',
+ params: {
+ providerUserId: normalizedProviderUserId,
+ providerName: provider?.name,
+ },
+ })
+ }
+ />
+ );
}
diff --git a/apps/product-app/src/features/auth/auth-action-panel.js b/apps/product-app/src/features/auth/auth-action-panel.js
index 3376c85..5c1f2b1 100644
--- a/apps/product-app/src/features/auth/auth-action-panel.js
+++ b/apps/product-app/src/features/auth/auth-action-panel.js
@@ -30,7 +30,7 @@ const createPanelContent = (actionId, authEntryState) => {
title: 'Sign in',
description: 'Fast returning-customer entry, now wired to a real session token endpoint.',
fields: [
- ['Email address', 'customer.demo@quickwerk.local'],
+ ['Email address', 'customer@quickwerk.local'],
['Role', 'customer'],
],
buttonLabel: 'Continue with sign in',
diff --git a/apps/product-app/src/features/auth/auth-entry-section.js b/apps/product-app/src/features/auth/auth-entry-section.js
index ee23937..fb727da 100644
--- a/apps/product-app/src/features/auth/auth-entry-section.js
+++ b/apps/product-app/src/features/auth/auth-entry-section.js
@@ -116,7 +116,7 @@ export function AuthEntrySection({ authEntryState, actionStatusMessage, isSubmit
{authEntryState.errorMessage ? (
- Local demo data is active so the walkthrough stays reliable.
+ Session bootstrap is degraded: {authEntryState.errorMessage}
) : null}
diff --git a/apps/product-app/src/features/discovery/provider-detail-actions.ts b/apps/product-app/src/features/discovery/provider-detail-actions.ts
index a38eb88..6f27429 100644
--- a/apps/product-app/src/features/discovery/provider-detail-actions.ts
+++ b/apps/product-app/src/features/discovery/provider-detail-actions.ts
@@ -7,6 +7,7 @@
import { createGetPublicProviderRequest } from '@quickwerk/api-client';
+import { runtimeConfig } from '../../shared/runtime-config';
import type { PublicProviderSummary } from './provider-discovery-state';
export type LoadProviderDetailResult =
@@ -34,7 +35,7 @@ export async function loadProviderDetail(
let response: Response;
try {
- response = await fetchImpl(request.path, { method: request.method });
+ response = await fetchImpl(`${runtimeConfig.platformApiBaseUrl}${request.path}`, { method: request.method });
} catch (err) {
return { errorMessage: err instanceof Error ? err.message : 'Unexpected error loading provider.' };
}
diff --git a/apps/product-app/src/features/marketplace/marketplace-preview-data.test.ts b/apps/product-app/src/features/marketplace/marketplace-preview-data.test.ts
index 0532817..8e1b444 100644
--- a/apps/product-app/src/features/marketplace/marketplace-preview-data.test.ts
+++ b/apps/product-app/src/features/marketplace/marketplace-preview-data.test.ts
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
import {
defaultMarketplacePreviewResult,
- fallbackMarketplacePreviewSections,
loadMarketplacePreview,
} from './marketplace-preview-data';
@@ -35,7 +34,7 @@ describe('loadMarketplacePreview', () => {
});
});
- it('sanitizes invalid payload sections and returns fallback sections with platform-api source', async () => {
+ it('returns an explicit degraded platform-api result when payload sections are invalid', async () => {
const fetchMock = async () =>
({
ok: true,
@@ -47,10 +46,11 @@ describe('loadMarketplacePreview', () => {
const result = await loadMarketplacePreview(fetchMock as typeof fetch);
expect(result).toMatchObject({
- sections: fallbackMarketplacePreviewSections,
+ sections: [],
source: 'platform-api',
+ errorMessage: 'Marketplace preview payload did not include valid sections.',
});
- expect(result.errorMessage).toBeUndefined();
+ expect(result.previewHealth.level).toBe('critical');
});
it('sanitizes invalid optional fields while preserving an otherwise valid section', async () => {
@@ -306,4 +306,80 @@ describe('loadMarketplacePreview', () => {
expect(result.sections[0]?.dataCoverageBandToken).toBe('coverage-medium');
expect(result.sections[0]?.sectionAlignmentToken).toBe('align-mixed');
});
-});
+
+ it('returns degraded platform-api response when payload.sections is an object', async () => {
+ const fetchMock = async () =>
+ ({
+ ok: true,
+ json: async () => ({
+ sections: { invalid: 'object' },
+ }),
+ }) as Response;
+
+ const result = await loadMarketplacePreview(fetchMock as typeof fetch);
+
+ expect(result).toMatchObject({
+ sections: [],
+ source: 'platform-api',
+ errorMessage: 'Marketplace preview payload did not include valid sections.',
+ });
+ expect(result.previewHealth.level).toBe('critical');
+ });
+
+ it('returns degraded platform-api response when payload.sections is a string', async () => {
+ const fetchMock = async () =>
+ ({
+ ok: true,
+ json: async () => ({
+ sections: 'bad',
+ }),
+ }) as Response;
+
+ const result = await loadMarketplacePreview(fetchMock as typeof fetch);
+
+ expect(result).toMatchObject({
+ sections: [],
+ source: 'platform-api',
+ errorMessage: 'Marketplace preview payload did not include valid sections.',
+ });
+ expect(result.previewHealth.level).toBe('critical');
+ });
+
+ it('returns degraded platform-api response when payload.sections is a number', async () => {
+ const fetchMock = async () =>
+ ({
+ ok: true,
+ json: async () => ({
+ sections: 42,
+ }),
+ }) as Response;
+
+ const result = await loadMarketplacePreview(fetchMock as typeof fetch);
+
+ expect(result).toMatchObject({
+ sections: [],
+ source: 'platform-api',
+ errorMessage: 'Marketplace preview payload did not include valid sections.',
+ });
+ expect(result.previewHealth.level).toBe('critical');
+ });
+
+ it('returns degraded platform-api response when payload.sections is null', async () => {
+ const fetchMock = async () =>
+ ({
+ ok: true,
+ json: async () => ({
+ sections: null,
+ }),
+ }) as Response;
+
+ const result = await loadMarketplacePreview(fetchMock as typeof fetch);
+
+ expect(result).toMatchObject({
+ sections: [],
+ source: 'platform-api',
+ errorMessage: 'Marketplace preview payload did not include valid sections.',
+ });
+ expect(result.previewHealth.level).toBe('critical');
+ });
+});
\ No newline at end of file
diff --git a/apps/product-app/src/features/marketplace/marketplace-preview-data.ts b/apps/product-app/src/features/marketplace/marketplace-preview-data.ts
index 7b9e1ac..1866de4 100644
--- a/apps/product-app/src/features/marketplace/marketplace-preview-data.ts
+++ b/apps/product-app/src/features/marketplace/marketplace-preview-data.ts
@@ -158,6 +158,25 @@ const buildPreviewStatusDigest = ({
`${level}|${severityBadgeToken}|${coverageBandToken}|${alignmentToken}|g${goodSections}-w${watchSections}-c${criticalSections}|cw${coverageWellSections}-cp${coveragePartialSections}-cm${coverageMinimalSections}`;
const derivePreviewHealth = (sections: readonly MarketplacePreviewSection[]): PreviewHealthIndicator => {
+ if (sections.length === 0) {
+ return {
+ level: 'critical',
+ summary: 'Marketplace preview payload is empty.',
+ narrative: 'No valid preview sections were returned by platform-api.',
+ riskHeadline: 'Critical risk: preview cannot render customer entry sections from live data.',
+ severityBadgeToken: 'badge-critical',
+ statusDigest: 'critical|badge-critical|coverage-low|align-risk|g0-w0-c0|cw0-cp0-cm0',
+ coverageBandToken: 'coverage-low',
+ alignmentToken: 'align-risk',
+ criticalSections: 0,
+ watchSections: 0,
+ goodSections: 0,
+ coverageMinimalSections: 0,
+ coveragePartialSections: 0,
+ coverageWellSections: 0,
+ };
+ }
+
const completenessValues = sections
.map((section) => section.payloadCompletenessPercent)
.filter((value): value is number => typeof value === 'number');
@@ -419,16 +438,24 @@ export async function loadMarketplacePreview(fetchImpl: typeof fetch = fetch): P
}
const payload = (await response.json()) as MarketplacePreviewPayload;
- const sections =
- payload.sections
- ?.map((section) => normalizeMarketplacePreviewSection(section))
- .filter((section): section is MarketplacePreviewSection => section !== null) ?? [];
+ const sections = Array.isArray(payload.sections)
+ ? payload.sections
+ .map((section) => normalizeMarketplacePreviewSection(section))
+ .filter((section): section is MarketplacePreviewSection => section !== null)
+ : [];
- const resolvedSections = sections.length > 0 ? sections : fallbackMarketplacePreviewSections;
+ if (sections.length === 0) {
+ return {
+ sections: [],
+ previewHealth: derivePreviewHealth([]),
+ source: 'platform-api',
+ errorMessage: 'Marketplace preview payload did not include valid sections.',
+ };
+ }
return {
- sections: resolvedSections,
- previewHealth: derivePreviewHealth(resolvedSections),
+ sections,
+ previewHealth: derivePreviewHealth(sections),
source: 'platform-api',
};
} catch (error) {
@@ -437,4 +464,4 @@ export async function loadMarketplacePreview(fetchImpl: typeof fetch = fetch): P
errorMessage: error instanceof Error ? error.message : 'Unknown marketplace preview failure.',
};
}
-}
+}
\ No newline at end of file
diff --git a/apps/product-app/src/features/marketplace/marketplace-preview-screen.js b/apps/product-app/src/features/marketplace/marketplace-preview-screen.js
index c0c7256..4bfced8 100644
--- a/apps/product-app/src/features/marketplace/marketplace-preview-screen.js
+++ b/apps/product-app/src/features/marketplace/marketplace-preview-screen.js
@@ -170,7 +170,12 @@ export function MarketplacePreviewScreen() {
}}
>
- Demo mode: {isLoading ? 'loading…' : previewResult.source === 'platform-api' ? 'Live preview fixtures' : 'Local fallback fixtures'}
+ Data source:{' '}
+ {isLoading
+ ? 'loading…'
+ : previewResult.source === 'platform-api'
+ ? 'Platform API'
+ : 'Fallback fixture (request failure)'}
- We are currently showing local demo data to keep the walkthrough stable.
+ {previewResult.errorMessage}
) : null}
diff --git a/apps/product-app/src/features/marketplace/provider-profile-screen.js b/apps/product-app/src/features/marketplace/provider-profile-screen.js
index 088469c..119d281 100644
--- a/apps/product-app/src/features/marketplace/provider-profile-screen.js
+++ b/apps/product-app/src/features/marketplace/provider-profile-screen.js
@@ -166,21 +166,27 @@ function ReviewCard({ review }) {
);
}
-const DEFAULT_PROVIDER = {
- name: 'Alex Schneider',
- title: 'Verified Emergency Plumber',
- etaMin: 12,
- rating: 4.9,
- jobCount: 342,
- bio: 'With over 15 years of experience in Vienna, I specialize in rapid response plumbing emergencies. My goal is to fix your issue quickly while making sure you understand exactly what\'s being done.',
- review: {
- text: '"Alex was a lifesaver! Arrived exactly on time, was very reassuring, and fixed the burst pipe without any hidden fees. Highly recommend for any plumbing emergency."',
- reviewer: 'Sarah M.',
- },
-};
+const createResolvedProvider = (provider) => ({
+ name: provider?.name?.trim() || 'Provider',
+ title: provider?.title?.trim() || 'Profile details are loading from platform data.',
+ etaMin: Number.isFinite(provider?.etaMin) ? provider.etaMin : null,
+ rating: Number.isFinite(provider?.rating) ? provider.rating : null,
+ jobCount: Number.isFinite(provider?.jobCount) ? provider.jobCount : null,
+ bio:
+ provider?.bio?.trim() ||
+ 'Provider biography is currently unavailable from the API. Please continue with discovery or retry later.',
+ review:
+ provider?.review &&
+ typeof provider.review.text === 'string' &&
+ provider.review.text.trim() &&
+ typeof provider.review.reviewer === 'string' &&
+ provider.review.reviewer.trim()
+ ? provider.review
+ : null,
+});
-export function ProviderProfile({ provider = DEFAULT_PROVIDER, onRequest, onClose }) {
- const p = { ...DEFAULT_PROVIDER, ...provider };
+export function ProviderProfile({ provider, onRequest, onClose }) {
+ const p = createResolvedProvider(provider);
return (
@@ -274,11 +280,11 @@ export function ProviderProfile({ provider = DEFAULT_PROVIDER, onRequest, onClos
...shadow.soft,
}}
>
-
+
-
+
-
+
{/* About section */}
@@ -303,18 +309,21 @@ export function ProviderProfile({ provider = DEFAULT_PROVIDER, onRequest, onClos
{p.bio}
- {/* Recent Review */}
-
- Recent Review
-
-
+ {p.review ? (
+ <>
+
+ Recent Review
+
+
+ >
+ ) : null}
{/* Fixed bottom CTA */}
diff --git a/apps/product-app/src/shared/session-bootstrap.ts b/apps/product-app/src/shared/session-bootstrap.ts
index a37b087..187003e 100644
--- a/apps/product-app/src/shared/session-bootstrap.ts
+++ b/apps/product-app/src/shared/session-bootstrap.ts
@@ -100,7 +100,7 @@ export async function loadSessionBootstrap(
export async function signInWithSessionBootstrap(
fetchImpl: typeof fetch = fetch,
- signIn: SignInRequestBody = { email: 'customer.demo@quickwerk.local', role: 'customer' },
+ signIn: SignInRequestBody = { role: 'customer' },
): Promise {
const request = createSignInRequest(signIn);