@swan-io/shared-business 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swan-io/shared-business",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "engines": {
5
5
  "node": ">=14.0.0",
6
6
  "yarn": "^1.20.0"
@@ -29,7 +29,7 @@
29
29
  "@swan-io/lake": "^1.3.2"
30
30
  },
31
31
  "dependencies": {
32
- "@formatjs/intl": "^2.6.9",
32
+ "@formatjs/intl": "^2.7.0",
33
33
  "@googlemaps/js-api-loader": "^1.15.1",
34
34
  "@swan-io/boxed": "^0.13.0",
35
35
  "dayjs": "^1.11.7",
@@ -37,22 +37,22 @@
37
37
  "react-atomic-state": "^1.2.7",
38
38
  "react-dom": "^18.2.0",
39
39
  "react-dropzone": "^14.2.3",
40
- "react-native-web": "^0.19.1",
40
+ "react-native-web": "^0.19.4",
41
41
  "react-ux-form": "^1.3.0",
42
42
  "rifm": "^0.12.1",
43
- "ts-pattern": "^4.2.1",
43
+ "ts-pattern": "^4.2.2",
44
44
  "uuid": "^9.0.0"
45
45
  },
46
46
  "devDependencies": {
47
- "@swan-io/lake": "^1.3.2",
47
+ "@swan-io/lake": "^1.7.0",
48
48
  "@testing-library/react": "^14.0.0",
49
49
  "@testing-library/user-event": "^14.4.3",
50
- "@types/google.maps": "^3.52.3",
51
- "@types/react": "^18.0.28",
50
+ "@types/google.maps": "^3.52.5",
51
+ "@types/react": "^18.0.35",
52
52
  "@types/react-dom": "^18.0.11",
53
53
  "@types/react-native": "^0.71.5",
54
54
  "@types/uuid": "^9.0.1",
55
55
  "jsdom": "^21.1.1",
56
- "vitest": "^0.29.7"
56
+ "vitest": "^0.30.1"
57
57
  }
58
58
  }
