0

我正在与一个使用 Stepper 组件来控制结帐过程页面的客户合作。流程是购物车 --> 结帐 --> 确认付款 --> 订单摘要。每个页面都有一个带有数字索引的“步骤”,从 0 到 3。在 Checkout 组件中,有一个 CheckoutLayout 组件包装其他屏幕并控制 handleSubmit 和后退按钮功能。后退按钮本身可以正常工作,但我在结帐布局中添加了一个 panResponder,它在向左滑动时触发回调。出于某种原因,滑动触发了重新渲染,它将活动步骤重置为 0(单击自定义后退按钮时不会发生这种情况)。一旦在 panResponder 中触发回调,有谁知道如何在用户滑动时停止手势?

结帐组件:

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Animated, StyleSheet } from 'react-native';

import {
  Button,
  Footer,
  Stepper,
  useDeviceType,
  useStyles,
} from '@nutrien/bonsai-core';
import { usePlatformNavigation } from '@nutrien/hub-routing';
import { EcommFooter } from '@nutrien/hub-shared';
import { OrderConfirmationProvider } from '../../../contexts/OrderConfirmationContext';
import useTranslation from '../../../hooks/usePackageTranslation';
import CheckoutConfirmation from '../../organisms-br/CheckoutConfirmation';
import CheckoutForm from '../../organisms-br/CheckoutForm';
import CheckoutLayout from '../../organisms-br/CheckoutLayout';
import CheckoutPayment from '../../organisms-br/CheckoutPayment';

export interface CheckoutPageRef {
  submit: () => void;
}

export const Checkout = (): JSX.Element => {
  const TABLET_WIDTH = 466;

  const { push, goBack } = usePlatformNavigation();
  const { t } = useTranslation();
  const { isHandset, isTablet, isDesktop } = useDeviceType();
  const [showOverlay, setOverlay] = useState(false);

  const [isSubmitEnable, enableSubmit] = useState(false);
  const [isLoading, setLoading] = useState(false);

  const checkoutFormRef = useRef<CheckoutPageRef>();
  const paymentFormRef = useRef<CheckoutPageRef>();
  const confirmationFormRef = useRef<CheckoutPageRef>();
  const overlayOpacity = useRef(new Animated.Value(0)).current;

  const styles = useStyles((theme) => ({
    container: {
      flex: 1,
      backgroundColor: theme.colors.grey0,
      flexGrow: 1,
    },
    footer: {
      justifyContent: isDesktop ? 'flex-end' : 'center',
      backgroundColor: theme.colors.white,
      paddingBottom: theme.spacing(1),
      borderStyle: 'solid',
      borderTopWidth: 1,
      borderColor: theme.colors.grey[200],
    },
    button: {
      width: isTablet ? TABLET_WIDTH : 'auto',
    },
    footerWrapper: {
      marginHorizontal: isHandset ? theme.spacing(5) : theme.spacing(6),
      marginBottom: isDesktop ? theme.spacing(3) : 0,
    },
  }));

  const stepLabels = [
    t('checkout.steps.checkout'),
    t('checkout.steps.payment'),
    t('checkout.steps.confirmation'),
  ];

  const pageTitle = [
    {
      title: t('checkout.addressTitle'),
      submitLabel: t('checkout.goToPayment'),
    },
    {
      title: t('checkout.payment'),
      submitLabel: t('checkout.confirmOrder'),
    },
    {
      title: t('checkout.success.title'),
      submitLabel: t('checkout.goToMyOrders'),
    },
  ];

  useEffect(() => {
    Animated.timing(overlayOpacity, {
      toValue: showOverlay ? 1 : 0,
      duration: 450,
      useNativeDriver: true,
    }).start();
  }, [overlayOpacity, showOverlay]);

  const [activeStep, setActiveStep] = useState(0);

  const handleSubmit = () => {
    if (activeStep === 0) {
      checkoutFormRef.current?.submit();
    }
    if (activeStep === 1) {
      paymentFormRef.current?.submit();
    }
    if (activeStep === 2) {
      confirmationFormRef.current?.submit();
    }
  };
  const handleBack = () => {
    if (activeStep > 0) {
      setActiveStep(activeStep - 1);
    } else {
      goBack();
    }
  };
  const onSwipeBack = useCallback(() => {
    console.log('how many times does this get hit?');
    console.log('activeStep: ', activeStep);
    setActiveStep((prev) => {
      console.log('prev: ', prev);
      if (prev === 0) {
        goBack();
        return 0;
      } else {
        return prev - 1;
      }
    });
    // if (activeStep === 0) goBack();
  }, [activeStep, goBack]);
  return (
    <>
      <CheckoutLayout
        title={pageTitle[activeStep].title}
        showBackButton={activeStep !== 3}
        onBackButtonPress={handleBack}
        onSwipeBack={onSwipeBack}
      >
        <Stepper
          activeStep={activeStep}
          labels={stepLabels}
          orientation="horizontal"
        >
          <CheckoutForm
            ref={checkoutFormRef}
            handleSubmit={() => setActiveStep(1)}
            onLoading={setLoading}
            onEnableSubmit={enableSubmit}
            onOverlay={setOverlay}
          />
          <CheckoutPayment
            ref={paymentFormRef}
            handleSubmit={() => setActiveStep(2)}
            onLoading={setLoading}
            onEnableSubmit={enableSubmit}
          />
          <CheckoutConfirmation
            ref={confirmationFormRef}
            handleSubmit={() => push('accounts.orders')}
            onEnableSubmit={enableSubmit}
          />
        </Stepper>
        <EcommFooter simplified style={styles.footerWrapper} />
      </CheckoutLayout>
      <Footer style={styles.footer}>
        <Button
          large
          title={pageTitle[activeStep].submitLabel}
          disabled={!isSubmitEnable || isLoading}
          onPress={handleSubmit}
          fullWidth={isHandset}
          style={styles.button}
        />
      </Footer>
      <Animated.View
        pointerEvents="none"
        style={{
          ...StyleSheet.absoluteFillObject,
          backgroundColor: '#000',
          opacity: overlayOpacity.interpolate({
            inputRange: [0, 1],
            outputRange: [0, 0.2],
          }),
        }}
      />
    </>
  );
};

