Skip to content

Commit

Permalink
chore(Messaggi a valore legale): [IAMVL-20] Display legal message met…
Browse files Browse the repository at this point in the history
…adata (#3596)

* wip

* add legalmessageheader

* simplify CtaBar and HeaderDueDateBar

* fix

* fix

* rework CTABar

* refactoring

* add placeholders

* fix tests

* add comments

* addd tests

* align images to center

* refactoring

* fix mock due date

* delete style

* fix merge

* add react-native-render-html and first MvlBody

* refactoring

* renaming

* change mock text

* graphical refinement

* add tests

* remove comment

* add digital information representation

* add comments

* finalize mvlAttachments style

* add comments

* add confirmation bottomsheet

* clean

* fix test

* add todo

* add todo

* refactoring

* add tests

* add rawaccordion

* add Animation

* Add metadata description

* render links

* change arrow rotation

* refactoring

* refactoring RawAccordion

* add IOAccordion and showroom update

* update snapshot

* change message title to mvl subject

* fix

* update mock

* add tests

* add mvlmetadata test

* fix

* sender sender link only if there are others cc

* add tests and refactoring

* add missing data for MvlMetadata

* Update ts/components/core/accordion/IOAccordion.tsx

* remove unnecessary data

* remove feedback

* use isAndroid

* add comment

Co-authored-by: Matteo Boschi <[email protected]>
Co-authored-by: pietro909 <[email protected]>
  • Loading branch information
3 people authored Dec 29, 2021
1 parent e98e05f commit 0dc8658
Show file tree
Hide file tree
Showing 19 changed files with 663 additions and 27 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
}
},
setupFiles: ["./jestSetup.js"],
globalSetup: "./jestGlobalSetup.js",
setupFilesAfterEnv: ["@testing-library/jest-native/extend-expect"],
collectCoverage: true
};
3 changes: 3 additions & 0 deletions jestGlobalSetup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default () => {
process.env.TZ = "UTC";
};
17 changes: 15 additions & 2 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ global:
tomorrow: "tomorrow"
dateFormats:
shortFormat: "%d/%m/%Y"
shortFormatWithTime: "%d/%m/%Y, %H.%M"
shortFormatWithTime: "%d/%m/%Y, %H:%M"
fullFormatShortMonthLiteral: "%d %b %Y"
fullFormatShortMonthLiteralWithTime: "%d %b %Y, %H:%M"
fullFormatShortMonthLiteralWithoutTime: "%d %b %Y"
Expand All @@ -115,7 +115,8 @@ global:
fullMonthLiteralWithYear: "%B %Y"
numericMonthYear: "%m/%Y"
shortNumericMonthYear: "%m/%-y"
timeFormat: "%H.%M"
timeFormat: "%H:%M"
timeFormatWithTimezone: "%H:%M:%S (%z)"
dayMonthWithTime: "%d %b, %H:%M"
dayMonthWithoutTime: "%d %b"
dayFullMonth: "%d %B"
Expand Down Expand Up @@ -2858,6 +2859,18 @@ features:
body:
plain: "plain text"
html: "html"
metadata:
title: "the message has legal value"
description: "On {{date}} at {{time}} the message \"{{subject}}\" was sent to you by \"{{sender}}\""
messageId: "Message identifier:"
links:
copy: "Copy message identifier"
recipients: "Show full list of recipients"
signature: "Show signature details"
certificates: "Show certificates"
originalMessage: "Download original message"
datiCert: "Download daticert.xml"
smime: "Download smime.p7s"
euCovidCertificate:
save:
noPermission: "It seems that the app does not have permissions to save images. Please check your device settings."
Expand Down
17 changes: 15 additions & 2 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,15 @@ global:
tomorrow: "domani"
dateFormats:
shortFormat: "%d/%m/%Y"
shortFormatWithTime: "%d/%m/%Y, %H.%M"
shortFormatWithTime: "%d/%m/%Y, %H:%M"
fullFormatShortMonthLiteral: "%d %b %Y"
fullFormatShortMonthLiteralWithTime: "%d %b %Y, %H:%M"
fullFormatShortMonthLiteralWithoutTime: "%d %b %Y"
fullFormatFullMonthLiteral: "%d %B %Y"
fullMonthLiteral: "%B"
fullMonthLiteralWithYear: "%B %Y"
timeFormat: "%H.%M"
timeFormat: "%H:%M"
timeFormatWithTimezone: "%H:%M:%S (%z)"
dayMonthWithTime: "%d %b, %H:%M"
dayMonthWithoutTime: "%d %b"
dayFullMonth: "%d %B"
Expand Down Expand Up @@ -2886,6 +2887,18 @@ features:
body:
plain: "testo semplice"
html: "html"
metadata:
title: "il messaggio ha valore legale"
description: "Il giorno {{date}} alle ore {{time}} il messaggio \"{{subject}}\" ti è stato inviato da \"{{sender}}\""
messageId: "Identificativo messaggio:"
links:
copy: "Copia identificativo messaggio"
recipients: "Mostra elenco completo dei destinatari"
signature: "Mostra dettagli della firma"
certificates: "Mostra i certificati"
originalMessage: "Scarica il messaggio originale"
datiCert: "Scarica daticert.xml"
smime: "Scarica smime.p7s"
euCovidCertificate:
save:
noPermission: "Sembra che l'app non abbia i permessi per salvare immagini. Controlla le impostazioni del tuo dispositivo."
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"start-breaking-cycle": "yarn pre-cycle && standard-version -t \"\" --prerelease rc --release-as major",
"attach": "react-native attach",
"postinstall": "patch-package && rn-nodeify --install path,buffer && chmod +x ./bin/add-ios-source-maps.sh && ./bin/add-ios-source-maps.sh && chmod +x ./bin/add-ios-env-config.sh && ./bin/add-ios-env-config.sh",
"test:ci": "TZ=utc jest --ci --runInBand",
"test:dev": "TZ=utc jest --detectOpenHandles --coverage=false",
"test:ci": "jest --ci --runInBand",
"test:dev": "jest --detectOpenHandles --coverage=false",
"prettify": "prettier --write \"ts/**/*.(ts|tsx)\"",
"prettier:check": "prettier --check \"ts/**/*.(ts|tsx)\"",
"packager:clear": "rm -rf $TMPDIR/react-native-packager-cache-* && rm -rf $TMPDIR/metro-bundler-cache-*",
Expand Down
36 changes: 36 additions & 0 deletions ts/components/core/accordion/IOAccordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react";
import { StyleSheet } from "react-native";
import themeVariables from "../../../theme/variables";
import { H3 } from "../typography/H3";
import { IOStyles } from "../variables/IOStyles";
import { RawAccordion } from "./RawAccordion";

