teamdigitale/italia-app

View on GitHub
ts/features/services/details/components/ServiceDetailsScreenComponent.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
import {
  ButtonLink,
  ButtonOutline,
  ButtonSolid,
  IOColors,
  IOSpacer,
  IOSpacingScale,
  IOStyles,
  IOVisualCostants,
  VSpacer,
  hexToRgba
} from "@pagopa/io-app-design-system";
import React, { ComponentProps, useCallback, useMemo, useState } from "react";
import {
  LayoutChangeEvent,
  LayoutRectangle,
  StyleSheet,
  View
} from "react-native";
import { easeGradient } from "react-native-easing-gradient";
import LinearGradient from "react-native-linear-gradient";
import Animated, {
  Easing,
  useAnimatedScrollHandler,
  useAnimatedStyle,
  useSharedValue,
  withTiming
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useHeaderSecondLevel } from "../../../../hooks/useHeaderSecondLevel";
import { ServiceSpecialAction } from "./ServiceSpecialAction";

const scrollTriggerOffsetValue: number = 88;

const HEADER_BG_COLOR: IOColors = "white";

const styles = StyleSheet.create({
  scrollContentContainer: {
    flexGrow: 1
  },
  gradientBottomActions: {
    width: "100%",
    position: "absolute",
    bottom: 0,
    justifyContent: "flex-end"
  },
  buttonContainer: {
    paddingHorizontal: IOVisualCostants.appMarginDefault,
    width: "100%",
    flexShrink: 0
  },
  gradientContainer: {
    ...StyleSheet.absoluteFillObject
  }
});

const { colors, locations } = easeGradient({
  colorStops: {
    0: { color: hexToRgba(IOColors[HEADER_BG_COLOR], 0) },
    1: { color: IOColors[HEADER_BG_COLOR] }
  },
  easing: Easing.ease,
  extraColorStopsPerTransition: 20
});

/* Extended gradient area above the actions */
const gradientSafeAreaHeight: IOSpacingScale = 96;
/* End content margin before the actions */
const contentEndMargin: IOSpacingScale = 32;
/* Margin between ButtonSolid and ButtonOutline */
const spaceBetweenActions: IOSpacer = 8;
/* Margin between ButtonSolid and ButtonLink */
const spaceBetweenActionAndLink: IOSpacer = 16;

export type ServiceActionsProps =
  | {
      type: "SingleCta";
      primaryActionProps: Omit<ComponentProps<typeof ButtonSolid>, "fullWidth">;
      secondaryActionProps?: never;
      tertiaryActionProps?: never;
    }
  | {
      type: "SingleCtaCustomFlow";
      primaryActionProps: ComponentProps<typeof ServiceSpecialAction>;
      secondaryActionProps?: never;
      tertiaryActionProps?: never;
    }
  | {
      type: "SingleCtaWithCustomFlow";
      primaryActionProps: ComponentProps<typeof ServiceSpecialAction>;
      secondaryActionProps: ComponentProps<typeof ButtonLink>;
      tertiaryActionProps?: never;
    }
  | {
      type: "TwoCtas";
      primaryActionProps: Omit<ComponentProps<typeof ButtonSolid>, "fullWidth">;
      secondaryActionProps: ComponentProps<typeof ButtonLink>;
      tertiaryActionProps?: never;
    }
  | {
      type: "TwoCtasWithCustomFlow";
      primaryActionProps: ComponentProps<typeof ServiceSpecialAction>;
      secondaryActionProps: Omit<
        ComponentProps<typeof ButtonOutline>,
        "fullWidth"
      >;
      tertiaryActionProps: ComponentProps<typeof ButtonLink>;
    };

type ServiceDetailsScreenComponentProps = {
  children: React.ReactNode;
  actionsProps?: ServiceActionsProps;
  debugMode?: boolean;
  title?: string;
};

