Skip to content

Commit a1457f6

Browse files
committed
refactor: centralize responsive value selection
1 parent b3c5603 commit a1457f6

File tree

12 files changed

+523
-131
lines changed

12 files changed

+523
-131
lines changed

app/src/components/Mnemonic.tsx

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
// SPDX-License-Identifier: BUSL-1.1
33
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
44

5-
import React, { useCallback, useState } from 'react';
5+
import React, { useCallback, useMemo, useState } from 'react';
66
import { Button, Text, XStack, YStack } from 'tamagui';
77
import Clipboard from '@react-native-clipboard/clipboard';
88

99
import { useSettingStore } from '@/stores/settingStore';
10+
import useCompactLayout from '@/hooks/useCompactLayout';
1011
import {
1112
black,
1213
slate50,
@@ -25,23 +26,24 @@ interface MnemonicProps {
2526
interface WordPill {
2627
index: number;
2728
word: string;
29+
compact: boolean;
2830
}
29-
const WordPill = ({ index, word }: WordPill) => {
31+
const WordPill = ({ index, word, compact }: WordPill) => {
3032
return (
3133
<XStack
3234
key={index}
3335
borderColor={slate300}
3436
backgroundColor={white}
3537
borderWidth="$0.5"
3638
borderRadius="$2"
37-
padding={4}
38-
minWidth={26}
39-
gap={4}
39+
padding={compact ? 3 : 4}
40+
minWidth={compact ? 22 : 26}
41+
gap={compact ? 3 : 4}
4042
>
41-
<Text color={slate300} fontSize={14} fontWeight={500}>
43+
<Text color={slate300} fontSize={compact ? 13 : 14} fontWeight={500}>
4244
{index}
4345
</Text>
44-
<Text color={slate500} fontSize={14} fontWeight={500}>
46+
<Text color={slate500} fontSize={compact ? 13 : 14} fontWeight={500}>
4547
{word}
4648
</Text>
4749
</XStack>
@@ -54,6 +56,34 @@ const Mnemonic = ({ words = REDACTED, onRevealWords }: MnemonicProps) => {
5456
const [revealWords, setRevealWords] = useState(false);
5557
const [copied, setCopied] = useState(false);
5658
const { setHasViewedRecoveryPhrase } = useSettingStore();
59+
const { isCompactWidth, selectResponsiveValues } = useCompactLayout();
60+
const {
61+
containerPaddingHorizontal,
62+
containerPaddingVertical,
63+
containerGap,
64+
buttonPadding,
65+
} = selectResponsiveValues({
66+
containerPaddingHorizontal: {
67+
compact: 18,
68+
regular: 26,
69+
dimension: 'width',
70+
},
71+
containerPaddingVertical: {
72+
compact: 22,
73+
regular: 28,
74+
dimension: 'width',
75+
},
76+
containerGap: { compact: 10, regular: 12, dimension: 'width' },
77+
buttonPadding: { compact: 12, regular: 16, dimension: 'width' },
78+
});
79+
const containerSpacing = useMemo(
80+
() => ({
81+
paddingHorizontal: containerPaddingHorizontal,
82+
paddingVertical: containerPaddingVertical,
83+
gap: containerGap,
84+
}),
85+
[containerGap, containerPaddingHorizontal, containerPaddingVertical],
86+
);
5787
const copyToClipboardOrReveal = useCallback(async () => {
5888
confirmTap();
5989
if (!revealWords) {
@@ -76,13 +106,18 @@ const Mnemonic = ({ words = REDACTED, onRevealWords }: MnemonicProps) => {
76106
borderBottomWidth={0}
77107
borderTopLeftRadius="$5"
78108
borderTopRightRadius="$5"
79-
gap={12}
80-
paddingHorizontal={26}
81-
paddingVertical={28}
109+
gap={containerSpacing.gap}
110+
paddingHorizontal={containerSpacing.paddingHorizontal}
111+
paddingVertical={containerSpacing.paddingVertical}
82112
flexWrap="wrap"
83113
>
84114
{(revealWords ? words : REDACTED).map((word, i) => (
85-
<WordPill key={i} word={word} index={i} />
115+
<WordPill
116+
key={i}
117+
word={word}
118+
index={i}
119+
compact={isCompactWidth}
120+
/>
86121
))}
87122
</XStack>
88123
<XStack
@@ -100,7 +135,7 @@ const Mnemonic = ({ words = REDACTED, onRevealWords }: MnemonicProps) => {
100135
borderTopWidth={0}
101136
borderBottomLeftRadius="$5"
102137
borderBottomRightRadius="$5"
103-
paddingVertical={16}
138+
paddingVertical={buttonPadding}
104139
onPress={copyToClipboardOrReveal}
105140
width="100%"
106141
textAlign="center"

app/src/hooks/useCompactLayout.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
4+
5+
import { useCallback } from 'react';
6+
import { useWindowDimensions } from 'react-native';
7+
8+
export const DEFAULT_COMPACT_WIDTH = 360;
9+
export const DEFAULT_COMPACT_HEIGHT = 720;
10+
11+
interface UseCompactLayoutOptions {
12+
compactWidth?: number;
13+
compactHeight?: number;
14+
}
15+
16+
type ResponsiveDimension = 'width' | 'height' | 'any';
17+
18+
interface ResponsivePaddingOptions {
19+
min?: number;
20+
max?: number;
21+
percent?: number;
22+
}
23+
24+
type ResponsiveValueConfig<T> = {
25+
compact: T;
26+
regular: T;
27+
dimension?: ResponsiveDimension;
28+
};
29+
30+
const useCompactLayout = (
31+
options: UseCompactLayoutOptions = {},
32+
): {
33+
width: number;
34+
height: number;
35+
isCompactWidth: boolean;
36+
isCompactHeight: boolean;
37+
isCompact: boolean;
38+
selectResponsiveValue: <T>(
39+
compactValue: T,
40+
regularValue: T,
41+
dimension?: ResponsiveDimension,
42+
) => T;
43+
selectResponsiveValues: <TConfig extends Record<string, ResponsiveValueConfig<unknown>>>(
44+
config: TConfig,
45+
) => {
46+
[K in keyof TConfig]: TConfig[K] extends ResponsiveValueConfig<infer TValue>
47+
? TValue
48+
: never;
49+
};
50+
getResponsiveHorizontalPadding: (options?: ResponsivePaddingOptions) => number;
51+
} => {
52+
const { width, height } = useWindowDimensions();
53+
const compactWidth = options.compactWidth ?? DEFAULT_COMPACT_WIDTH;
54+
const compactHeight = options.compactHeight ?? DEFAULT_COMPACT_HEIGHT;
55+
56+
const isCompactWidth = width < compactWidth;
57+
const isCompactHeight = height < compactHeight;
58+
const selectResponsiveValue = useCallback(
59+
<T>(
60+
compactValue: T,
61+
regularValue: T,
62+
dimension: ResponsiveDimension = 'any',
63+
): T => {
64+
if (dimension === 'width') {
65+
return isCompactWidth ? compactValue : regularValue;
66+
}
67+
68+
if (dimension === 'height') {
69+
return isCompactHeight ? compactValue : regularValue;
70+
}
71+
72+
return isCompactWidth || isCompactHeight ? compactValue : regularValue;
73+
},
74+
[isCompactHeight, isCompactWidth],
75+
);
76+
77+
const selectResponsiveValues = useCallback(
78+
<TConfig extends Record<string, ResponsiveValueConfig<unknown>>>(
79+
config: TConfig,
80+
) => {
81+
const entries = Object.entries(config) as Array<[
82+
keyof TConfig,
83+
ResponsiveValueConfig<unknown>,
84+
]>;
85+
const result = {} as {
86+
[K in keyof TConfig]: TConfig[K] extends ResponsiveValueConfig<infer TValue>
87+
? TValue
88+
: never;
89+
};
90+
91+
entries.forEach(([key, { compact, regular, dimension }]) => {
92+
result[key] = selectResponsiveValue(compact, regular, dimension);
93+
});
94+
95+
return result;
96+
},
97+
[selectResponsiveValue],
98+
);
99+
100+
const getResponsiveHorizontalPadding = useCallback(
101+
(paddingOptions: ResponsivePaddingOptions = {}): number => {
102+
const { min = 16, max, percent = 0.06 } = paddingOptions;
103+
const computed = width * percent;
104+
const withMin = Math.max(min, computed);
105+
return typeof max === 'number' ? Math.min(max, withMin) : withMin;
106+
},
107+
[width],
108+
);
109+
110+
return {
111+
width,
112+
height,
113+
isCompactWidth,
114+
isCompactHeight,
115+
isCompact: isCompactWidth || isCompactHeight,
116+
selectResponsiveValue,
117+
selectResponsiveValues,
118+
getResponsiveHorizontalPadding,
119+
};
120+
};
121+
122+
export default useCompactLayout;

app/src/layouts/ExpandableBottomLayout.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: BUSL-1.1
33
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
44

5-
import React from 'react';
5+
import React, { useMemo } from 'react';
66
import {
77
Dimensions,
88
PixelRatio,
@@ -17,6 +17,7 @@ import { View } from 'tamagui';
1717

1818
import { black, white } from '@/utils/colors';
1919
import { extraYPadding } from '@/utils/constants';
20+
import useCompactLayout from '@/hooks/useCompactLayout';
2021

2122
// Get the current font scale factor
2223
const fontScale = PixelRatio.getFontScale();
@@ -57,7 +58,17 @@ const TopSection: React.FC<TopSectionProps> = ({
5758
...props
5859
}) => {
5960
const { top } = useSafeAreaInsets();
60-
const { roundTop, ...restProps } = props;
61+
const { roundTop, style: incomingStyle, ...restProps } = props;
62+
const { selectResponsiveValue } = useCompactLayout({
63+
compactHeight: 760,
64+
});
65+
const spacingStyle = useMemo(
66+
() => ({
67+
paddingHorizontal: selectResponsiveValue(16, 20, 'width'),
68+
paddingVertical: selectResponsiveValue(16, 20, 'height'),
69+
}),
70+
[selectResponsiveValue],
71+
);
6172
return (
6273
<View
6374
{...restProps}
@@ -66,7 +77,9 @@ const TopSection: React.FC<TopSectionProps> = ({
6677
styles.topSection,
6778
roundTop && styles.roundTop,
6879
roundTop ? { marginTop: top } : { paddingTop: top },
80+
spacingStyle,
6981
{ backgroundColor },
82+
incomingStyle,
7083
]}
7184
>
7285
{children}
@@ -108,6 +121,16 @@ const BottomSection: React.FC<BottomSectionProps> = ({
108121
const minBottom = safeAreaBottom + extraYPadding;
109122
const totalBottom =
110123
typeof incomingBottom === 'number' ? minBottom + incomingBottom : minBottom;
124+
const { selectResponsiveValue } = useCompactLayout({
125+
compactHeight: 760,
126+
});
127+
const spacingStyle = useMemo(
128+
() => ({
129+
paddingHorizontal: selectResponsiveValue(16, 20, 'width'),
130+
paddingTop: selectResponsiveValue(18, 30, 'height'),
131+
}),
132+
[selectResponsiveValue],
133+
);
111134

112135
let panelHeight: number | 'auto' = 'auto';
113136
// set bottom section height to 38% of screen height
@@ -129,7 +152,7 @@ const BottomSection: React.FC<BottomSectionProps> = ({
129152
<View
130153
{...props}
131154
height={panelHeight}
132-
style={[styles.bottomSection, style]}
155+
style={[styles.bottomSection, spacingStyle, style]}
133156
paddingBottom={totalBottom}
134157
>
135158
{children}

app/src/screens/account/recovery/AccountRecoveryScreen.tsx

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {
1212
Title,
1313
} from '@selfxyz/mobile-sdk-alpha/components';
1414
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
15+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
1516

1617
import useHapticNavigation from '@/hooks/useHapticNavigation';
1718
import RestoreAccountSvg from '@/images/icons/restore_account.svg';
1819
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
20+
import useCompactLayout from '@/hooks/useCompactLayout';
1921
import { black, slate600, white } from '@/utils/colors';
2022

2123
const AccountRecoveryScreen: React.FC = () => {
@@ -25,6 +27,30 @@ const AccountRecoveryScreen: React.FC = () => {
2527
nextScreen: 'SaveRecoveryPhrase',
2628
},
2729
});
30+
const { selectResponsiveValues, getResponsiveHorizontalPadding } = useCompactLayout();
31+
const { bottom } = useSafeAreaInsets();
32+
33+
const {
34+
iconSize,
35+
iconPadding,
36+
contentGap,
37+
descriptionSize,
38+
titleSize,
39+
buttonStackGap,
40+
buttonPaddingTop,
41+
extraBottomPadding,
42+
} = selectResponsiveValues({
43+
iconSize: { compact: 64, regular: 80 },
44+
iconPadding: { compact: '$4', regular: '$5' },
45+
contentGap: { compact: '$2', regular: '$2.5' },
46+
descriptionSize: { compact: 15, regular: 16 },
47+
titleSize: { compact: 26, regular: 32 },
48+
buttonStackGap: { compact: '$2', regular: '$2.5' },
49+
buttonPaddingTop: { compact: '$4', regular: '$6' },
50+
extraBottomPadding: { compact: 16, regular: 24 },
51+
});
52+
const horizontalPadding = getResponsiveHorizontalPadding({ percent: 0.06 });
53+
const bottomPadding = bottom + extraBottomPadding;
2854

2955
return (
3056
<ExpandableBottomLayout.Layout backgroundColor={black}>
@@ -33,20 +59,28 @@ const AccountRecoveryScreen: React.FC = () => {
3359
borderColor={slate600}
3460
borderWidth="$1"
3561
borderRadius="$10"
36-
padding="$5"
62+
padding={iconPadding}
3763
>
38-
<RestoreAccountSvg height={80} width={80} color={white} />
64+
<RestoreAccountSvg height={iconSize} width={iconSize} color={white} />
3965
</View>
4066
</ExpandableBottomLayout.TopSection>
41-
<ExpandableBottomLayout.BottomSection backgroundColor={white}>
42-
<YStack alignItems="center" gap="$2.5" paddingBottom="$2.5">
43-
<Title>Restore your Self account</Title>
44-
<Description>
67+
<ExpandableBottomLayout.BottomSection
68+
backgroundColor={white}
69+
paddingBottom={bottomPadding}
70+
paddingHorizontal={horizontalPadding}
71+
>
72+
<YStack alignItems="center" gap={contentGap} paddingBottom="$2">
73+
<Title style={{ fontSize: titleSize, textAlign: 'center' }}>
74+
Restore your Self account
75+
</Title>
76+
<Description
77+
style={{ fontSize: descriptionSize, textAlign: 'center' }}
78+
>
4579
By continuing, you certify that this passport belongs to you and is
4680
not stolen or forged.
4781
</Description>
4882

49-
<YStack gap="$2.5" width="100%" paddingTop="$6">
83+
<YStack gap={buttonStackGap} width="100%" paddingTop={buttonPaddingTop}>
5084
<PrimaryButton
5185
trackEvent={BackupEvents.ACCOUNT_RECOVERY_STARTED}
5286
onPress={onRestoreAccountPress}

0 commit comments

Comments
 (0)