export default (): JSX.Element => (
  <OrderConfirmationProvider>
    <Checkout />
  </OrderConfirmationProvider>
);

CheckoutLayout 组件:

import {
  Dimensions,
  ScrollView,
  TextStyle,
  View,
  ViewStyle,
} from 'react-native';

import {
  ActivityIndicator,
  Button,
  Text,
  useDeviceType,
  useStyles,
  useTheme,
} from '@nutrien/bonsai-core';
import {
  useSelectedRetailAccount,
  useSwipeCallback,
} from '@nutrien/hub-shared';
import useTranslation from '../../../hooks/usePackageTranslation';

interface Styles {
  container: ViewStyle;
  titleStyle: TextStyle;
  scrollView: ViewStyle;
  header: ViewStyle;
  headerContainer: ViewStyle;
  backButton: ViewStyle;
  backButtonContainer: ViewStyle;
  activityIndicator: ViewStyle;
}

interface CheckoutLayoutProps {
  title: string;
  showBackButton: boolean;
  onBackButtonPress: () => void;
  onSwipeBack: () => void;
}

const CheckoutLayout: React.FC<CheckoutLayoutProps> = ({
  children,
  title,
  showBackButton,
  onBackButtonPress,
  onSwipeBack,
}) => {
  const TABLE_WIDTH_FOR_TABLETS = 466;

  const { height } = Dimensions.get('window');
  const { t } = useTranslation();
  const { isHandset, isTablet, isDesktop } = useDeviceType();
  const theme = useTheme();
  const { selectedAccount } = useSelectedRetailAccount();
  const swipeCallback = useSwipeCallback({
    callback: onSwipeBack,
  }).panHandlers;
  const styles = useStyles<Styles>((theme) => ({
    container: {
      flex: 1,
      backgroundColor: theme.colors.grey0,
      flexGrow: 1,
    },
    scrollView: {
      height: height / 2,
    },
    titleStyle: {
      marginBottom: 0,
    },
    header: {
      position: 'relative',
      backgroundColor: theme.colors.grey1,
      padding: isHandset ? theme.spacing(2) : theme.spacing(3),
      paddingTop: isHandset
        ? theme.spacing(7)
        : isTablet
        ? theme.spacing(8)
        : theme.spacing(9),
    },
    headerContainer: {
      width: '100%',
      maxWidth: isTablet ? TABLE_WIDTH_FOR_TABLETS : 'auto',
      alignSelf: 'center',
    },
    backButton: {
      paddingHorizontal: 0,
    },
    backButtonContainer: {
      position: 'absolute',
      top: isDesktop ? theme.spacing(3) : theme.spacing(2),
      left: isHandset ? theme.spacing(1) : theme.spacing(2),
      paddingHorizontal: isHandset ? 0 : theme.spacing(0.5),
    },
    activityIndicator: {
      alignSelf: 'flex-start',
    },
  }));
  return (
    <View style={styles.container} {...swipeCallback}>
      <ScrollView style={styles.scrollView}>
        <View style={styles.header}>
          {showBackButton && (
            <Button
              onPress={() => onBackButtonPress()}
              icon={{
                color: theme.colors.primary,
                name: 'chevron-left',
              }}
              containerStyle={styles.backButtonContainer}
              buttonStyle={styles.backButton}
              title={t('navigation.back')}
              type="clear"
            />
          )}
          <View style={styles.headerContainer}>
            <Text h1 style={styles.titleStyle}>
              {title}
            </Text>
            {selectedAccount ? (
              <Text style={styles.titleStyle}>
                {selectedAccount?.name || ''}
              </Text>
            ) : (
              <ActivityIndicator style={styles.activityIndicator} />
            )}
          </View>
        </View>
        {children}
      </ScrollView>
    </View>
  );
};

export default CheckoutLayout;

UseSwipeCallback 钩子:

import { PanResponder, Platform } from 'react-native';

const useSwipeCallback = ({
  callback,
  direction,
}: {
  callback: () => void;
  direction?: 'forward' | 'back';
}) => {
  return useRef(
    PanResponder.create({
      /** onPanResponderRelease does not work on android, so using onMoveShouldSetPanResponder, instead */
      onMoveShouldSetPanResponder: (evt, gestureState) => {
        const swipeDirection =
          direction === 'forward' ? gestureState.vx < 0 : gestureState.vx > 0;
        const gestureDistanceX = Math.floor(gestureState.dx);
        const gestureDistanceYScaled = Math.abs(gestureState.dy) * 2;
        const isUniSwipeHorizontal =
          direction === 'forward'
            ? gestureDistanceX < -1 * gestureDistanceYScaled
            : gestureDistanceX > gestureDistanceYScaled;

        if (Platform.OS !== 'web' && swipeDirection && isUniSwipeHorizontal) {
          callback();
        }
        return false;
      },
      // onPanResponderTerminationRequest: () => true,
    })
  ).current;
};
export default useSwipeCallback;
4

0 回答 0