export const ServiceDetailsScreenComponent = ({
  children,
  actionsProps,
  debugMode = false,
  title = ""
}: ServiceDetailsScreenComponentProps) => {
  const safeAreaInsets = useSafeAreaInsets();

  const gradientOpacity = useSharedValue(1);
  const scrollTranslationY = useSharedValue(0);

  const [actionBlockHeight, setActionBlockHeight] =
    useState<LayoutRectangle["height"]>(0);

  const getActionBlockHeight = (event: LayoutChangeEvent) => {
    setActionBlockHeight(event.nativeEvent.layout.height);
  };

  const bottomMargin: number = useMemo(
    () =>
      safeAreaInsets.bottom === 0
        ? IOVisualCostants.appMarginDefault
        : safeAreaInsets.bottom,
    [safeAreaInsets]
  );

  const safeBackgroundBlockHeight: number = useMemo(
    () => (bottomMargin + actionBlockHeight) * 0.85,
    [actionBlockHeight, bottomMargin]
  );

  /* Total height of "Actions + Gradient" area */
  const gradientAreaHeight: number = useMemo(
    () => bottomMargin + actionBlockHeight + gradientSafeAreaHeight,
    [actionBlockHeight, bottomMargin]
  );

  /* Height of the safe bottom area, applied to the ScrollView:
     Actions + Content end margin */
  const safeBottomAreaHeight: number = useMemo(
    () => bottomMargin + actionBlockHeight + contentEndMargin,
    [actionBlockHeight, bottomMargin]
  );

  useHeaderSecondLevel({
    title,
    supportRequest: true,
    transparent: true,
    scrollValues: {
      triggerOffset: scrollTriggerOffsetValue,
      contentOffsetY: scrollTranslationY
    }
  });

  const footerGradientOpacityTransition = useAnimatedStyle(() => ({
    opacity: withTiming(gradientOpacity.value, {
      duration: 200,
      easing: Easing.ease
    })
  }));

  const scrollHandler = useAnimatedScrollHandler(({ contentOffset }) => {
    // eslint-disable-next-line functional/immutable-data
    scrollTranslationY.value = contentOffset.y;
  });

  const renderFooter = useCallback((props: ServiceActionsProps) => {
    switch (props.type) {
      case "SingleCta":
        return <ButtonSolid fullWidth {...props.primaryActionProps} />;
      case "SingleCtaCustomFlow":
        return <ServiceSpecialAction {...props.primaryActionProps} />;
      case "SingleCtaWithCustomFlow":
        return (
          <>
            <ServiceSpecialAction {...props.primaryActionProps} />
            <VSpacer size={spaceBetweenActionAndLink} />
            <View style={IOStyles.selfCenter}>
              <ButtonLink {...props.secondaryActionProps} />
            </View>
          </>
        );
      case "TwoCtas":
        return (
          <>
            <ButtonSolid fullWidth {...props.primaryActionProps} />
            <VSpacer size={spaceBetweenActionAndLink} />
            <View style={IOStyles.selfCenter}>
              <ButtonLink {...props.secondaryActionProps} />
            </View>
          </>
        );
      case "TwoCtasWithCustomFlow":
        return (
          <>
            <ServiceSpecialAction {...props.primaryActionProps} />
            <VSpacer size={spaceBetweenActions} />
            <ButtonOutline fullWidth {...props.secondaryActionProps} />
            <VSpacer size={spaceBetweenActionAndLink} />
            <View style={IOStyles.selfCenter}>
              <ButtonLink {...props.tertiaryActionProps} />
            </View>
          </>
        );
    }
  }, []);

  return (
    <>
      <Animated.ScrollView
        contentContainerStyle={[
          styles.scrollContentContainer,
          {
            paddingBottom: actionsProps
              ? safeBottomAreaHeight
              : bottomMargin + contentEndMargin
          }
        ]}
        onScroll={scrollHandler}
        scrollEventThrottle={16}
        snapToOffsets={[0, scrollTriggerOffsetValue]}
        snapToEnd={false}
        decelerationRate="normal"
      >
        {children}
      </Animated.ScrollView>
      {actionsProps && (
        <View
          style={[
            styles.gradientBottomActions,
            {
              height: gradientAreaHeight,
              paddingBottom: bottomMargin
            }
          ]}
          pointerEvents="box-none"
        >
          <Animated.View
            style={[
              styles.gradientContainer,
              debugMode && {
                borderTopColor: IOColors["error-500"],
                borderTopWidth: 1,
                backgroundColor: hexToRgba(IOColors["error-500"], 0.5)
              },
              footerGradientOpacityTransition
            ]}
            pointerEvents="none"
          >
            <LinearGradient
              style={{
                height: gradientAreaHeight - safeBackgroundBlockHeight
              }}
              locations={locations}
              colors={colors}
            />
            <View
              style={{
                bottom: 0,
                height: safeBackgroundBlockHeight,
                backgroundColor: HEADER_BG_COLOR
              }}
            />
          </Animated.View>
          <View
            style={styles.buttonContainer}
            pointerEvents="box-none"
            onLayout={getActionBlockHeight}
          >
            {renderFooter(actionsProps)}
          </View>
        </View>
      )}
    </>
  );
};