Skip to content

Commit d38b764

Browse files
committed
[#115] Add ui for making a claim
Changelog: feature
1 parent 0a340a3 commit d38b764

21 files changed

Lines changed: 893 additions & 123 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@/src/features/verifications/VerificationsScreen';

apps/polycentric/app/(tabs)/verify.tsx

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@/src/features/verifications/VerificationsCreateClaimScreen';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@/src/features/verifications/VerificationsScreen';

apps/polycentric/src/common/components/Icon.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ export const IconsMap = {
2525
addOutline: defineIcon(Ionicons, 'add-circle-outline'),
2626
arrowBack: defineIcon(Ionicons, 'arrow-back'),
2727
ban: defineIcon(Ionicons, 'ban'),
28+
bookOutline: defineIcon(MaterialCommunityIcons, 'book-open-outline'),
29+
briefcaseOutline: defineIcon(MaterialCommunityIcons, 'briefcase-outline'),
2830
camera: defineIcon(Ionicons, 'camera-outline'),
31+
cardOutline: defineIcon(
32+
MaterialCommunityIcons,
33+
'card-account-details-outline',
34+
),
35+
certificateOutline: defineIcon(MaterialCommunityIcons, 'certificate-outline'),
2936
checkmark: defineIcon(Ionicons, 'checkmark'),
3037
checkmarkCircle: defineIcon(Ionicons, 'checkmark-circle'),
3138
checkmarkSharp: defineIcon(Ionicons, 'checkmark-sharp'),
@@ -38,6 +45,7 @@ export const IconsMap = {
3845
dotsVertical: defineIcon(MaterialCommunityIcons, 'dots-vertical'),
3946
emoji: defineIcon(MaterialIcons, 'emoji-emotions'),
4047
flag: defineIcon(Ionicons, 'flag-outline'),
48+
form: defineIcon(MaterialCommunityIcons, 'form-select'),
4149
home: defineIcon(Ionicons, 'home-outline'),
4250
image: defineIcon(Ionicons, 'image-outline'),
4351
images: defineIcon(Ionicons, 'images-outline'),

apps/polycentric/src/common/components/layout/nav/VerticalNav.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ export function VerticalNav({ style }: VerticalNavProps) {
3636
icon={<Icon name="notification" size={24} />}
3737
href="/notifications"
3838
/>
39+
40+
<NavItem
41+
label="Verifications"
42+
icon={<Icon name="verify" size={24} />}
43+
href="/verifications"
44+
/>
45+
3946
{identity?.identityKey && (
4047
<NavItem
4148
label="Profile"

apps/polycentric/src/common/components/primitives/Button.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ViewStyle,
2323
} from 'react-native';
2424
import { Text } from './Text';
25+
import Icon, { IconName } from '../Icon';
2526

2627
type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'destructive';
2728

@@ -38,8 +39,8 @@ interface ButtonProps extends Omit<PressableProps, 'style'> {
3839
title: string;
3940
variant?: ButtonVariant;
4041
size?: ButtonSize;
41-
style?: StyleProp<ViewStyle>;
42-
icon?: IconRenderFn;
42+
style?: PressableProps['style'];
43+
icon?: IconRenderFn | IconName;
4344
fullWidth?: boolean;
4445
disabled?: boolean;
4546
}
@@ -109,7 +110,7 @@ export function Button({
109110
onHoverOut={isDisabled ? undefined : onHoverOut}
110111
disabled={isDisabled}
111112
hitSlop={8}
112-
style={[
113+
style={(state) => [
113114
styles.base,
114115
fullWidth && Atoms.w_full,
115116
!fullWidth && styles.fitContent,
@@ -120,16 +121,17 @@ export function Button({
120121
},
121122
surfaceStyle,
122123
hoverStyle,
123-
style,
124+
typeof style === 'function' ? style(state) : style,
124125
]}
125126
{...props}
126127
>
127128
<View style={[styles.content]}>
128129
{icon &&
129-
icon({
130-
size: sizeConfig.iconSize,
131-
color: iconColor,
132-
})}
130+
(typeof icon === 'function' ? (
131+
icon({ size: sizeConfig.iconSize, color: iconColor })
132+
) : (
133+
<Icon name={icon} size={sizeConfig.iconSize} color={iconColor} />
134+
))}
133135
{!iconOnly && (
134136
<Text
135137
fontSize={size}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Button, Text, TextInput } from '@/src/common/components';
2+
import { Atoms, useTheme } from '@/src/common/theme';
3+
import { useMemo, useState } from 'react';
4+
import { View } from 'react-native';
5+
import { ClaimType, FormField } from './utils/forms';
6+
import useCreateClaim from './hooks/useCreateClaim';
7+
import { formToSchema } from './utils/schemas';
8+
9+
// Renders the input form for a claim type, collects values, and publishes the
10+
// claim. `onSubmitted` fires once the claim is created. Remount (via a `key`
11+
// on the claim type) to reset between types.
12+
export function ClaimForm({
13+
claimType,
14+
onSubmitted,
15+
}: {
16+
claimType: ClaimType;
17+
onSubmitted: () => void;
18+
}) {
19+
const { theme } = useTheme();
20+
const { submit, isPending } = useCreateClaim();
21+
const [values, setValues] = useState<Record<string, string>>({});
22+
const [error, setError] = useState<string | null>(null);
23+
24+
const isValid = useMemo(
25+
() =>
26+
claimType.fields.every(
27+
(f) => !f.required || (values[f.key]?.trim().length ?? 0) > 0,
28+
),
29+
[claimType, values],
30+
);
31+
32+
const onSubmit = async () => {
33+
if (!isValid || isPending) return;
34+
setError(null);
35+
try {
36+
await submit({ schema: formToSchema(claimType), values });
37+
onSubmitted();
38+
} catch (e) {
39+
setError(e instanceof Error ? e.message : String(e));
40+
}
41+
};
42+
43+
return (
44+
<View style={Atoms.gap_md}>
45+
{claimType.fields.map((field) => (
46+
<FieldInput
47+
key={field.key}
48+
field={field}
49+
value={values[field.key] ?? ''}
50+
onChange={(text) =>
51+
setValues((prev) => ({ ...prev, [field.key]: text }))
52+
}
53+
/>
54+
))}
55+
56+
{error && (
57+
<Text variant="body" style={{ color: theme.palette.negative_600 }}>
58+
{error}
59+
</Text>
60+
)}
61+
62+
<Button
63+
title={isPending ? 'Submitting…' : 'Continue'}
64+
variant="primary"
65+
onPress={onSubmit}
66+
disabled={!isValid || isPending}
67+
/>
68+
</View>
69+
);
70+
}
71+
72+
function FieldInput({
73+
field,
74+
value,
75+
onChange,
76+
}: {
77+
field: FormField;
78+
value: string;
79+
onChange: (text: string) => void;
80+
}) {
81+
const { theme } = useTheme();
82+
const multiline = field.kind === 'multiline';
83+
const isDate = field.kind === 'date';
84+
85+
return (
86+
<View style={Atoms.gap_xs}>
87+
<Text
88+
variant="small"
89+
style={theme.atoms.text_neutral_medium}
90+
fontWeight="semibold"
91+
>
92+
{field.required ? `${field.label} *` : field.label}
93+
</Text>
94+
<TextInput
95+
value={value}
96+
onChangeText={onChange}
97+
placeholder={isDate ? 'YYYY-MM-DD' : field.label}
98+
autoCapitalize={isDate ? 'none' : 'sentences'}
99+
multiline={multiline}
100+
numberOfLines={multiline ? 4 : undefined}
101+
/>
102+
</View>
103+
);
104+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Text } from '@/src/common/components';
2+
import { Atoms, useTheme } from '@/src/common/theme';
3+
import { useMemo, useState } from 'react';
4+
import { View } from 'react-native';
5+
import { ClaimForm } from './ClaimForm';
6+
import { CLAIM_TYPES, ClaimType } from './utils/forms';
7+
import { PlatformPicker } from './PlatformPicker';
8+
import { SelectChip } from './SelectChip';
9+
10+
export function CreateClaim({ onSubmitted }: { onSubmitted?: () => void }) {
11+
const { theme } = useTheme();
12+
const [selectedClaimType, setSelectedClaimType] =
13+
useState<ClaimType['name']>();
14+
15+
const selected = useMemo(
16+
() => CLAIM_TYPES.find((s) => s.name === selectedClaimType),
17+
[selectedClaimType],
18+
);
19+
20+
const onSelectClaimType = (name: ClaimType['name']) =>
21+
setSelectedClaimType(name === selectedClaimType ? undefined : name);
22+
23+
const handleSubmitted = () => {
24+
setSelectedClaimType(undefined);
25+
onSubmitted?.();
26+
};
27+
28+
return (
29+
<View style={Atoms.gap_2xl}>
30+
{/* Claim-type selector. */}
31+
<View style={Atoms.gap_sm}>
32+
<Text
33+
variant="small"
34+
style={theme.atoms.text_neutral_medium}
35+
fontWeight="semibold"
36+
>
37+
Claim type
38+
</Text>
39+
<View style={[Atoms.flex_row, Atoms.gap_sm, Atoms.flex_wrap]}>
40+
{CLAIM_TYPES.map((s) => (
41+
<SelectChip
42+
key={s.name}
43+
title={s.name}
44+
icon={s.icon}
45+
color={s.color}
46+
selected={selected?.name === s.name}
47+
onPress={() => onSelectClaimType(s.name)}
48+
/>
49+
))}
50+
</View>
51+
</View>
52+
53+
{/* Platform claims pick a platform and link an account. */}
54+
{selected?.platform && <PlatformPicker />}
55+
56+
{/* Everything else is a field form. Keyed so state resets per type. */}
57+
{selected && !selected.platform && (
58+
<ClaimForm
59+
key={selected.name}
60+
claimType={selected}
61+
onSubmitted={handleSubmitted}
62+
/>
63+
)}
64+
</View>
65+
);
66+
}

0 commit comments

Comments
 (0)