Skip to content

Commit a1623e4

Browse files
Merge upstream develop
2 parents 3bb90e4 + 35edf3f commit a1623e4

26 files changed

Lines changed: 867 additions & 60 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@/src/features/verifications/claims/ClaimViewScreen';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Public web base URL for this app, used to build shareable links (e.g. to a
3+
* verification claim). Override with EXPO_PUBLIC_POLYCENTRIC_APP_URL; trailing
4+
* slashes are stripped so callers can append paths directly.
5+
*/
6+
export const POLYCENTRIC_APP_URL = (
7+
process.env.EXPO_PUBLIC_POLYCENTRIC_APP_URL ?? 'https://polycentric.io'
8+
).replace(/\/+$/, '');

apps/polycentric/src/common/constants/routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export const Routes = {
3939
editProfile: (identityId: string) => `/${identityId}/edit` as const,
4040
post: (identityId: string, keyFingerprint: string, sequence: string) =>
4141
`/${identityId}/post/${keyFingerprint}/${sequence}` as const,
42+
verification: (
43+
identityId: string,
44+
keyFingerprint: string,
45+
sequence: string,
46+
) => `/${identityId}/verifications/${keyFingerprint}/${sequence}` as const,
4247
settings: {
4348
index: '/settings',
4449
identity: '/settings/identity',

apps/polycentric/src/common/theme/tokens.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ export const Atoms = StyleSheet.create({
8181
left: 0,
8282
},
8383

84+
/**
85+
* Cursors
86+
*/
87+
cursor_default: {
88+
cursor: 'auto',
89+
},
90+
cursor_pointer: {
91+
cursor: 'pointer',
92+
},
93+
8494
/**
8595
* Flex
8696
*/
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Button, Text } from '@/src/common/components';
2+
import { Sheet } from '@/src/common/components/sheet';
3+
import { POLYCENTRIC_APP_URL } from '@/src/common/constants/app';
4+
import { Atoms, useTheme } from '@/src/common/theme';
5+
import { Platform, Share, View } from 'react-native';
6+
import { CopyLinkComponent } from './CopyLinkComponent';
7+
8+
// Native gets the OS share sheet; web only if the browser supports navigator.share.
9+
const canShare =
10+
Platform.OS !== 'web' || typeof globalThis.navigator?.share === 'function';
11+
12+
export function RequestVerificationSheet({
13+
open,
14+
onClose,
15+
identityId,
16+
keyFingerprint,
17+
sequence,
18+
}: {
19+
open: boolean;
20+
onClose: () => void;
21+
identityId: string;
22+
keyFingerprint: string;
23+
sequence: string;
24+
}) {
25+
const { theme } = useTheme();
26+
const link = `${POLYCENTRIC_APP_URL}/${identityId}/verifications/${keyFingerprint}/${sequence}`;
27+
28+
const onShare = async () => {
29+
try {
30+
if (Platform.OS === 'web') {
31+
await globalThis.navigator?.share?.({ url: link });
32+
} else {
33+
await Share.share({ message: link });
34+
}
35+
} catch {
36+
// User dismissed the share sheet — nothing to do.
37+
}
38+
};
39+
40+
return (
41+
<Sheet open={open} onClose={onClose}>
42+
<View style={[Atoms.p_lg, Atoms.gap_md]}>
43+
<Text variant="title" style={theme.atoms.text}>
44+
Request verification
45+
</Text>
46+
<Text variant="body" style={theme.atoms.text_neutral_medium}>
47+
Share this link to request verification of your claim.
48+
</Text>
49+
50+
<CopyLinkComponent link={link} />
51+
52+
{canShare && (
53+
<Button
54+
title="Share"
55+
variant="secondary"
56+
icon="share"
57+
onPress={onShare}
58+
/>
59+
)}
60+
</View>
61+
</Sheet>
62+
);
63+
}

apps/polycentric/src/features/verifications/VerificationsCreateClaimScreen.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@ import { Screen } from '@/src/common/components/layout';
22
import Topbar from '@/src/common/components/layout/Topbar';
33
import { ScrollView } from '@/src/common/components/ScrollView';
44
import { Atoms } from '@/src/common/theme';
5-
import { useRouter } from 'expo-router';
65
import { View } from 'react-native';
7-
import { CreateClaim } from './CreateClaim';
6+
import { ClaimCreate } from './claims/ClaimCreate';
87

98
// Standalone route for creating a claim. The content also appears inline on
109
// the main Verifications screen; this just wraps it in its own screen.
10+
// Submitting navigates to the new claim's view (handled inside CreateClaim).
1111
export default function VerificationsCreateClaimScreen() {
12-
const router = useRouter();
13-
1412
return (
1513
<Screen>
1614
<Screen.PrimaryColumn>
@@ -19,7 +17,7 @@ export default function VerificationsCreateClaimScreen() {
1917
showsVerticalScrollIndicator={false}
2018
>
2119
<View style={Atoms.p_lg}>
22-
<CreateClaim onSubmitted={() => router.back()} />
20+
<ClaimCreate />
2321
</View>
2422
</ScrollView>
2523
</Screen.PrimaryColumn>

apps/polycentric/src/features/verifications/VerificationsScreen.tsx

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Topbar from '@/src/common/components/layout/Topbar';
44
import { ScrollView } from '@/src/common/components/ScrollView';
55
import { Atoms, Spacing, useTheme } from '@/src/common/theme';
66
import { isWeb } from '@/src/common/util/platform';
7-
import { type Ref, useCallback, useState } from 'react';
7+
import { type Ref, useCallback } from 'react';
88
import { View } from 'react-native';
99
import Animated, {
1010
type AnimatedRef,
@@ -14,24 +14,28 @@ import Animated, {
1414
useScrollOffset,
1515
} from 'react-native-reanimated';
1616
import { scheduleOnUI } from 'react-native-worklets';
17-
import { CreateClaim } from './CreateClaim';
17+
import { ClaimCreate } from './claims/ClaimCreate';
18+
import { ClaimList } from './claims/ClaimList';
1819
import { SelectChip } from './SelectChip';
1920
import { VerificationsScrollProvider } from './VerificationsScrollContext';
2021
import { useSafeAreaInsets } from 'react-native-safe-area-context';
21-
22-
type Mode = 'create' | 'verify';
22+
import {
23+
useVerificationsStore,
24+
VerificationScreenMode,
25+
} from './hooks/useVerificationsStore';
2326

2427
const MARGIN = Spacing.lg;
2528

2629
export default function VerificationsScreen() {
2730
const { theme } = useTheme();
2831
const insets = useSafeAreaInsets();
29-
const [mode, setMode] = useState<Mode>();
32+
const mode = useVerificationsStore((s) => s.mode);
33+
const setMode = useVerificationsStore((s) => s.setMode);
3034
const scrollRef = useAnimatedRef<Animated.ScrollView>();
3135
const scrollOffset = useScrollOffset(scrollRef);
3236

33-
const select = (next: Mode) =>
34-
setMode((prev) => (prev === next ? undefined : next));
37+
const select = (next: VerificationScreenMode) =>
38+
setMode(mode === next ? undefined : next);
3539

3640
// Align a revealed form section to the top of the viewport.
3741
const scrollIntoView = useCallback(
@@ -61,41 +65,44 @@ export default function VerificationsScreen() {
6165
>
6266
<View
6367
style={[
64-
Atoms.p_lg,
6568
Atoms.gap_2xl,
6669
{
6770
paddingBottom: insets.bottom + Spacing['lg'],
6871
},
6972
]}
7073
>
71-
<View style={[Atoms.flex_row, Atoms.gap_sm, Atoms.flex_wrap]}>
72-
<SelectChip
73-
title="Create a claim"
74-
icon="addOutline"
75-
color="primary_500"
76-
selected={mode === 'create'}
77-
onPress={() => select('create')}
78-
/>
79-
<SelectChip
80-
title="Verify a claim"
81-
icon="verify"
82-
color="positive_500"
83-
selected={mode === 'verify'}
84-
onPress={() => select('verify')}
85-
/>
86-
</View>
74+
<View style={[Atoms.p_lg, Atoms.gap_2xl]}>
75+
<View style={[Atoms.flex_row, Atoms.gap_sm, Atoms.flex_wrap]}>
76+
<SelectChip
77+
title="Create a claim"
78+
icon="addOutline"
79+
color="primary_500"
80+
selected={mode === 'claim'}
81+
onPress={() => select('claim')}
82+
/>
83+
<SelectChip
84+
title="Verify a claim"
85+
icon="verify"
86+
color="positive_500"
87+
selected={mode === 'verify'}
88+
onPress={() => select('verify')}
89+
/>
90+
</View>
8791

88-
{mode === 'create' && (
89-
<VerificationsScrollProvider value={scrollIntoView}>
90-
<CreateClaim />
91-
</VerificationsScrollProvider>
92-
)}
92+
{mode === 'claim' && (
93+
<VerificationsScrollProvider value={scrollIntoView}>
94+
<ClaimCreate onSubmitted={() => setMode(undefined)} />
95+
</VerificationsScrollProvider>
96+
)}
9397

94-
{mode === 'verify' && (
95-
<Text variant="body" style={theme.atoms.text_neutral_medium}>
96-
Coming soon
97-
</Text>
98-
)}
98+
{mode === 'verify' && (
99+
<Text variant="body" style={theme.atoms.text_neutral_medium}>
100+
Coming soon
101+
</Text>
102+
)}
103+
</View>
104+
{/* Resting state: the current identity's created claims. */}
105+
{!mode && <ClaimList />}
99106
</View>
100107
</ScrollView>
101108
</Screen.PrimaryColumn>

apps/polycentric/src/features/verifications/CreateClaim.tsx renamed to apps/polycentric/src/features/verifications/claims/ClaimCreate.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ import Animated, {
77
FadeOutDown,
88
useAnimatedRef,
99
} from 'react-native-reanimated';
10-
import { ClaimForm } from './ClaimForm';
11-
import { CLAIM_TYPES, ClaimType } from './utils/forms';
12-
import { PlatformPicker } from './PlatformPicker';
13-
import { SelectChip } from './SelectChip';
14-
import { useScrollIntoView } from './VerificationsScrollContext';
10+
import { useToast } from '@/src/common/components/toast';
11+
import { Routes } from '@/src/common/constants/routes';
12+
import { router } from 'expo-router';
13+
import { ClaimCreateForm } from './ClaimCreateForm';
14+
import { ClaimRef } from '../hooks/useCreateClaim';
15+
import { CLAIM_TYPES, ClaimType } from '../utils/forms';
16+
import { ClaimCreatePlatformPicker } from './ClaimCreatePlatformPicker';
17+
import { SelectChip } from '../SelectChip';
18+
import { useScrollIntoView } from '../VerificationsScrollContext';
1519

16-
export function CreateClaim({ onSubmitted }: { onSubmitted?: () => void }) {
20+
export function ClaimCreate({ onSubmitted }: { onSubmitted?: () => void }) {
1721
const { theme } = useTheme();
22+
const toast = useToast();
1823
const [selectedClaimType, setSelectedClaimType] =
1924
useState<ClaimType['name']>();
2025

@@ -40,7 +45,12 @@ export function CreateClaim({ onSubmitted }: { onSubmitted?: () => void }) {
4045
scrollIntoView(formRef);
4146
};
4247

43-
const handleSubmitted = () => {
48+
const handleSubmitted = (ref: ClaimRef) => {
49+
// Toast first, then navigate to the new claim's view.
50+
toast.success('Claim created');
51+
router.push(
52+
Routes.tabs.verification(ref.identity, ref.keyFingerprint, ref.sequence),
53+
);
4454
setSelectedClaimType(undefined);
4555
onSubmitted?.();
4656
};
@@ -84,9 +94,12 @@ export function CreateClaim({ onSubmitted }: { onSubmitted?: () => void }) {
8494
onLayout={onFormLayout}
8595
>
8696
{selected.platform ? (
87-
<PlatformPicker />
97+
<ClaimCreatePlatformPicker />
8898
) : (
89-
<ClaimForm claimType={selected} onSubmitted={handleSubmitted} />
99+
<ClaimCreateForm
100+
claimType={selected}
101+
onSubmitted={handleSubmitted}
102+
/>
90103
)}
91104
</Animated.View>
92105
)}

apps/polycentric/src/features/verifications/ClaimForm.tsx renamed to apps/polycentric/src/features/verifications/claims/ClaimCreateForm.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ import { Button, Text, TextInput } from '@/src/common/components';
22
import { Atoms, useTheme } from '@/src/common/theme';
33
import { useMemo, useState } from 'react';
44
import { View } from 'react-native';
5-
import { ClaimType, FormField } from './utils/forms';
6-
import useCreateClaim from './hooks/useCreateClaim';
7-
import { formToSchema } from './utils/schemas';
5+
import { ClaimType, FormField } from '../utils/forms';
6+
import useCreateClaim, { ClaimRef } from '../hooks/useCreateClaim';
7+
import { formToSchema } from '../utils/schemas';
88

99
// Renders the input form for a claim type, collects values, and publishes the
1010
// claim. `onSubmitted` fires once the claim is created. Remount (via a `key`
1111
// on the claim type) to reset between types.
12-
export function ClaimForm({
12+
export function ClaimCreateForm({
1313
claimType,
1414
onSubmitted,
1515
}: {
1616
claimType: ClaimType;
17-
onSubmitted: () => void;
17+
onSubmitted: (ref: ClaimRef) => void;
1818
}) {
1919
const { theme } = useTheme();
2020
const { submit, isPending } = useCreateClaim();
@@ -33,8 +33,8 @@ export function ClaimForm({
3333
if (!isValid || isPending) return;
3434
setError(null);
3535
try {
36-
await submit({ schema: formToSchema(claimType), values });
37-
onSubmitted();
36+
const ref = await submit({ schema: formToSchema(claimType), values });
37+
if (ref) onSubmitted(ref);
3838
} catch (e) {
3939
setError(e instanceof Error ? e.message : String(e));
4040
}

apps/polycentric/src/features/verifications/PlatformPicker.tsx renamed to apps/polycentric/src/features/verifications/claims/ClaimCreatePlatformPicker.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import Animated, {
99
FadeOutDown,
1010
useAnimatedRef,
1111
} from 'react-native-reanimated';
12-
import { CopyLinkComponent } from './CopyLinkComponent';
13-
import { SelectChip } from './SelectChip';
14-
import { useScrollIntoView } from './VerificationsScrollContext';
12+
import { CopyLinkComponent } from '../CopyLinkComponent';
13+
import { SelectChip } from '../SelectChip';
14+
import { useScrollIntoView } from '../VerificationsScrollContext';
1515

1616
// A platform a claim can be verified against. `logo` matches SelectChip's icon
1717
// render-prop; `color` tints the logo and its chip; `location` is where the
@@ -111,7 +111,7 @@ function isProfileUrl(value: string): boolean {
111111
}
112112

113113
// Pick a platform, then link the account by pasting a pairing token into it.
114-
export function PlatformPicker() {
114+
export function ClaimCreatePlatformPicker() {
115115
const { theme } = useTheme();
116116
const { identityKey } = useCurrentIdentity();
117117
const [selected, setSelected] = useState<Platform>();

0 commit comments

Comments
 (0)