Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
95 changes: 94 additions & 1 deletion apps/product-app/app/provider-profile.js
Original file line number Diff line number Diff line change
@@ -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 <ProviderProfile />;
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 (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 10 }}>
<ActivityIndicator size="small" color={productAppShell.theme.color.primary} />
<Text style={{ color: '#64748B' }}>Loading provider profile…</Text>
</View>
);
}

if (errorMessage) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }}>
<Text style={{ color: '#B91C1C', textAlign: 'center' }}>{errorMessage}</Text>
<Pressable
accessibilityLabel="Back to discovery"
accessibilityRole="button"
onPress={() => router.replace('/discovery')}
style={{ marginTop: 10 }}
>
<Text style={{ color: productAppShell.theme.color.primary, fontWeight: '600' }}>Back to discovery</Text>
</Pressable>
</View>
);
}

return (
<ProviderProfile
provider={provider}
onClose={() => router.replace('/discovery')}
onRequest={() =>
router.push({
pathname: '/booking-wizard',
params: {
providerUserId: normalizedProviderUserId,
providerName: provider?.name,
},
})
}
/>
);
}
2 changes: 1 addition & 1 deletion apps/product-app/src/features/auth/auth-action-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion apps/product-app/src/features/auth/auth-entry-section.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function AuthEntrySection({ authEntryState, actionStatusMessage, isSubmit
</Text>
{authEntryState.errorMessage ? (
<Text testID="auth-entry-error-message" style={{ marginTop: 8, color: '#B26A00' }}>
Local demo data is active so the walkthrough stays reliable.
Session bootstrap is degraded: {authEntryState.errorMessage}
</Text>
) : null}
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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.' };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';

import {
defaultMarketplacePreviewResult,
fallbackMarketplacePreviewSections,
loadMarketplacePreview,
} from './marketplace-preview-data';

Expand Down Expand Up @@ -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,
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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.',
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
sections: resolvedSections,
previewHealth: derivePreviewHealth(resolvedSections),
sections,
previewHealth: derivePreviewHealth(sections),
source: 'platform-api',
};
} catch (error) {
Expand All @@ -437,4 +464,4 @@ export async function loadMarketplacePreview(fetchImpl: typeof fetch = fetch): P
errorMessage: error instanceof Error ? error.message : 'Unknown marketplace preview failure.',
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,12 @@ export function MarketplacePreviewScreen() {
}}
>
<Text style={{ color: productAppShell.theme.color.accent }}>
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)'}
</Text>

<View
Expand Down Expand Up @@ -215,7 +220,7 @@ export function MarketplacePreviewScreen() {

{previewResult.errorMessage ? (
<Text testID="marketplace-preview-error-message" style={{ marginTop: 10, color: '#B26A00' }}>
We are currently showing local demo data to keep the walkthrough stable.
{previewResult.errorMessage}
</Text>
) : null}
</View>
Expand Down
Loading
Loading