@@ -8,7 +8,7 @@ export type EditorState = {
8
8
  firstName: string;
9
9
  lastName: string;
10
10
  birthDate: string;
11
- birthCountryCode: CountryCCA3;
11
+ birthCountryCode: CountryCCA3 | undefined;
12
12
  birthCity: string;
13
13
  birthCityPostalCode: string;
14
14
  type: BeneficiaryType;
@@ -38,6 +38,16 @@ const beneficiaryTypes = [
38
38
  { value: "HasCapital", name: t("beneficiaryForm.beneficiary.ownershipOfCapital") },
39
39
  { value: "Other", name: t("beneficiaryForm.beneficiary.other") },
40
40
  ];
41
+ const validateCca3CountryCode = value => {
42
+ if (value == null) {
43
+ return t("error.requiredField");
44
+ }
45
+ if (!isCountryCCA3(value)) {
46
+ // no need to set an error message because country picker contains only valid values
47
+ // this is used only for validateUbo function to display an error indicator without opening UBO modal
48
+ return " ";
49
+ }
50
+ };
41
51
  export const validateUbo = (editorState, accountCountry) => {
42
52
  const isAddressRequired = accountCountry === "DEU";
43
53
  const isBirthInfoRequired = accountCountry !== "DEU";
@@ -51,7 +61,7 @@ export const validateUbo = (editorState, accountCountry) => {
51
61
  birthDate: isBirthInfoRequired
52
62
  ? validateNullableRequired(editorState.birthDate)
53
63
  : undefined,
54
- birthCountryCode: validateNullableRequired(editorState.birthCountryCode),
64
+ birthCountryCode: validateCca3CountryCode(editorState.birthCountryCode),
55
65
  birthCity: isBirthInfoRequired
56
66
  ? validateNullableRequired(editorState.birthCity)
57
67
  : undefined,
@@ -52,10 +52,10 @@ export const SupportingDocument = forwardRef(({ documents, getAwsUrl, onChange,
52
52
  [doc.purpose]: [
53
53
  ...(acc[doc.purpose] ?? []),
54
54
  {
55
+ status: "finished",
55
56
  id: doc.id,
56
57
  name: doc.name,
57
58
  fileUrl: doc.downloadUrl,
58
- finished: true,
59
59
  },
60
60
  ],
61
61
  };
@@ -133,9 +133,9 @@ export const SupportingDocument = forwardRef(({ documents, getAwsUrl, onChange,
133
133
  const state = getFieldState(fieldName).value;
134
134
  setFieldValue(fieldName, state.map(doc => doc.id === NO_ID_YET
135
135
  ? {
136
+ status: "uploading",
136
137
  progress: 0,
137
138
  name: file.name,
138
- finished: false,
139
139
  id,
140
140
  }
141
141
  : doc));
@@ -144,15 +144,33 @@ export const SupportingDocument = forwardRef(({ documents, getAwsUrl, onChange,
144
144
  const state = getFieldState(fieldName).value;
145
145
  setFieldValue(fieldName, state.map(uploadState => uploadState.id === id ? { ...uploadState, progress } : uploadState));
146
146
  };
147
+ xhr.onerror = () => {
148
+ const state = getFieldState(fieldName).value;
149
+ setFieldValue(fieldName, state.map(uploadState => uploadState.id === id
150
+ ? {
151
+ status: "failed",
152
+ id: uploadState.id,
153
+ name: uploadState.name,
154
+ fileUrl: uploadState.fileUrl,
155
+ error: t("supportingDoc.errorUpload"),
156
+ }
157
+ : uploadState));
158
+ };
147
159
  xhr.onload = () => {
148
160
  const state = getFieldState(fieldName).value;
149
161
  if (xhr.status !== 200 && xhr.status !== 204) {
150
- setFieldValue(fieldName, state.filter(uploadState => (uploadState.id === id ? false : true)));
162
+ setFieldValue(fieldName, state.map(uploadState => uploadState.id === id
163
+ ? {
164
+ status: "failed",
165
+ id: uploadState.id,
166
+ name: uploadState.name,
167
+ fileUrl: uploadState.fileUrl,
168
+ error: t("supportingDoc.errorUpload"),
169
+ }
170
+ : uploadState));
151
171
  return;
152
172
  }
153
- setFieldValue(fieldName, state.map(uploadState => uploadState.id === id
154
- ? { ...uploadState, progress: undefined, finished: true }
155
- : uploadState));
173
+ setFieldValue(fieldName, state.map(uploadState => uploadState.id === id ? { ...uploadState, status: "finished" } : uploadState));
156
174
  };
157
175
  const formData = new FormData();
158
176
  fields.forEach(({ key, value }) => {
@@ -161,28 +179,39 @@ export const SupportingDocument = forwardRef(({ documents, getAwsUrl, onChange,
161
179
  formData.append("file", file);
162
180
  xhr.send(formData);
163
181
  })
164
- .catch(console.error);
182
+ .catch(() => {
183
+ const state = getFieldState(fieldName).value;
184
+ setFieldValue(fieldName, state.map(uploadState => uploadState.id === NO_ID_YET
185
+ ? {
186
+ status: "failed",
187
+ id: uploadState.id,
188
+ name: uploadState.name,
189
+ fileUrl: uploadState.fileUrl,
190
+ error: t("supportingDoc.errorUpload"),
191
+ }
192
+ : uploadState));
193
+ });
165
194
  };
166
195
  return (_jsxs(Form, { children: [requiredDocumentTypes.includes("CompanyRegistration") && (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: t("supportingDoc.companyRegistration"), help: _jsx(Help, { type: "tooltip", text: "supportingDoc.companyRegistration.description" }), render: () => (_jsx(Field, { name: "CompanyRegistration", children: ({ value, onChange, error }) => (_jsx(UploadArea, { layout: "horizontal", error: error, onDropAccepted: files => {
167
- onChange([...value, { id: NO_ID_YET }]);
196
+ onChange([...value, { id: NO_ID_YET, status: "uploading", progress: 0 }]);
168
197
  handleUpload(files, "CompanyRegistration");
169
198
  }, documents: value, accept: ACCEPTED_FORMATS, icon: "document-regular", description: t("supportingDoc.documentTypes"), maxSize: MAX_SUPPORTING_DOCUMENT_UPLOAD_SIZE })) })) }), _jsx(Space, { height: 24 })] })), requiredDocumentTypes.includes("AssociationRegistration") && (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: t("supportingDoc.associationRegistration"), help: _jsx(Help, { type: "tooltip", text: "supportingDoc.associationRegistration.description" }), render: () => (_jsx(Field, { name: "AssociationRegistration", children: ({ value, onChange, error }) => (_jsx(UploadArea, { layout: "horizontal", onDropAccepted: files => {
170
- onChange([...value, { id: NO_ID_YET }]);
199
+ onChange([...value, { id: NO_ID_YET, status: "uploading", progress: 0 }]);
171
200
  handleUpload(files, "AssociationRegistration");
172
201
  }, error: error, documents: value, accept: ACCEPTED_FORMATS, icon: "document-regular", description: t("supportingDoc.documentTypes"), maxSize: MAX_SUPPORTING_DOCUMENT_UPLOAD_SIZE })) })) }), _jsx(Space, { height: 24 })] })), requiredDocumentTypes.includes("SignedStatus") && (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: t("supportingDoc.signedStatus"), help: _jsx(Help, { type: "tooltip", text: "supportingDoc.signedStatus.description" }), render: () => (_jsx(Field, { name: "SignedStatus", children: ({ value, onChange, error }) => (_jsx(UploadArea, { layout: "horizontal", onDropAccepted: files => {
173
- onChange([...value, { id: NO_ID_YET }]);
202
+ onChange([...value, { id: NO_ID_YET, status: "uploading", progress: 0 }]);
174
203
  handleUpload(files, "SignedStatus");
175
204
  }, error: error, documents: value, accept: ACCEPTED_FORMATS, icon: "document-regular", description: t("supportingDoc.documentTypes"), maxSize: MAX_SUPPORTING_DOCUMENT_UPLOAD_SIZE })) })) }), _jsx(Space, { height: 24 })] })), requiredDocumentTypes.includes("UBODeclaration") && (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: t("supportingDoc.UBODeclaration"), help: _jsx(Help, { type: "tooltip", text: "supportingDoc.UBODeclaration.description" }), render: () => (_jsx(Field, { name: "UBODeclaration", children: ({ value, onChange, error }) => (_jsx(UploadArea, { layout: "horizontal", onDropAccepted: files => {
176
- onChange([...value, { id: NO_ID_YET }]);
205
+ onChange([...value, { id: NO_ID_YET, status: "uploading", progress: 0 }]);
177
206
  handleUpload(files, "UBODeclaration");
178
207
  }, error: error, documents: value, accept: ACCEPTED_FORMATS, icon: "document-regular", description: t("supportingDoc.documentTypes"), maxSize: MAX_SUPPORTING_DOCUMENT_UPLOAD_SIZE })) })) }), _jsx(Space, { height: 24 })] })), requiredDocumentTypes.includes("ProofOfIdentity") && (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: t("supportingDoc.proofOfIdentity"), help: _jsx(Help, { type: "tooltip", width: 600, text: "supportingDoc.proofOfIdentity.description" }), render: () => (_jsx(Field, { name: "ProofOfIdentity", children: ({ value, onChange, error }) => (_jsx(UploadArea, { layout: "horizontal", onDropAccepted: files => {
179
- onChange([...value, { id: NO_ID_YET }]);
208
+ onChange([...value, { id: NO_ID_YET, status: "uploading", progress: 0 }]);
180
209
  handleUpload(files, "ProofOfIdentity");
181
210
  }, error: error, documents: value, accept: ACCEPTED_FORMATS, icon: "document-regular", description: t("supportingDoc.documentTypes"), maxSize: MAX_SUPPORTING_DOCUMENT_UPLOAD_SIZE })) })) }), _jsx(Space, { height: 24 })] })), requiredDocumentTypes.includes("PowerOfAttorney") && (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: t("supportingDoc.powerAttornySigned"), help: _jsx(Help, { type: "button", label: t("supportingDoc.whatIsThis"), onPress: () => setShowPowerOfAttorneyModal(true) }), render: () => (_jsx(Field, { name: "PowerOfAttorney", children: ({ value, onChange, error }) => (_jsx(UploadArea, { layout: "horizontal", onDropAccepted: files => {
182
- onChange([...value, { id: NO_ID_YET }]);
211
+ onChange([...value, { id: NO_ID_YET, status: "uploading", progress: 0 }]);
183
212
  handleUpload(files, "PowerOfAttorney");
184
213
  }, error: error, documents: value, accept: ACCEPTED_FORMATS, icon: "document-regular", description: t("supportingDoc.documentTypes"), maxSize: MAX_SUPPORTING_DOCUMENT_UPLOAD_SIZE })) })) }), _jsx(Space, { height: 24 })] })), requiredDocumentTypes.includes("SwornStatement") && (_jsxs(_Fragment, { children: [_jsx(LakeLabel, { label: t("supportingDoc.swornStatementDeclaration"), help: _jsx(Help, { type: "button", label: t("supportingDoc.whatIsThis"), onPress: () => setShowSwornStatementModal(true) }), render: () => (_jsx(Field, { name: "SwornStatement", children: ({ value, onChange, error }) => (_jsx(UploadArea, { layout: "horizontal", onDropAccepted: files => {
185
- onChange([...value, { id: NO_ID_YET }]);
214
+ onChange([...value, { id: NO_ID_YET, status: "uploading", progress: 0 }]);
186
215
  handleUpload(files, "SwornStatement");
187
216
  }, error: error, documents: value, accept: ACCEPTED_FORMATS, icon: "document-regular", description: t("supportingDoc.documentTypes"), maxSize: MAX_SUPPORTING_DOCUMENT_UPLOAD_SIZE })) })) }), _jsx(Space, { height: 24 })] })), requiredDocumentTypes.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Space, { height: 24 }), _jsx(LakeText, { children: t("supportingDoc.noRequiredDocuments") }), _jsx(Space, { height: 24 })] })) : null, _jsxs(LakeModal, { visible: showPowerOfAttorneyModal, title: t("supportingDoc.powerAttorney"), icon: "document-regular", onPressClose: () => setShowPowerOfAttorneyModal(false), children: [_jsx(LakeText, { children: t("supportingDoc.powerAttornySigned.description") }), _jsx(Space, { height: 16 }), _jsx(LakeButtonGroup, { paddingBottom: 0, children: _jsx(LakeButton, { grow: true, color: "current", onPress: () => window.open(`/power-of-attorney-template/${match(onboardingLanguage)
188
217
  .with("fr", () => "fr")
@@ -1,12 +1,18 @@
1
1
  /// <reference types="react" />
2
2
  import { IconName } from "@swan-io/lake/src/components/Icon";
3
3
  import { DropzoneOptions } from "react-dropzone";
4
- export type UploadFileStatus = {
4
+ export type UploadFileStatus = ({
5
+ status: "uploading";
6
+ progress: number;
7
+ } | {
8
+ status: "finished";
9
+ } | {
10
+ status: "failed";
11
+ error: string;
12
+ }) & {
13
+ fileUrl?: string;
5
14
  id: string;
6
15
  name?: string;
7
- fileUrl?: string;
8
- progress?: number;
9
- finished?: boolean;
10
16
  };
11
17
  type Props = {
12
18
  icon: IconName;
@@ -9,10 +9,11 @@ import { commonStyles } from "@swan-io/lake/src/constants/commonStyles";
9
9
  import { backgroundColor, colors, gray75, radii, shadows, spacings, } from "@swan-io/lake/src/constants/design";
10
10
  import { useBoolean } from "@swan-io/lake/src/hooks/useBoolean";
11
11
  import { getIconNameFromFilename } from "@swan-io/lake/src/utils/file";
12
- import { isNotNullish, isNullish } from "@swan-io/lake/src/utils/nullish";
12
+ import { isNotNullish } from "@swan-io/lake/src/utils/nullish";
13
13
  import { Fragment, useLayoutEffect, useRef } from "react";
14
14
  import { useDropzone } from "react-dropzone";
15
15
  import { StyleSheet, Text, View } from "react-native";
16
+ import { match } from "ts-pattern";
16
17
  import { formatNestedMessage, t } from "../utils/i18n";
17
18
  const styles = StyleSheet.create({
18
19
  container: {
@@ -154,8 +155,11 @@ export const UploadArea = ({ icon, accept, value, documents = [], disabled = fal
154
155
  : formatNestedMessage("uploadArea.dropFile", {
155
156
  browse: _jsx(Text, { style: styles.browse, children: t("uploadArea.browse") }),
156
157
  }) }), isNotNullish(error) ? (_jsxs(_Fragment, { children: [_jsx(Space, { height: 4 }), _jsx(LakeText, { color: colors.negative[400], align: layout === "horizontal" ? "left" : "center", children: error })] })) : (!disabled &&
157
- isNotNullish(description) && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 4 }), _jsx(LakeText, { align: layout === "horizontal" ? "left" : "center", children: description })] })))] })] }))] }) }), documents.map(({ finished = false, ...document }, index) => {
158
- const progress = Math.max(document.progress ?? 0, 3);
159
- return (_jsxs(Fragment, { children: [_jsx(Space, { height: index === 0 ? 8 : 4 }), finished ? (_jsx(FileTile, { name: document.name ?? t("uploadArea.unknownFileName"), url: document.fileUrl, onRemove: isNullish(onRemoveFile) ? undefined : () => onRemoveFile(document.id) })) : (_jsxs(Box, { direction: "row", alignItems: "center", style: styles.item, children: [_jsx(LakeText, { numberOfLines: 1, color: colors.gray[700], children: t("uploadArea.uploading") }), _jsx(Space, { width: 20 }), _jsx(View, { role: "progressbar", style: styles.progressBar, children: _jsx(View, { style: [styles.progress, { width: `${progress}%` }] }) })] }))] }, document.id));
158
+ isNotNullish(description) && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 4 }), _jsx(LakeText, { align: layout === "horizontal" ? "left" : "center", children: description })] })))] })] }))] }) }), documents.map(({ ...document }, index) => {
159
+ return (_jsxs(Fragment, { children: [_jsx(Space, { height: index === 0 ? 8 : 4 }), match(document)
160
+ .with({ status: "finished" }, () => (_jsx(FileTile, { name: document.name ?? t("uploadArea.unknownFileName"), url: document.fileUrl, onRemove: () => onRemoveFile?.(document.id) })))
161
+ .with({ status: "failed" }, ({ error }) => (_jsx(FileTile, { name: document.name ?? t("uploadArea.unknownFileName"), url: document.fileUrl, onRemove: () => onRemoveFile?.(document.id), variant: "refused", title: error })))
162
+ .with({ status: "uploading" }, ({ progress }) => (_jsxs(Box, { direction: "row", alignItems: "center", style: styles.item, children: [_jsx(LakeText, { numberOfLines: 1, color: colors.gray[700], children: t("uploadArea.uploading") }), _jsx(Space, { width: 20 }), _jsx(View, { role: "progressbar", style: styles.progressBar, children: _jsx(View, { style: [styles.progress, { width: `${progress}%` }] }) })] })))
163
+ .exhaustive()] }, document.id));
160
164
  })] }));
161
165
  };
@@ -102,6 +102,7 @@
102
102
  "supportingDoc.companyRegistration.description": "Official document proving the legal existence of a company and providing info about its legal personality.\n(< 3 months old)",
103
103
  "supportingDoc.documentTypes": "Supported documents: .pdf, .png, .jpg",
104
104
  "supportingDoc.downloadTemplate": "Download a template",
105
+ "supportingDoc.errorUpload": "Failed to upload file",
105
106
  "supportingDoc.legalRepresentative": "Legal representative",
106
107
  "supportingDoc.noRequiredDocuments": "We don't need any documents from you. You can continue.",
107
108
  "supportingDoc.other": "Other",