type Props = Omit<React.ComponentProps<typeof RawAccordion>, "header"> & {
title: string;
};

const styles = StyleSheet.create({
header: {
marginVertical: themeVariables.contentPadding
}
});

/**
* A simplified accordion that accepts a title and one child and uses {@link RawAccordion}
* @param props
* @constructor
*/
export const IOAccordion = (props: Props): React.ReactElement => (
<RawAccordion
animated={props.animated}
headerStyle={styles.header}
accessibilityLabel={props.title}
header={
<H3 numberOfLines={1} style={IOStyles.flex}>
{props.title}
</H3>
}
>
{props.children}
</RawAccordion>
);
125 changes: 125 additions & 0 deletions ts/components/core/accordion/RawAccordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { View } from "native-base";
import * as React from "react";
import { useEffect, useRef, useState } from "react";
import {
AccessibilityProps,
Animated,
Easing,
LayoutAnimation,
StyleSheet,
TouchableWithoutFeedback,
UIManager
} from "react-native";
import I18n from "../../../i18n";
import themeVariables from "../../../theme/variables";
import customVariables from "../../../theme/variables";
import { isAndroid } from "../../../utils/platform";
import IconFont from "../../ui/IconFont";
import { IOStyles } from "../variables/IOStyles";

// TODO: handle external initial open/closed state
type Props = {
// The header component, an arrow indicating the open/closed state will be added on the right
header: React.ReactElement;
// The accordion component must accept one children
children: React.ReactElement;
// The component should be animated? default: true
animated?: boolean;
headerStyle?: React.ComponentProps<typeof View>["style"];
accessibilityLabel?: AccessibilityProps["accessibilityLabel"];
};

const styles = StyleSheet.create({
headerIcon: {
alignSelf: "center"
},
row: {
...IOStyles.row,
justifyContent: "space-between"
},
internalHeader: {
flex: 1,
paddingRight: themeVariables.contentPadding
}
});

/**
* Obtains the degree starting from the open state
* @param isOpen
*/
const getDegree = (isOpen: boolean) => (isOpen ? "-90deg" : "-270deg");

/**
* The base accordion component, implements the opening and closing logic for viewing the children
* @param props
* @constructor
*/
export const RawAccordion: React.FunctionComponent<Props> = props => {
const [isOpen, setOpen] = useState<boolean>(false);
const animatedController = useRef(new Animated.Value(1)).current;
const shouldAnimate = props.animated ?? true;
const headerStyle = props.headerStyle ?? {};
const accessibilityLabel = props.accessibilityLabel
? `${props.accessibilityLabel}, `
: "";

const arrowAngle = shouldAnimate
? animatedController.interpolate({
inputRange: [0, 1],
outputRange: ["-90deg", "-270deg"]
})
: getDegree(isOpen);

useEffect(() => {
if (isAndroid) {
UIManager.setLayoutAnimationEnabledExperimental(shouldAnimate);
}
}, [shouldAnimate]);

const onPress = () => {
if (shouldAnimate) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
Animated.timing(animatedController, {
duration: 300,
toValue: isOpen ? 1 : 0,
useNativeDriver: true,
easing: Easing.linear
}).start();
}
setOpen(!isOpen);
};

