Skip to content

Commit 9e8016a

Browse files
committed
Add animations for when steps appear
1 parent ccab027 commit 9e8016a

4 files changed

Lines changed: 93 additions & 35 deletions

File tree

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ export type ScrollViewProps = RNScrollViewProps & {
1919
| undefined;
2020
};
2121

22-
export function ScrollView({
23-
HeaderComponent,
24-
contentContainerStyle,
25-
onScroll: _ignoredOnScroll,
26-
children,
27-
...rest
28-
}: ScrollViewProps) {
22+
export const ScrollView = React.forwardRef<
23+
Animated.ScrollView,
24+
ScrollViewProps
25+
>(function ScrollView(
26+
{
27+
HeaderComponent,
28+
contentContainerStyle,
29+
onScroll: _ignoredOnScroll,
30+
children,
31+
...rest
32+
},
33+
ref,
34+
) {
2935
const { onScroll, headerHeight, headerAnimatedStyle, onHeaderLayout } =
3036
useHidingHeader();
3137

@@ -40,6 +46,7 @@ export function ScrollView({
4046
) : null}
4147

4248
<Animated.ScrollView
49+
ref={ref}
4350
{...rest}
4451
onScroll={onScroll}
4552
scrollEventThrottle={16}
@@ -52,4 +59,4 @@ export function ScrollView({
5259
</Animated.ScrollView>
5360
</View>
5461
);
55-
}
62+
});

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import { Text } 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 Animated, { FadeInDown, FadeOutDown } from 'react-native-reanimated';
56
import { ClaimForm } from './ClaimForm';
67
import { CLAIM_TYPES, ClaimType } from './utils/forms';
78
import { PlatformPicker } from './PlatformPicker';
89
import { SelectChip } from './SelectChip';
910

10-
export function CreateClaim({ onSubmitted }: { onSubmitted?: () => void }) {
11+
export function CreateClaim({
12+
onSubmitted,
13+
onPlatformSelected,
14+
}: {
15+
onSubmitted?: () => void;
16+
onPlatformSelected?: () => void;
17+
}) {
1118
const { theme } = useTheme();
1219
const [selectedClaimType, setSelectedClaimType] =
1320
useState<ClaimType['name']>();
@@ -37,29 +44,42 @@ export function CreateClaim({ onSubmitted }: { onSubmitted?: () => void }) {
3744
Claim type
3845
</Text>
3946
<View style={[Atoms.flex_row, Atoms.gap_sm, Atoms.flex_wrap]}>
40-
{CLAIM_TYPES.map((s) => (
41-
<SelectChip
47+
{CLAIM_TYPES.map((s, i) => (
48+
<Animated.View
4249
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-
/>
50+
entering={FadeInDown.delay(i * 40).duration(200)}
51+
>
52+
<SelectChip
53+
title={s.name}
54+
icon={s.icon}
55+
color={s.color}
56+
selected={selected?.name === s.name}
57+
onPress={() => onSelectClaimType(s.name)}
58+
/>
59+
</Animated.View>
4960
))}
5061
</View>
5162
</View>
5263

5364
{/* Platform claims pick a platform and link an account. */}
54-
{selected?.platform && <PlatformPicker />}
65+
{selected?.platform && (
66+
<Animated.View
67+
entering={FadeInDown.duration(200)}
68+
exiting={FadeOutDown.duration(150)}
69+
>
70+
<PlatformPicker onSelect={onPlatformSelected} />
71+
</Animated.View>
72+
)}
5573

5674
{/* Everything else is a field form. Keyed so state resets per type. */}
5775
{selected && !selected.platform && (
58-
<ClaimForm
76+
<Animated.View
5977
key={selected.name}
60-
claimType={selected}
61-
onSubmitted={handleSubmitted}
62-
/>
78+
entering={FadeInDown.duration(200)}
79+
exiting={FadeOutDown.duration(150)}
80+
>
81+
<ClaimForm claimType={selected} onSubmitted={handleSubmitted} />
82+
</Animated.View>
6383
)}
6484
</View>
6585
);

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FontAwesome6 } from '@expo/vector-icons';
44
import { useCurrentIdentity } from '@/src/common/lib/polycentric-hooks';
55
import { ReactNode, useState } from 'react';
66
import { View } from 'react-native';
7+
import Animated, { FadeInDown, FadeOutDown } from 'react-native-reanimated';
78
import { SelectChip } from './SelectChip';
89

910
// A platform a claim can be verified against. `logo` matches SelectChip's icon
@@ -104,7 +105,9 @@ function isProfileUrl(value: string): boolean {
104105
}
105106

106107
// Pick a platform, then link the account by pasting a pairing token into it.
107-
export function PlatformPicker() {
108+
// `onSelect` fires after a platform is chosen so the parent can scroll the
109+
// freshly revealed link form into view.
110+
export function PlatformPicker({ onSelect }: { onSelect?: () => void }) {
108111
const { theme } = useTheme();
109112
const { identityKey } = useCurrentIdentity();
110113
const [selected, setSelected] = useState<Platform>();
@@ -118,6 +121,7 @@ export function PlatformPicker() {
118121
const onSelectPlatform = (platform: Platform) => {
119122
setSelected(platform);
120123
setProfileUrl('');
124+
onSelect?.();
121125
};
122126

123127
return (
@@ -131,20 +135,29 @@ export function PlatformPicker() {
131135
</Text>
132136

133137
<View style={[Atoms.flex_row, Atoms.gap_sm, Atoms.flex_wrap]}>
134-
{PLATFORMS.map((platform) => (
135-
<SelectChip
138+
{PLATFORMS.map((platform, i) => (
139+
<Animated.View
136140
key={platform.name}
137-
title={platform.name}
138-
icon={platform.logo}
139-
color={platform.color}
140-
selected={selected?.name === platform.name}
141-
onPress={() => onSelectPlatform(platform)}
142-
/>
141+
entering={FadeInDown.delay(i * 40).duration(200)}
142+
>
143+
<SelectChip
144+
title={platform.name}
145+
icon={platform.logo}
146+
color={platform.color}
147+
selected={selected?.name === platform.name}
148+
onPress={() => onSelectPlatform(platform)}
149+
/>
150+
</Animated.View>
143151
))}
144152
</View>
145153

146154
{selected && (
147-
<View style={[Atoms.gap_sm, Atoms.mt_sm]}>
155+
<Animated.View
156+
key={selected.name}
157+
entering={FadeInDown.duration(200)}
158+
exiting={FadeOutDown.duration(150)}
159+
style={[Atoms.gap_sm, Atoms.mt_sm]}
160+
>
148161
<Text
149162
variant="small"
150163
style={theme.atoms.text_neutral_medium}
@@ -194,7 +207,7 @@ export function PlatformPicker() {
194207
// TODO: start the platform loop-back verification flow.
195208
onPress={() => {}}
196209
/>
197-
</View>
210+
</Animated.View>
198211
)}
199212
</View>
200213
);

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ 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 { useState } from 'react';
7+
import { useRef, useState } from 'react';
88
import { View } from 'react-native';
9+
import Animated from 'react-native-reanimated';
910
import { CreateClaim } from './CreateClaim';
1011
import { SelectChip } from './SelectChip';
1112
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -16,18 +17,33 @@ export default function VerificationsScreen() {
1617
const { theme } = useTheme();
1718
const insets = useSafeAreaInsets();
1819
const [mode, setMode] = useState<Mode>();
20+
const scrollRef = useRef<Animated.ScrollView>(null);
21+
// Set when something below the fold is revealed; the scroll happens once the
22+
// content actually grows (see onContentSizeChange), so we measure the new
23+
// height rather than the old one.
24+
const pendingScroll = useRef(false);
1925

2026
const select = (next: Mode) =>
2127
setMode((prev) => (prev === next ? undefined : next));
2228

29+
const scrollToBottom = () => {
30+
pendingScroll.current = true;
31+
};
32+
2333
return (
2434
<Screen>
2535
<Screen.PrimaryColumn>
2636
<ScrollView
37+
ref={scrollRef}
2738
HeaderComponent={() => (
2839
<Topbar title="Verifications" left={isWeb ? <></> : undefined} />
2940
)}
3041
showsVerticalScrollIndicator={false}
42+
onContentSizeChange={() => {
43+
if (!pendingScroll.current) return;
44+
pendingScroll.current = false;
45+
scrollRef.current?.scrollToEnd({ animated: true });
46+
}}
3147
>
3248
<View
3349
style={[
@@ -55,7 +71,9 @@ export default function VerificationsScreen() {
5571
/>
5672
</View>
5773

58-
{mode === 'create' && <CreateClaim />}
74+
{mode === 'create' && (
75+
<CreateClaim onPlatformSelected={scrollToBottom} />
76+
)}
5977

6078
{mode === 'verify' && (
6179
<Text variant="body" style={theme.atoms.text_neutral_medium}>

0 commit comments

Comments
 (0)