[Contacts] Contacts matching flow (#9486)
* add expo-contacts * add expo-sms * update copy * add basic settings screen * state machine, flow screen * phone input screen * otp screen * tweak spacing * resend code logic * add layoutanimationconfig * check availablility in settings screen * get contacts * matches screen * search, temp setup for matches UI * add a bunch of number parsing logic with libphonenumber * cast to looser type * rename sync to find FCF (Find Contacts Flow) * update geolocation hook * nicer design for settings screen * add completed state * up border contrast * update expo deps * add pending spinner * add country whitelist * add empty state screen * drop add more functionality * upload -> import * fix typo * fix permission string * copy updates * rm envelope icon * update sms copy * add inviteinfo component * woke is back * [Contacts] NUXes (#9515) * add a bunch of number parsing logic with libphonenumber * add banner nux * add announcement nux * native only nux * rm shitty animation * move isNative check * [Contacts] Onboarding step (#9489) * add a bunch of number parsing logic with libphonenumber * restructure onboarding to better support dynamic screens * integrate existing flow into onboarding * add intro step * lift state up to allow going back freely * gate onboarding by geo if unsupported country * add done button to standalone flow * [Contacts] Add `contact-match` notification type to feed (#9519) * add a bunch of number parsing logic with libphonenumber * add contact-joined notif type * update string, api package * Update NotificationFeedItem.tsx * Update NotificationFeedItem.tsx * Delete SyncContactsFlow.web.tsx * fix follow back btn for this case * [Contacts] API integration (#9487) * api integration for flow * copy tweak * tweaks after running it * wire up status page * rename toast * use api lib * rm temp code * maybe fix otp error * clear code on error/resend * add 1s delay to verify success * update package versions * Delete SyncContactsFlow.web.tsx * try and fix yarn.lock lint * even woker * fix uppercase friends * surfdude feedback Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * rm log * allow resend on invalid code * Update GetContacts.tsx * overwrite country code if possible * fix Apple's Best Feature * devenv latest * disable bounces * interactive keyboard dismiss * copy changes * national format * allow resending immediately * move success state down a bit * Update FindContactsSettings.tsx * [Contacts] More onboarding changes (#9491) * integrate existing flow into onboarding * refreshed onboarding styles, rm stepper * center content on web * Add back dismiss button for internal onboarding * Import sort --------- Co-authored-by: Eric Bailey <git@esb.lol> * add matches query to shadow * add clockwise arrow * update status design, fix dismiss, fix queries * add metrics * add more comments * Update metrics.ts * refetch both queries on PTR * fix shadow state in matches page * reduce empty space at bottom * show contact info on matches * filter out contacts without numbers at an earlier stage * Error handling ✨ * get notifs working * filter out businesses * rm log * remove TODO from learn more links * try and exclude from web bundle --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import React, {useState} from 'react'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {Dimensions} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isAndroid, isWeb} from '#/platform/detection'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {useOnboardingInternalState} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
atoms as a,
|
||||
native,
|
||||
@@ -22,6 +21,7 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons
|
||||
import {HEADER_SLOT_SIZE} from '#/components/Layout'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import {P, Text} from '#/components/Typography'
|
||||
import {IS_INTERNAL} from '#/env'
|
||||
|
||||
const ONBOARDING_COL_WIDTH = 420
|
||||
|
||||
@@ -34,20 +34,20 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
const insets = useSafeAreaInsets()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
const scrollview = React.useRef<ScrollView>(null)
|
||||
const prevActiveStep = React.useRef<string>(state.activeStep)
|
||||
const {state, dispatch} = useOnboardingInternalState()
|
||||
const scrollview = useRef<ScrollView>(null)
|
||||
const prevActiveStep = useRef<string>(state.activeStep)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (state.activeStep !== prevActiveStep.current) {
|
||||
prevActiveStep.current = state.activeStep
|
||||
scrollview.current?.scrollTo({y: 0, animated: false})
|
||||
}
|
||||
}, [state])
|
||||
|
||||
const paddingTop = gtMobile ? a.py_5xl : a.py_lg
|
||||
const dialogLabel = _(msg`Set up your account`)
|
||||
|
||||
const [headerHeight, setHeaderHeight] = useState(0)
|
||||
const [footerHeight, setFooterHeight] = useState(0)
|
||||
|
||||
return (
|
||||
@@ -59,33 +59,12 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
accessibilityLabel={dialogLabel}
|
||||
accessibilityHint={_(msg`Customizes your Bluesky experience`)}
|
||||
style={[isWeb ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
|
||||
{__DEV__ && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="negative"
|
||||
size="tiny"
|
||||
onPress={() => onboardDispatch({type: 'skip'})}
|
||||
// DEV ONLY
|
||||
label="Clear onboarding state"
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
{
|
||||
left: isWeb ? '50%' : Dimensions.get('window').width / 2 - 45,
|
||||
top: insets.top + 2,
|
||||
},
|
||||
web({transform: [{translateX: '-50%'}]}),
|
||||
]}>
|
||||
<ButtonText>[DEV] Clear</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!gtMobile && (
|
||||
{!gtMobile ? (
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[
|
||||
web(a.fixed),
|
||||
native(a.absolute),
|
||||
a.top_0,
|
||||
a.left_0,
|
||||
a.right_0,
|
||||
a.flex_row,
|
||||
@@ -93,85 +72,94 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
a.justify_center,
|
||||
a.z_20,
|
||||
a.px_xl,
|
||||
{top: paddingTop.paddingTop + insets.top - 1},
|
||||
]}>
|
||||
{paddingTop: (web(tokens.space.lg) ?? 0) + insets.top},
|
||||
native([t.atoms.bg, a.pb_xs, {minHeight: 48}]),
|
||||
web(a.pointer_events_box_none),
|
||||
]}
|
||||
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
style={[
|
||||
a.w_full,
|
||||
a.align_start,
|
||||
a.align_center,
|
||||
a.flex_row,
|
||||
a.justify_between,
|
||||
{maxWidth: ONBOARDING_COL_WIDTH},
|
||||
web({maxWidth: ONBOARDING_COL_WIDTH}),
|
||||
web(a.pointer_events_box_none),
|
||||
]}>
|
||||
{state.hasPrev ? (
|
||||
<HeaderSlot>
|
||||
{state.canGoBack && (
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
size="small"
|
||||
label={_(msg`Go back to previous step`)}
|
||||
onPress={() => dispatch({type: 'prev'})}>
|
||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||
</Button>
|
||||
)}
|
||||
</HeaderSlot>
|
||||
|
||||
{IS_INTERNAL && (
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
size="small"
|
||||
label={_(msg`Go back to previous step`)}
|
||||
onPress={() => dispatch({type: 'prev'})}
|
||||
style={[a.bg_transparent]}>
|
||||
<ButtonIcon icon={ArrowLeft} size="lg" />
|
||||
color="negative"
|
||||
size="tiny"
|
||||
onPress={() => onboardDispatch({type: 'skip'})}
|
||||
// DEV ONLY
|
||||
label="Clear onboarding state">
|
||||
<ButtonText>[DEV] Clear</ButtonText>
|
||||
</Button>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
|
||||
<OnboardingHeaderSlot.Outlet />
|
||||
<HeaderSlot>
|
||||
<OnboardingHeaderSlot.Outlet />
|
||||
</HeaderSlot>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{IS_INTERNAL && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.align_center,
|
||||
a.z_10,
|
||||
{top: 0, left: 0, right: 0},
|
||||
]}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="negative"
|
||||
size="tiny"
|
||||
onPress={() => onboardDispatch({type: 'skip'})}
|
||||
// DEV ONLY
|
||||
label="Clear onboarding state">
|
||||
<ButtonText>[DEV] Clear</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
ref={scrollview}
|
||||
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
|
||||
contentContainerStyle={{borderWidth: 0, minHeight: '100%'}}
|
||||
style={[a.h_full, a.w_full]}
|
||||
contentContainerStyle={{
|
||||
borderWidth: 0,
|
||||
minHeight: '100%',
|
||||
paddingTop: gtMobile ? 40 : headerHeight,
|
||||
paddingBottom: footerHeight,
|
||||
}}
|
||||
showsVerticalScrollIndicator={!isAndroid}
|
||||
scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}}
|
||||
// @ts-expect-error web only --prf
|
||||
dataSet={{'stable-gutters': 1}}>
|
||||
dataSet={{'stable-gutters': 1}}
|
||||
centerContent={gtMobile}>
|
||||
<View
|
||||
style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
|
||||
<View style={[a.flex_1, {maxWidth: ONBOARDING_COL_WIDTH}]}>
|
||||
<View style={[a.w_full, a.align_center, paddingTop]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
a.w_full,
|
||||
a.align_center,
|
||||
{height: HEADER_SLOT_SIZE, maxWidth: '60%'},
|
||||
]}>
|
||||
{Array(state.totalSteps)
|
||||
.fill(0)
|
||||
.map((__, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.pt_xs,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_50,
|
||||
{
|
||||
backgroundColor:
|
||||
i + 1 <= state.activeStepIndex
|
||||
? t.palette.primary_500
|
||||
: t.palette.contrast_100,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.w_full, a.h_full, a.mb_5xl, a.pt_md]}>
|
||||
{children}
|
||||
</View>
|
||||
|
||||
<View style={{height: 100 + footerHeight}} />
|
||||
<View style={[a.flex_1, web({maxWidth: ONBOARDING_COL_WIDTH})]}>
|
||||
<View style={[a.w_full, a.py_md]}>{children}</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -200,7 +188,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
gtMobile && [a.flex_row, a.justify_between, a.align_center],
|
||||
]}>
|
||||
{gtMobile &&
|
||||
(state.hasPrev ? (
|
||||
(state.canGoBack ? (
|
||||
<Button
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
color="secondary"
|
||||
@@ -221,22 +209,46 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function TitleText({
|
||||
function HeaderSlot({children}: {children?: React.ReactNode}) {
|
||||
return (
|
||||
<View style={[{minHeight: HEADER_SLOT_SIZE, minWidth: HEADER_SLOT_SIZE}]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function OnboardingPosition() {
|
||||
const {state} = useOnboardingInternalState()
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Text style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Step {state.activeStepIndex + 1} of {state.totalSteps}
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function OnboardingTitleText({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<TextStyleProp>) {
|
||||
return (
|
||||
<Text
|
||||
style={[a.pb_sm, a.text_4xl, a.font_semi_bold, a.leading_tight, style]}>
|
||||
<Text style={[a.text_3xl, a.font_bold, a.leading_snug, style]}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function DescriptionText({
|
||||
export function OnboardingDescriptionText({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<TextStyleProp>) {
|
||||
const t = useTheme()
|
||||
return <P style={[t.atoms.text_contrast_medium, style]}>{children}</P>
|
||||
return (
|
||||
<P style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium, style]}>
|
||||
{children}
|
||||
</P>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user