return (
<View style={IOStyles.flex}>
<TouchableWithoutFeedback
onPress={onPress}
accessible={true}
accessibilityRole={"button"}
accessibilityLabel={
accessibilityLabel +
(isOpen
? I18n.t("global.accessibility.expanded")
: I18n.t("global.accessibility.collapsed"))
}
>
<View style={[styles.row, headerStyle]}>
<View style={styles.internalHeader}>{props.header}</View>
<Animated.View
testID={"ArrowAccordion"}
style={{
...styles.headerIcon,
transform: [{ rotateZ: arrowAngle }]
}}
>
<IconFont
name={"io-right"}
color={customVariables.brandPrimary}
size={24}
/>
</Animated.View>
</View>
</TouchableWithoutFeedback>
{isOpen && props.children}
</View>
);
};
131 changes: 131 additions & 0 deletions ts/components/core/accordion/__test__/RawAccordion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { fireEvent, render } from "@testing-library/react-native";
import { View } from "native-base";
import * as React from "react";
import Fingerprint from "../../../../../img/test/fingerprint.svg";
import I18n from "../../../../i18n";
import { Body } from "../../typography/Body";
import { H3 } from "../../typography/H3";
import { IOColors } from "../../variables/IOColors";
import { IOStyles } from "../../variables/IOStyles";
import { RawAccordion } from "../RawAccordion";

const bodyText = "This is a body text";
const headerText = "This is an header text";

describe("RawAccordion", () => {
jest.useFakeTimers();

// eslint-disable-next-line sonarjs/cognitive-complexity
describe("When a RawAccordion is rendered for the first time", () => {
it("Should be in the closed state", () => {
const res = renderRawAccordion();
expect(res.queryByText(headerText)).not.toBeNull();
expect(res.queryByText(bodyText)).toBeNull();
});
describe("And the header is tapped", () => {
it("Should expand and display the content", () => {
const res = renderRawAccordion();
const header = res.queryByText(headerText);
if (header !== null) {
fireEvent.press(header);
expect(res.queryByText(headerText)).not.toBeNull();
expect(res.queryByText(bodyText)).not.toBeNull();
} else {
fail("header not found");
}
});
describe("And the header is tapped again", () => {
it("Should close and hide the content", () => {
const res = renderRawAccordion();
const header = res.queryByText(headerText);
if (header !== null) {
fireEvent.press(header);
fireEvent.press(header);
expect(res.queryByText(bodyText)).toBeNull();
} else {
fail("header not found");
}
});
});
});
describe("And no accessibilityLabel is used", () => {
it("Should use only global.accessibility.collapsed", () => {
const res = renderRawAccordion();
expect(
res.queryByA11yLabel(I18n.t("global.accessibility.collapsed"))
).not.toBeNull();
});
describe("And the header is tapped", () => {
it("Should use only global.accessibility.expanded", () => {
const res = renderRawAccordion();
const header = res.queryByText(headerText);
if (header !== null) {
fireEvent.press(header);
expect(
res.queryByA11yLabel(I18n.t("global.accessibility.expanded"))
).not.toBeNull();
} else {
fail("header not found");
}
});
});
});
describe("Accessibility", () => {
describe("And an accessibilityLabel is used", () => {
it("Should use accessibilityLabel and global.accessibility.collapsed", () => {
const res = renderRawAccordion({
accessibilityLabel: "CustomAccessibilityLabel"
});
expect(
res.queryByA11yLabel(
`CustomAccessibilityLabel, ${I18n.t(
"global.accessibility.collapsed"
)}`
)
).not.toBeNull();
});
describe("And the header is tapped", () => {
it("Should use accessibilityLabel and global.accessibility.expanded", () => {
const res = renderRawAccordion({
accessibilityLabel: "CustomAccessibilityLabel"
});
const header = res.queryByText(headerText);
if (header !== null) {
fireEvent.press(header);
expect(
res.queryByA11yLabel(
`CustomAccessibilityLabel, ${I18n.t(
"global.accessibility.expanded"
)}`
)
).not.toBeNull();
} else {
fail("header not found");
}
});
});
});
});
});
});

const renderRawAccordion = (
props?: Pick<React.ComponentProps<typeof RawAccordion>, "accessibilityLabel">
) =>
render(
<RawAccordion
accessibilityLabel={props?.accessibilityLabel}
headerStyle={{
paddingVertical: 16,
backgroundColor: IOColors.greyLight
}}
header={
<View style={IOStyles.row}>
<Fingerprint width={32} height={32} />
<H3 style={{ alignSelf: "center" }}>{headerText}</H3>
</View>
}
>
<Body>{bodyText}</Body>
</RawAccordion>
);
Loading

0 comments on commit 0dc8658

Please sign in to comment.