Skip to content

Commit

Permalink
chore(IT Wallet): [SIW-1868] Add full screen skeumorphic card modal (#…
Browse files Browse the repository at this point in the history
…6447)

## Short description
This PR introduces a feature that allows users to view a credential's
skeuomorphic card in full-screen mode. This mode can be activated by
tapping the card on the credential details screen.

## List of changes proposed in this pull request
- Added `useScaleAnimation` hook (to be removed once this PR is merged:
pagopa/io-app-design-system#358)
- Added `FlipGestureDetector` component, which handles swipe gestures to
flip the skeumorphic cards in the credential details screen
- Updated `FlippableCard` component in order to handle an optional
`orientation` prop, which changes the axis type for the flip animation
- Updated `ItwSkeumorphicCard` component in roder to handle an optional
`orientation` prop, which rotates the card and adjust its scale to fit
the screen correctly.
- Refactored `getCredentialStatus` and `itwCredentialStatusSelector`:
- Extracted logic from `itwCredentialStatusSelector` and moved it to
`getCredentialStatusObject`, `getCredentialStatusObject` can be freely
used without relying to the redux store and
`itwCredentialStatusSelector` for the memoized value
- Moved `getCredentialStatus` and `getCredentialStatusObject` to
`itwCredentialStatusUtils.ts` file
- Added `ItwPresentationCredentialCardModal` in the IT Wallet navigation
stack
  - Updated IT Wallet playgrounds to test the new feature

## How to test
1. Navigate to the credential details screen or the IT Wallet playground
in the settings screen.
2. Tap on a skeuomorphic credential card to view it in full-screen mode.
3. Verify the following:
    - The card is rendered correctly in full-screen mode.
- The screen brightness gently increases when entering full-screen mode.
- The screen brightness reverts to its original value upon exiting
full-screen mode.

## Preview


https://github.com/user-attachments/assets/66743a6f-ea96-4de3-a150-f88ccfa30eea
  • Loading branch information
mastro993 authored Nov 25, 2024
1 parent 09d9ed9 commit 102c584
Show file tree
Hide file tree
Showing 30 changed files with 967 additions and 599 deletions.
56 changes: 56 additions & 0 deletions ts/components/ui/utils/hooks/useScaleAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Remove this once this is merged: https://github.com/pagopa/io-app-design-system/pull/358
*/
import { IOSpringValues } from "@pagopa/io-app-design-system";
import { GestureResponderEvent, ViewStyle } from "react-native";
import {
AnimatedStyle,
Extrapolation,
interpolate,
SharedValue,
useAnimatedStyle
} from "react-native-reanimated";
import { useSpringPressProgressValue } from "./useSpringPressProgressValue";

export const IOScaleEffect = {
// Slight scale effect
slight: 0.99,
// Medium scale effect
medium: 0.97,
// Exaggerated scale effect
exaggerated: 0.95
};

export type IOScaleEffect = keyof typeof IOScaleEffect;

export const useScaleAnimation = (
magnitude: IOScaleEffect = "slight"
): {
progress: SharedValue<number>;
onPressIn: (event: GestureResponderEvent) => void;
onPressOut: (event: GestureResponderEvent) => void;
scaleAnimatedStyle: AnimatedStyle<ViewStyle>;
} => {
const { progress, onPressIn, onPressOut } = useSpringPressProgressValue(
IOSpringValues.button
);

// Scaling transformation applied when the button is pressed
const animationScaleValue = IOScaleEffect[magnitude];

const scaleAnimatedStyle = useAnimatedStyle(() => {
// Scale down button slightly when pressed
const scale = interpolate(
progress.value,
[0, 1],
[1, animationScaleValue],
Extrapolation.CLAMP
);

return {
transform: [{ scale }]
};
});

return { progress, onPressIn, onPressOut, scaleAnimatedStyle };
};
2 changes: 2 additions & 0 deletions ts/features/design-system/core/DSCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ const ItwCards = () => (
<DSComponentViewerBox name="Valid">
<ItwSkeumorphicCardPreview
credential={ItwStoredCredentialsMocks.mdl}
status="valid"
/>
</DSComponentViewerBox>
</VStack>
Expand Down Expand Up @@ -471,6 +472,7 @@ const ItwCards = () => (
<DSComponentViewerBox name="Valid">
<ItwSkeumorphicCardPreview
credential={ItwStoredCredentialsMocks.dc}
status="valid"
/>
</DSComponentViewerBox>
</VStack>
Expand Down
4 changes: 4 additions & 0 deletions ts/features/itwallet/__mocks__/dc.json
Original file line number Diff line number Diff line change
Expand Up @@ -722,5 +722,9 @@
"exp": 1727857781,
"iat": 1727771381
}
},
"jwt": {
"expiration": "2030-11-22T14:09:41Z",
"issuedAt": "2024-11-13T14:09:41Z"
}
}
6 changes: 5 additions & 1 deletion ts/features/itwallet/__mocks__/eid.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,5 +335,9 @@
"evidence"
]
},
"credentialType": "PersonIdentificationData"
"credentialType": "PersonIdentificationData",
"jwt": {
"expiration": "2030-11-22T14:09:41Z",
"issuedAt": "2024-11-13T14:09:41Z"
}
}
8 changes: 6 additions & 2 deletions ts/features/itwallet/__mocks__/mdl.json
Original file line number Diff line number Diff line change
Expand Up @@ -758,5 +758,9 @@
]
}
},
"keyTag": "de68e737-b264-4f5d-affb-65f7bbc5c397"
}
"keyTag": "de68e737-b264-4f5d-affb-65f7bbc5c397",
"jwt": {
"expiration": "2030-11-22T14:09:41Z",
"issuedAt": "2024-11-13T14:09:41Z"
}
}
6 changes: 5 additions & 1 deletion ts/features/itwallet/__mocks__/ts.json
Original file line number Diff line number Diff line change
Expand Up @@ -1206,5 +1206,9 @@
"document_number_team",
"evidence"
]
},
"jwt": {
"expiration": "2030-11-22T14:09:41Z",
"issuedAt": "2024-11-13T14:09:41Z"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react";
import {
Directions,
Gesture,
GestureDetector
} from "react-native-gesture-handler";
import { runOnJS } from "react-native-reanimated";

const directions = {
updown: Directions.UP + Directions.DOWN,
leftright: Directions.LEFT + Directions.RIGHT
};

type FlipsGestureDetectorProps = {
isFlipped: boolean;
setIsFlipped: (isFlipped: boolean) => void;
children: React.ReactNode;
direction?: keyof typeof directions;
};

/**
* This component wraps the children in a GestureDetector that flips the card when the user flicks left or right.
*/
export const FlipGestureDetector = ({
isFlipped,
setIsFlipped,
children,
direction = "leftright"
}: FlipsGestureDetectorProps) => {
const flipGesture = Gesture.Fling()
.direction(directions[direction])
.onEnd(() => runOnJS(setIsFlipped)(!isFlipped));

return <GestureDetector gesture={flipGesture}>{children}</GestureDetector>;
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import React from "react";
import {
AccessibilityProps,
StyleProp,
StyleSheet,
View,
ViewStyle
} from "react-native";
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
import Animated, {
interpolate,
useAnimatedStyle,
Expand All @@ -21,7 +15,7 @@ export type FlippableCardProps = {
duration?: number;
isFlipped?: boolean;
containerStyle?: StyleProp<ViewStyle>;
} & AccessibilityProps;
};

/**
* Renders a component which can be flipped to show both of its sides with an animation.
Expand All @@ -31,8 +25,7 @@ const FlippableCard = ({
BackComponent,
containerStyle,
duration = DEFAULT_DURATION,
isFlipped: _isFlipped,
...accessibilityProps
isFlipped: _isFlipped
}: FlippableCardProps) => {
const isFlipped = useSharedValue(_isFlipped);

Expand All @@ -48,7 +41,7 @@ const FlippableCard = ({
return {
transform: [{ rotateY: rotateValue }]
};
});
}, []);

const flippedCardAnimatedStyle = useAnimatedStyle(() => {
const spinValue = interpolate(Number(isFlipped.value), [0, 1], [180, 360]);
Expand All @@ -57,10 +50,10 @@ const FlippableCard = ({
return {
transform: [{ rotateY: rotateValue }]
};
});
}, []);

return (
<View style={containerStyle} {...accessibilityProps}>
<View style={containerStyle}>
<Animated.View
style={[styles.card, styles.front, regularCardAnimatedStyle]}
>
Expand Down
107 changes: 66 additions & 41 deletions ts/features/itwallet/common/components/ItwSkeumorphicCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Tag } from "@pagopa/io-app-design-system";
import React, { ReactNode, useMemo } from "react";
import {
AccessibilityProps,
Pressable,
StyleProp,
StyleSheet,
View,
ViewStyle
} from "react-native";
import Animated from "react-native-reanimated";
import { useScaleAnimation } from "../../../../../components/ui/utils/hooks/useScaleAnimation";
import I18n from "../../../../../i18n";
import { useIOSelector } from "../../../../../store/hooks";
import { itwCredentialStatusSelector } from "../../../credentials/store/selectors";
import { accessibilityLabelByStatus } from "../../utils/itwAccessibilityUtils";
import {
borderColorByStatus,
Expand All @@ -25,48 +26,19 @@ import { CardBackground } from "./CardBackground";
import { CardData } from "./CardData";
import { FlippableCard } from "./FlippableCard";

type CardSideBaseProps = {
status: ItwCredentialStatus;
children: ReactNode;
};

const CardSideBase = ({ status, children }: CardSideBaseProps) => {
const statusTagProps = tagPropsByStatus[status];
const borderColor = borderColorByStatus[status];

const dynamicStyle: StyleProp<ViewStyle> = {
borderColor,
backgroundColor: validCredentialStatuses.includes(status)
? undefined
: "rgba(255,255,255,0.7)"
};

return (
<View>
{statusTagProps && (
<View style={styles.tag}>
<Tag {...statusTagProps} />
</View>
)}
<View style={[styles.faded, dynamicStyle]} />
{children}
</View>
);
};

export type ItwSkeumorphicCardProps = {
credential: StoredCredential;
status: ItwCredentialStatus;
isFlipped?: boolean;
onPress?: () => void;
};

const ItwSkeumorphicCard = ({
credential,
isFlipped = false
status,
isFlipped = false,
onPress
}: ItwSkeumorphicCardProps) => {
const { status = "valid" } = useIOSelector(state =>
itwCredentialStatusSelector(state, credential.credentialType)
);

const FrontSide = useMemo(
() => (
<CardSideBase status={status}>
Expand Down Expand Up @@ -104,26 +76,79 @@ const ItwSkeumorphicCard = ({
? "features.itWallet.presentation.credentialDetails.card.back"
: "features.itWallet.presentation.credentialDetails.card.front"
)}`,
accessibilityRole: "image",
accessibilityValue: { text: accessibilityLabelByStatus[status] }
} as AccessibilityProps),
[credential.credentialType, isFlipped, status]
);

return (
const card = (
<FlippableCard
containerStyle={styles.card}
containerStyle={[styles.card]}
FrontComponent={FrontSide}
BackComponent={BackSide}
isFlipped={isFlipped}
{...accessibilityProps}
/>
);

const { onPressIn, onPressOut, scaleAnimatedStyle } = useScaleAnimation();

if (onPress) {
return (
<Pressable
onPress={onPress}
{...accessibilityProps}
accessibilityRole="button"
onPressIn={onPressIn}
onPressOut={onPressOut}
>
<Animated.View style={scaleAnimatedStyle}>{card}</Animated.View>
</Pressable>
);
}

return (
<View {...accessibilityProps} accessibilityRole="image">
{card}
</View>
);
};

type CardSideBaseProps = {
status: ItwCredentialStatus;
children: ReactNode;
};

const CardSideBase = ({ status, children }: CardSideBaseProps) => {
const statusTagProps = tagPropsByStatus[status];
const borderColor = borderColorByStatus[status];

const dynamicStyle: StyleProp<ViewStyle> = {
borderColor,
backgroundColor: validCredentialStatuses.includes(status)
? undefined
: "rgba(255,255,255,0.7)"
};

return (
<View>
{statusTagProps && (
<View style={styles.tag}>
<Tag {...statusTagProps} />
</View>
)}
<View style={[styles.faded, dynamicStyle]} />
{children}
</View>
);
};

// Magic number for the aspect ratio of the card
// extracted from the design
export const SKEUMORPHIC_CARD_ASPECT_RATIO = 16 / 10.09;

const styles = StyleSheet.create({
card: {
aspectRatio: 16 / 10.09
aspectRatio: SKEUMORPHIC_CARD_ASPECT_RATIO
},
tag: {
position: "absolute",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/**
* This type represents the side of the card that should be shown
*/
export type CardSide = "front" | "back";
Loading

0 comments on commit 102c584

Please sign in to comment.