@swan-io/shared-business 1.5.1 → 1.6.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.5.1",
3
+ "version": "1.6.0",
4
4
  "engines": {
5
5
  "node": ">=14.0.0",
6
6
  "yarn": "^1.20.0"
@@ -16,6 +16,7 @@ type Props = {
16
16
  optionalLabel?: string;
17
17
  placeholder?: string;
18
18
  isLarge: boolean;
19
+ apiKey: string;
19
20
  } & FormProps;
20
- export declare const AddressFormPart: ({ initialAddress, initialCity, initialPostalCode, country, label, optionalLabel, placeholder, Field, setFieldValue, listenFields, isLarge, }: Props) => JSX.Element;
21
+ export declare const AddressFormPart: ({ initialAddress, initialCity, initialPostalCode, country, label, optionalLabel, placeholder, Field, setFieldValue, listenFields, isLarge, apiKey, }: Props) => JSX.Element;
21
22
  export {};
@@ -6,8 +6,8 @@ import { Space } from "@swan-io/lake/src/components/Space";
6
6
  import { useBoolean } from "@swan-io/lake/src/hooks/useBoolean";
7
7
  import { useCallback, useEffect } from "react";
8
8
  import { locale, t } from "../utils/i18n";
9
- import { AddressSearchInput } from "./AddressSearchInput";
10
- export const AddressFormPart = ({ initialAddress, initialCity, initialPostalCode, country, label, optionalLabel, placeholder, Field, setFieldValue, listenFields, isLarge, }) => {
9
+ import { GMapAddressSearchInput } from "./GMapAddressSearchInput";
10
+ export const AddressFormPart = ({ initialAddress, initialCity, initialPostalCode, country, label, optionalLabel, placeholder, Field, setFieldValue, listenFields, isLarge, apiKey, }) => {
11
11
  const [manualModeEnabled, setManualMode] = useBoolean(initialAddress !== "" || initialCity !== "" || initialPostalCode !== "");
12
12
  useEffect(() => {
13
13
  if (!manualModeEnabled) {
@@ -22,5 +22,5 @@ export const AddressFormPart = ({ initialAddress, initialCity, initialPostalCode
22
22
  setFieldValue("postalCode", place.postalCode);
23
23
  setManualMode.on();
24
24
  }, [setManualMode, setFieldValue]);
25
- return (_jsxs(_Fragment, { children: [_jsx(Field, { name: "address", children: ({ ref, value, onChange, error }) => (_jsx(LakeLabel, { label: label ?? t("addressFormPart.addressLabel"), optionalLabel: optionalLabel, render: id => (_jsx(AddressSearchInput, { inputRef: ref, emptyResultText: t("common.noResult"), placeholder: placeholder ?? t("addressFormPart.placeholder"), language: locale.language, id: id, country: country, value: value, error: error, onValueChange: onChange, onSuggestion: onSuggestion })), actions: !manualModeEnabled && isLarge ? (_jsx(LakeButton, { mode: "secondary", size: "small", onPress: setManualMode.on, children: t("addressFormPart.setManual") })) : null })) }), !manualModeEnabled && !isLarge ? (_jsx(LakeButton, { mode: "secondary", size: "small", onPress: setManualMode.on, children: t("addressFormPart.setManual") })) : null, manualModeEnabled ? (_jsxs(_Fragment, { children: [_jsx(Space, { height: 12 }), _jsx(Field, { name: "city", children: ({ ref, value, valid, error, onChange }) => (_jsx(LakeLabel, { label: t("addressFormPart.cityLabel"), render: id => (_jsx(LakeTextInput, { ref: ref, id: id, value: value, valid: valid, error: error, onChangeText: onChange })) })) }), _jsx(Space, { height: 12 }), _jsx(Field, { name: "postalCode", children: ({ ref, value, valid, error, onChange }) => (_jsx(LakeLabel, { label: t("addressFormPart.postCodeLabel"), render: id => (_jsx(LakeTextInput, { ref: ref, id: id, value: value, valid: valid, error: error, onChangeText: onChange })) })) })] })) : null] }));
25
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { name: "address", children: ({ ref, value, onChange, error }) => (_jsx(LakeLabel, { label: label ?? t("addressFormPart.addressLabel"), optionalLabel: optionalLabel, render: id => (_jsx(GMapAddressSearchInput, { inputRef: ref, apiKey: apiKey, emptyResultText: t("common.noResult"), placeholder: placeholder ?? t("addressFormPart.placeholder"), language: locale.language, id: id, country: country, value: value, error: error, onValueChange: onChange, onSuggestion: onSuggestion })), actions: !manualModeEnabled && isLarge ? (_jsx(LakeButton, { mode: "secondary", size: "small", onPress: setManualMode.on, children: t("addressFormPart.setManual") })) : null })) }), !manualModeEnabled && !isLarge ? (_jsx(LakeButton, { mode: "secondary", size: "small", onPress: setManualMode.on, children: t("addressFormPart.setManual") })) : null, manualModeEnabled ? (_jsxs(_Fragment, { children: [_jsx(Space, { height: 12 }), _jsx(Field, { name: "city", children: ({ ref, value, valid, error, onChange }) => (_jsx(LakeLabel, { label: t("addressFormPart.cityLabel"), render: id => (_jsx(LakeTextInput, { ref: ref, id: id, value: value, valid: valid, error: error, onChangeText: onChange })) })) }), _jsx(Space, { height: 12 }), _jsx(Field, { name: "postalCode", children: ({ ref, value, valid, error, onChange }) => (_jsx(LakeLabel, { label: t("addressFormPart.postCodeLabel"), render: id => (_jsx(LakeTextInput, { ref: ref, id: id, value: value, valid: valid, error: error, onChangeText: onChange })) })) })] })) : null] }));
26
26
  };
@@ -25,6 +25,7 @@ type Props = {
25
25
  initialState?: EditorState;
26
26
  accountCountry: AccountCountry;
27
27
  step: BeneficiaryFormStep;
28
+ googleMapApiKey: string;
28
29
  onStepChange: (step: BeneficiaryFormStep) => void;
29
30
  onSave: (editorState: EditorState) => void | Promise<void>;
30
31
  onClose: () => void;
@@ -13,7 +13,7 @@ import { noop } from "@swan-io/lake/src/utils/function";
13
13
  import { isNotNullish, isNotNullishOrEmpty } from "@swan-io/lake/src/utils/nullish";
14
14
  import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
15
15
  import { StyleSheet, View } from "react-native";
16
- import { hasDefinedKeys, useForm } from "react-ux-form";
16
+ import { combineValidators, hasDefinedKeys, useForm } from "react-ux-form";
17
17
  import { Rifm } from "rifm";
18
18
  import { match } from "ts-pattern";
19
19
  import { v4 as uuid } from "uuid";
@@ -23,7 +23,8 @@ import { locale, rifmDateProps, t } from "../utils/i18n";
23
23
  import { validateBooleanRequired, validateIndividualTaxNumber, validateNullableRequired, validateRequired, } from "../utils/validation";
24
24
  import { AddressFormPart } from "./AddressFormPart";
25
25
  import { CountryPicker } from "./CountryPicker";
26
- import { LakeCityInput } from "./LakeCityInput";
26
+ import { GMapCityInput } from "./GMapCityInput";
27
+ import { TaxIdentificationNumberInput } from "./TaxIdentificationNumberInput";
27
28
  const styles = StyleSheet.create({
28
29
  inputContainer: {
29
30
  flex: 1,
@@ -39,13 +40,24 @@ const beneficiaryTypes = [
39
40
  ];
40
41
  export const validateUbo = (editorState, accountCountry) => {
41
42
  const isAddressRequired = accountCountry === "DEU";
43
+ const isBirthInfoRequired = accountCountry !== "DEU";
44
+ const isTaxIdentificationNumberRequired = accountCountry === "DEU" && editorState.residencyAddressCountry === "DEU";
45
+ const validateTaxNumber = isTaxIdentificationNumberRequired
46
+ ? combineValidators(validateNullableRequired, validateIndividualTaxNumber(accountCountry))
47
+ : validateIndividualTaxNumber(accountCountry);
42
48
  return {
43
49
  firstName: validateNullableRequired(editorState.firstName),
44
50
  lastName: validateNullableRequired(editorState.lastName),
45
- birthDate: validateNullableRequired(editorState.birthDate),
51
+ birthDate: isBirthInfoRequired
52
+ ? validateNullableRequired(editorState.birthDate)
53
+ : undefined,
46
54
  birthCountryCode: validateNullableRequired(editorState.birthCountryCode),
47
- birthCity: validateNullableRequired(editorState.birthCity),
48
- birthCityPostalCode: validateNullableRequired(editorState.birthCityPostalCode),
55
+ birthCity: isBirthInfoRequired
56
+ ? validateNullableRequired(editorState.birthCity)
57
+ : undefined,
58
+ birthCityPostalCode: isBirthInfoRequired
59
+ ? validateNullableRequired(editorState.birthCityPostalCode)
60
+ : undefined,
49
61
  type: validateNullableRequired(editorState.type),
50
62
  totalCapitalPercentage: editorState.type === "HasCapital"
51
63
  ? validateNullableRequired(editorState.totalCapitalPercentage?.toString())
@@ -62,7 +74,7 @@ export const validateUbo = (editorState, accountCountry) => {
62
74
  residencyAddressPostalCode: isAddressRequired
63
75
  ? validateNullableRequired(editorState.residencyAddressPostalCode)
64
76
  : undefined,
65
- taxIdentificationNumber: validateIndividualTaxNumber(editorState.taxIdentificationNumber ?? ""),
77
+ taxIdentificationNumber: validateTaxNumber(editorState.taxIdentificationNumber ?? ""),
66
78
  indirect: editorState.type !== "HasCapital" || editorState.direct === true
67
79
  ? undefined
68
80
  : validateBooleanRequired(editorState.indirect),
@@ -127,9 +139,10 @@ const requiredStepFields = [
127
139
  "birthCityPostalCode",
128
140
  "type",
129
141
  ];
130
- export const BeneficiaryForm = forwardRef(({ initialState, accountCountry, step, onStepChange, onClose, onSave, onCityLoadError }, ref) => {
142
+ export const BeneficiaryForm = forwardRef(({ initialState, accountCountry, step, googleMapApiKey, onStepChange, onClose, onSave, onCityLoadError, }, ref) => {
131
143
  const [reference] = useState(() => initialState?.reference ?? uuid());
132
144
  const isAddressRequired = accountCountry === "DEU";
145
+ const isBirthInfoRequired = accountCountry !== "DEU";
133
146
  const commonStepValues = useRef();
134
147
  const initialBirthDate = initialState?.birthDate;
135
148
  const { Field, FieldsListener, setFieldValue, submitForm, listenFields, validateField } = useForm({
@@ -147,7 +160,7 @@ export const BeneficiaryForm = forwardRef(({ initialState, accountCountry, step,
147
160
  initialValue: isNotNullishOrEmpty(initialBirthDate)
148
161
  ? decodeBirthDate(initialBirthDate)
149
162
  : "",
150
- validate: validateNullableRequired,
163
+ validate: isBirthInfoRequired ? validateNullableRequired : undefined,
151
164
  sanitize: value => value?.trim(),
152
165
  },
153
166
  birthCountryCode: {
@@ -156,12 +169,12 @@ export const BeneficiaryForm = forwardRef(({ initialState, accountCountry, step,
156
169
  },
157
170
  birthCity: {
158
171
  initialValue: initialState?.birthCity ?? "",
159
- validate: validateNullableRequired,
172
+ validate: isBirthInfoRequired ? validateNullableRequired : undefined,
160
173
  sanitize: value => value?.trim(),
161
174
  },
162
175
  birthCityPostalCode: {
163
176
  initialValue: initialState?.birthCityPostalCode ?? "",
164
- validate: validateNullableRequired,
177
+ validate: isBirthInfoRequired ? validateNullableRequired : undefined,
165
178
  sanitize: value => value?.trim(),
166
179
  },
167
180
  type: {
@@ -206,7 +219,13 @@ export const BeneficiaryForm = forwardRef(({ initialState, accountCountry, step,
206
219
  },
207
220
  taxIdentificationNumber: {
208
221
  initialValue: initialState?.taxIdentificationNumber,
209
- validate: validateIndividualTaxNumber,
222
+ validate: (value, { getFieldState }) => {
223
+ const uboCountry = getFieldState("country").value;
224
+ if (accountCountry === "DEU" && uboCountry === "DEU") {
225
+ return combineValidators(validateNullableRequired, validateIndividualTaxNumber(accountCountry))(value);
226
+ }
227
+ return validateIndividualTaxNumber(accountCountry)(value);
228
+ },
210
229
  sanitize: value => value?.trim(),
211
230
  },
212
231
  });
@@ -269,14 +288,16 @@ export const BeneficiaryForm = forwardRef(({ initialState, accountCountry, step,
269
288
  };
270
289
  });
271
290
  return (_jsx(ResponsiveContainer, { breakpoint: breakpoints.tiny, children: ({ small, large }) => (_jsxs(_Fragment, { children: [match(step)
272
- .with("common", () => (_jsxs(View, { role: "form", children: [_jsxs(Box, { direction: small ? "column" : "row", children: [_jsx(Field, { name: "firstName", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.firstName"), style: styles.inputContainer, render: id => (_jsx(LakeTextInput, { error: error, placeholder: t("beneficiaryForm.beneficiary.firstNamePlaceholder"), id: id, value: value, onChangeText: onChange })) })) }), _jsx(Space, { width: 12 }), _jsx(Field, { name: "lastName", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.lastName"), style: styles.inputContainer, render: id => (_jsx(LakeTextInput, { error: error, placeholder: t("beneficiaryForm.beneficiary.lastNamePlaceholder"), id: id, value: value, onChangeText: onChange })) })) })] }), _jsxs(Box, { direction: small ? "column" : "row", children: [_jsx(Field, { name: "birthDate", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.birthDate"), style: styles.inputContainer, render: id => (_jsx(Rifm, { value: value ?? "", onChange: onChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { error: error, placeholder: locale.datePlaceholder, id: id, value: value, onChange: onChange })) })) })) }), _jsx(Space, { width: 12 }), _jsx(Field, { name: "birthCountryCode", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.birthCountry"), style: styles.inputContainer, render: id => (_jsx(CountryPicker, { id: id, error: error, value: value, placeholder: t("beneficiaryForm.beneficiary.birthCountryPlaceholder"), items: allCountriesItems, onValueChange: onChange })) })) })] }), _jsx(Box, { direction: small ? "column" : "row", children: _jsx(FieldsListener, { names: ["birthCountryCode"], children: ({ birthCountryCode }) => (_jsxs(_Fragment, { children: [_jsx(Field, { name: "birthCity", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.birthCity"), style: styles.inputContainer, render: id => (_jsx(LakeCityInput, { id: id, error: error, country: birthCountryCode.value, value: value ?? "", onValueChange: onChange, placeholder: birthCountryCode.value == null
291
+ .with("common", () => (_jsxs(View, { role: "form", children: [_jsxs(Box, { direction: small ? "column" : "row", children: [_jsx(Field, { name: "firstName", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.firstName"), style: styles.inputContainer, render: id => (_jsx(LakeTextInput, { error: error, placeholder: t("beneficiaryForm.beneficiary.firstNamePlaceholder"), id: id, value: value, onChangeText: onChange })) })) }), _jsx(Space, { width: 12 }), _jsx(Field, { name: "lastName", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.lastName"), style: styles.inputContainer, render: id => (_jsx(LakeTextInput, { error: error, placeholder: t("beneficiaryForm.beneficiary.lastNamePlaceholder"), id: id, value: value, onChangeText: onChange })) })) })] }), _jsxs(Box, { direction: small ? "column" : "row", children: [_jsx(Field, { name: "birthDate", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.birthDate"), style: styles.inputContainer, render: id => (_jsx(Rifm, { value: value ?? "", onChange: onChange, ...rifmDateProps, children: ({ value, onChange }) => (_jsx(LakeTextInput, { error: error, placeholder: locale.datePlaceholder, id: id, value: value, onChange: onChange })) })) })) }), _jsx(Space, { width: 12 }), _jsx(Field, { name: "birthCountryCode", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.birthCountry"), style: styles.inputContainer, render: id => (_jsx(CountryPicker, { id: id, error: error, value: value, placeholder: t("beneficiaryForm.beneficiary.birthCountryPlaceholder"), items: allCountriesItems, onValueChange: onChange })) })) })] }), _jsx(Box, { direction: small ? "column" : "row", children: _jsx(FieldsListener, { names: ["birthCountryCode"], children: ({ birthCountryCode }) => (_jsxs(_Fragment, { children: [_jsx(Field, { name: "birthCity", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.birthCity"), style: styles.inputContainer, render: id => (_jsx(GMapCityInput, { id: id, apiKey: googleMapApiKey, error: error, country: birthCountryCode.value, value: value ?? "", onValueChange: onChange, placeholder: birthCountryCode.value == null
273
292
  ? t("beneficiaryForm.beneficiary.fillBirthCountry")
274
293
  : t("beneficiaryForm.beneficiary.birthCityPlaceholder"), onSuggestion: place => {
275
294
  onChange(place.city);
276
295
  setFieldValue("birthCityPostalCode", place.postalCode);
277
296
  }, onLoadError: onCityLoadError })) })) }), _jsx(Space, { width: 12 }), _jsx(Field, { name: "birthCityPostalCode", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.birthPostalCode"), style: styles.inputContainer, render: id => (_jsx(LakeTextInput, { error: error, placeholder: birthCountryCode.value == null
278
297
  ? t("beneficiaryForm.beneficiary.fillBirthCountry")
279
- : t("beneficiaryForm.beneficiary.birthPostalCodePlaceholder"), id: id, disabled: birthCountryCode.value === undefined, value: value, onChangeText: onChange })) })) })] })) }) }), _jsx(Field, { name: "type", children: ({ value, onChange }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.type"), type: "radioGroup", render: () => (_jsx(RadioGroup, { direction: "row", value: value, onValueChange: onChange, items: beneficiaryTypes })) })) }), _jsx(FieldsListener, { names: ["type"], children: ({ type }) => type.value === "HasCapital" ? (_jsxs(_Fragment, { children: [_jsx(Field, { name: "totalCapitalPercentage", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.totalCapitalPercentage"), render: id => (_jsx(LakeTextInput, { error: error, unit: "%", inputMode: "decimal", "aria-valuemin": 0, "aria-valuemax": 100, id: id, value: value, onChangeText: onChange })) })) }), _jsx(Space, { height: 12 }), _jsx(Field, { name: "capitalType", children: ({ value, error, onChange }) => (_jsx(CapitalTypeCheckboxes, { value: value ?? "none", error: error, onChange: onChange })) })] })) : null })] })))
280
- .with("address", () => (_jsxs(View, { role: "form", children: [_jsx(Field, { name: "country", children: ({ value, onChange }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.country"), render: id => (_jsx(CountryPicker, { id: id, value: value, items: individualCountriesItems, onValueChange: onChange })) })) }), _jsx(Space, { height: 12 }), _jsx(FieldsListener, { names: ["country"], children: ({ country }) => (_jsxs(_Fragment, { children: [_jsx(AddressFormPart, { initialAddress: initialState?.residencyAddressLine1 ?? "", initialCity: initialState?.residencyAddressCity ?? "", initialPostalCode: initialState?.residencyAddressPostalCode ?? "", country: country?.value ?? accountCountry, label: t("beneficiaryForm.beneficiary.address"), Field: Field, setFieldValue: setFieldValue, listenFields: listenFields, isLarge: large }), country?.value === "DEU" && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 12 }), _jsx(Field, { name: "taxIdentificationNumber", children: ({ value, error, valid, onChange }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.taxIdentificationNumber"), optionalLabel: t("common.optional"), render: id => (_jsx(LakeTextInput, { id: id, placeholder: t("beneficiaryForm.beneficiary.taxIdentificationNumberPlaceholder"), value: value, error: error, valid: valid, onChangeText: onChange })) })) })] }))] })) })] })))
298
+ : t("beneficiaryForm.beneficiary.birthPostalCodePlaceholder"), id: id, disabled: birthCountryCode.value === undefined, value: value, onChangeText: onChange })) })) })] })) }) }), accountCountry === "ESP" && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 12 }), _jsx(Field, { name: "taxIdentificationNumber", children: ({ value, error, valid, onChange }) => (_jsx(TaxIdentificationNumberInput, { value: value ?? "", error: error, valid: valid, onChange: onChange, accountCountry: accountCountry, isCompany: false })) }), _jsx(Space, { height: 12 })] })), _jsx(Field, { name: "type", children: ({ value, onChange }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.type"), type: "radioGroup", render: () => (_jsx(RadioGroup, { direction: "row", value: value, onValueChange: onChange, items: beneficiaryTypes })) })) }), _jsx(FieldsListener, { names: ["type"], children: ({ type }) => type.value === "HasCapital" ? (_jsxs(_Fragment, { children: [_jsx(Field, { name: "totalCapitalPercentage", children: ({ value, onChange, error }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.totalCapitalPercentage"), render: id => (_jsx(LakeTextInput, { error: error, unit: "%", inputMode: "decimal", "aria-valuemin": 0, "aria-valuemax": 100, id: id, value: value, onChangeText: onChange })) })) }), _jsx(Space, { height: 12 }), _jsx(Field, { name: "capitalType", children: ({ value, error, onChange }) => (_jsx(CapitalTypeCheckboxes, { value: value ?? "none", error: error, onChange: onChange })) })] })) : null })] })))
299
+ .with("address", () => (_jsxs(View, { role: "form", children: [_jsx(Field, { name: "country", children: ({ value, onChange }) => (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.country"), render: id => (_jsx(CountryPicker, { id: id, value: value, items: individualCountriesItems, onValueChange: onChange })) })) }), _jsx(Space, { height: 12 }), _jsx(FieldsListener, { names: ["country"], children: ({ country }) => (_jsxs(_Fragment, { children: [_jsx(AddressFormPart, { apiKey: googleMapApiKey, initialAddress: initialState?.residencyAddressLine1 ?? "", initialCity: initialState?.residencyAddressCity ?? "", initialPostalCode: initialState?.residencyAddressPostalCode ?? "", country: country?.value ?? accountCountry, label: t("beneficiaryForm.beneficiary.address"), Field: Field, setFieldValue: setFieldValue, listenFields: listenFields, isLarge: large }), country?.value === "DEU" && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 12 }), _jsx(Field, { name: "taxIdentificationNumber", children: ({ value, error, valid, onChange }) => (_jsx(TaxIdentificationNumberInput, { value: value ?? "", error: error, valid: valid, onChange: onChange, accountCountry: accountCountry, isCompany: false,
300
+ // is mandatory for German accounts and UBO living in Germany
301
+ required: true })) })] }))] })) })] })))
281
302
  .exhaustive(), isAddressRequired && (_jsxs(_Fragment, { children: [_jsx(Space, { height: 12 }), _jsx(StepDots, { currentStep: step, steps: formSteps })] }))] })) }));
282
303
  });
@@ -1,13 +1,7 @@
1
1
  import { CountryCCA3 } from "@swan-io/shared-business/src/constants/countries";
2
2
  import { MutableRefObject } from "react";
3
3
  import { StyleProp, ViewStyle } from "react-native";
4
- export type PlaceDetail = {
5
- completeAddress: string;
6
- streetNumber: string;
7
- city: string;
8
- country: string;
9
- postalCode: string;
10
- };
4
+ import { PlaceDetail } from "../hooks/useGoogleMapSDK";
11
5
  type Props = {
12
6
  inputRef?: MutableRefObject<unknown>;
13
7
  id?: string;
@@ -22,7 +16,7 @@ type Props = {
22
16
  placeholder: string;
23
17
  shouldDisplaySuggestions?: boolean;
24
18
  emptyResultText: string;
25
- apiKey?: string;
19
+ apiKey: string;
26
20
  };
27
- export declare const AddressSearchInput: ({ inputRef, id, country, disabled, error, value: initialValue, onValueChange, onSuggestion, language, placeholder, shouldDisplaySuggestions, emptyResultText, apiKey, }: Props) => JSX.Element | null;
21
+ export declare const GMapAddressSearchInput: ({ inputRef, id, country, disabled, error, value, onValueChange, onSuggestion, language, placeholder, shouldDisplaySuggestions, emptyResultText, apiKey, }: Props) => JSX.Element;
28
22
  export {};
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Future, Result } from "@swan-io/boxed";
3
+ import { AutoWidthImage } from "@swan-io/lake/src/components/AutoWidthImage";
4
+ import { AutocompleteSearchInput } from "@swan-io/lake/src/components/AutocompleteSearchInput";
5
+ import { Box } from "@swan-io/lake/src/components/Box";
6
+ import { countriesWithMultipleCCA3, } from "@swan-io/shared-business/src/constants/countries";
7
+ import { useCallback, useMemo } from "react";
8
+ import { StyleSheet } from "react-native";
9
+ import poweredByGoogle from "../assets/images/powered_by_google_on_white_hdpi.png";
10
+ import { getPlaceDetails, useGoogleMapSDK } from "../hooks/useGoogleMapSDK";
11
+ const styles = StyleSheet.create({
12
+ poweredByGoogle: {
13
+ paddingVertical: 8,
14
+ paddingHorizontal: 16,
15
+ },
16
+ });
17
+ export const GMapAddressSearchInput = ({ inputRef, id, country, disabled, error, value, onValueChange, onSuggestion, language, placeholder, shouldDisplaySuggestions = true, emptyResultText, apiKey, }) => {
18
+ const sdk = useGoogleMapSDK({
19
+ language,
20
+ apiKey,
21
+ });
22
+ const autocomplete = useMemo(() => sdk.map(google => new google.maps.places.AutocompleteService()), [sdk]);
23
+ const loadSuggestions = useCallback((value) => {
24
+ if (autocomplete.isDone()) {
25
+ return Future.fromPromise(autocomplete.get().getPlacePredictions({
26
+ input: value,
27
+ componentRestrictions: { country: countriesWithMultipleCCA3[country] ?? [country] },
28
+ types: ["address"],
29
+ })).mapOk(({ predictions }) => predictions.map(p => ({
30
+ id: p.place_id,
31
+ title: p.structured_formatting.main_text,
32
+ subtitle: p.structured_formatting.secondary_text,
33
+ value: null,
34
+ })), true);
35
+ }
36
+ return Future.value(Result.Ok([]));
37
+ }, [autocomplete, country]);
38
+ const onSuggestionSelected = useCallback((suggestion) => {
39
+ getPlaceDetails(suggestion.id).onResolve(result => {
40
+ if (result.isOk()) {
41
+ onSuggestion?.(result.value);
42
+ }
43
+ });
44
+ }, [onSuggestion]);
45
+ return (_jsx(AutocompleteSearchInput, { inputRef: inputRef, value: value, onValueChange: onValueChange, disabled: disabled, id: id, placeholder: placeholder, error: error, emptyResultText: emptyResultText, ListFooterComponent: _jsx(Box, { direction: "row", justifyContent: "end", style: styles.poweredByGoogle, children: _jsx(AutoWidthImage, { height: 14, sourceUri: poweredByGoogle }) }), shouldDisplaySuggestions: shouldDisplaySuggestions, loadSuggestions: loadSuggestions, onSuggestion: onSuggestionSelected }));
46
+ };
@@ -1,9 +1,6 @@
1
1
  /// <reference types="react" />
2
2
  import { CountryCCA3 } from "../constants/countries";
3
- type PlaceDetail = {
4
- city: string;
5
- postalCode: string;
6
- };
3
+ import { PlaceDetail } from "../hooks/useGoogleMapSDK";
7
4
  type Props = {
8
5
  value: string;
9
6
  onValueChange: (value: string) => void;
@@ -12,9 +9,9 @@ type Props = {
12
9
  error?: string;
13
10
  id?: string;
14
11
  placeholder?: string;
15
- onSuggestion?: (place: PlaceDetail) => void;
12
+ onSuggestion?: (place: Pick<PlaceDetail, "city" | "postalCode">) => void;
16
13
  onLoadError: (error: unknown) => void;
17
- apiKey?: string;
14
+ apiKey: string;
18
15
  };
19
- export declare const LakeCityInput: ({ value, onValueChange, country, disabled, placeholder, id, error, onSuggestion, onLoadError, apiKey, }: Props) => JSX.Element | null;
16
+ export declare const GMapCityInput: ({ value, onValueChange, country, disabled, placeholder, id, error, onSuggestion, onLoadError, apiKey, }: Props) => JSX.Element;
20
17
  export {};
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Future, Result } from "@swan-io/boxed";
3
+ import { AutoWidthImage } from "@swan-io/lake/src/components/AutoWidthImage";
4
+ import { AutocompleteSearchInput } from "@swan-io/lake/src/components/AutocompleteSearchInput";
5
+ import { Box } from "@swan-io/lake/src/components/Box";
6
+ import { isNotNullish } from "@swan-io/lake/src/utils/nullish";
7
+ import { useCallback, useMemo } from "react";
8
+ import { StyleSheet } from "react-native";
9
+ import poweredByGoogle from "../assets/images/powered_by_google_on_white_hdpi.png";
10
+ import { countriesWithMultipleCCA3 } from "../constants/countries";
11
+ import { getPlaceDetails, useGoogleMapSDK } from "../hooks/useGoogleMapSDK";
12
+ import { t } from "../utils/i18n";
13
+ const styles = StyleSheet.create({
14
+ poweredByGoogle: {
15
+ paddingVertical: 8,
16
+ paddingHorizontal: 16,
17
+ },
18
+ });
19
+ export const GMapCityInput = ({ value, onValueChange, country, disabled, placeholder, id, error, onSuggestion, onLoadError, apiKey, }) => {
20
+ const sdk = useGoogleMapSDK({ apiKey });
21
+ const autocomplete = useMemo(() => sdk.map(google => new google.maps.places.AutocompleteService()), [sdk]);
22
+ const loadSuggestions = useCallback((value) => {
23
+ if (autocomplete.isDone() && isNotNullish(country)) {
24
+ return Future.fromPromise(autocomplete.get().getPlacePredictions({
25
+ input: value,
26
+ componentRestrictions: {
27
+ country: [...(countriesWithMultipleCCA3[country] ?? "")],
28
+ },
29
+ types: ["(cities)"],
30
+ })).mapOk(({ predictions }) => predictions.map(p => ({
31
+ id: p.place_id,
32
+ title: p.structured_formatting.main_text,
33
+ subtitle: p.structured_formatting.secondary_text,
34
+ value: null,
35
+ })), true);
36
+ }
37
+ return Future.value(Result.Ok([]));
38
+ }, [autocomplete, country]);
39
+ const onSuggestionSelected = useCallback((suggestion) => {
40
+ getPlaceDetails(suggestion.id).onResolve(result => {
41
+ if (result.isOk()) {
42
+ console.log("result.value", result.value);
43
+ onSuggestion?.(result.value);
44
+ }
45
+ });
46
+ }, [onSuggestion]);
47
+ return (_jsx(AutocompleteSearchInput, { value: value, onValueChange: onValueChange, disabled: disabled, id: id, placeholder: placeholder, error: error, emptyResultText: t("common.noResult"), ListFooterComponent: _jsx(Box, { direction: "row", justifyContent: "end", style: styles.poweredByGoogle, children: _jsx(AutoWidthImage, { height: 14, sourceUri: poweredByGoogle }) }), loadSuggestions: loadSuggestions, onSuggestion: onSuggestionSelected, onLoadError: onLoadError }));
48
+ };
@@ -0,0 +1,15 @@
1
+ /// <reference types="react" />
2
+ import { AccountCountry } from "../utils/templateTranslations";
3
+ type Props = {
4
+ value: string;
5
+ error: string | undefined;
6
+ valid: boolean;
7
+ disabled?: boolean;
8
+ onChange: (value: string) => void;
9
+ onBlur?: () => void;
10
+ accountCountry: AccountCountry;
11
+ isCompany: boolean;
12
+ required?: boolean;
13
+ };
14
+ export declare const TaxIdentificationNumberInput: ({ value, error, valid, disabled, onChange, onBlur, accountCountry, isCompany, required, }: Props) => JSX.Element;
15
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Icon } from "@swan-io/lake/src/components/Icon";
3
+ import { LakeLabel } from "@swan-io/lake/src/components/LakeLabel";
4
+ import { LakeTextInput } from "@swan-io/lake/src/components/LakeTextInput";
5
+ import { LakeTooltip } from "@swan-io/lake/src/components/LakeTooltip";
6
+ import { colors } from "@swan-io/lake/src/constants/design";
7
+ import { t } from "../utils/i18n";
8
+ import { getCompanyTaxNumberHelp, getCompanyTaxNumberPlaceholder, getIndividualTaxNumberHelp, getIndividualTaxNumberPlaceholder, getTaxNumberTooltip, } from "../utils/templateTranslations";
9
+ export const TaxIdentificationNumberInput = ({ value, error, valid, disabled, onChange, onBlur, accountCountry, isCompany, required, }) => {
10
+ return (_jsx(LakeLabel, { label: t("beneficiaryForm.beneficiary.taxIdentificationNumber"), optionalLabel: required === true ? undefined : t("common.optional"), help: _jsx(LakeTooltip, { content: getTaxNumberTooltip(accountCountry), placement: "top", width: accountCountry === "DEU" ? 800 : undefined, children: _jsx(Icon, { name: "question-circle-regular", size: 16, color: colors.gray[600] }) }), render: id => (_jsx(LakeTextInput, { id: id, placeholder: isCompany
11
+ ? getCompanyTaxNumberPlaceholder(accountCountry)
12
+ : getIndividualTaxNumberPlaceholder(accountCountry), help: isCompany
13
+ ? getCompanyTaxNumberHelp(accountCountry)
14
+ : getIndividualTaxNumberHelp(accountCountry), value: value, error: error, valid: valid, disabled: disabled, onChangeText: onChange, onBlur: onBlur })) }));
15
+ };
@@ -1,8 +1,16 @@
1
1
  /// <reference types="google.maps" />
2
- import { AsyncData } from "@swan-io/boxed";
2
+ import { AsyncData, Future, Result } from "@swan-io/boxed";
3
3
  type Props = {
4
4
  language?: string;
5
- apiKey?: string;
5
+ apiKey: string;
6
6
  };
7
7
  export declare const useGoogleMapSDK: ({ language, apiKey }: Props) => AsyncData<typeof google>;
8
+ export type PlaceDetail = {
9
+ completeAddress: string;
10
+ streetNumber: string;
11
+ city: string;
12
+ country: string;
13
+ postalCode: string;
14
+ };
15
+ export declare const getPlaceDetails: (placeId: string) => Future<Result<PlaceDetail, unknown>>;
8
16
  export {};
@@ -1,7 +1,9 @@
1
1
  import { Loader } from "@googlemaps/js-api-loader";
2
- import { AsyncData } from "@swan-io/boxed";
2
+ import { AsyncData, Future, Result } from "@swan-io/boxed";
3
3
  import { useEffect } from "react";
4
4
  import { atom, useAtom } from "react-atomic-state";
5
+ import { match } from "ts-pattern";
6
+ import { getCCA3forCCA2 } from "../constants/countries";
5
7
  const sdk = atom(AsyncData.NotAsked());
6
8
  export const useGoogleMapSDK = ({ language, apiKey }) => {
7
9
  const googleSdk = useAtom(sdk);
@@ -21,3 +23,40 @@ export const useGoogleMapSDK = ({ language, apiKey }) => {
21
23
  }, [language, googleSdk, apiKey]);
22
24
  return googleSdk;
23
25
  };
26
+ export const getPlaceDetails = (placeId) => {
27
+ return Future.make(resolve => {
28
+ const place = new google.maps.places.PlacesService(document.createElement("div"));
29
+ place.getDetails({ placeId }, placeDetail => {
30
+ if (placeDetail?.address_components == null) {
31
+ resolve(Result.Error("No place detail found"));
32
+ return;
33
+ }
34
+ const result = {
35
+ completeAddress: "",
36
+ streetNumber: "",
37
+ postalCode: "",
38
+ country: "",
39
+ city: "",
40
+ };
41
+ placeDetail.address_components.forEach(({ types, short_name, long_name }) => {
42
+ const type = types[0];
43
+ match(type)
44
+ .with("street_number", () => (result.streetNumber = long_name))
45
+ .with("route", () => (result.completeAddress = long_name))
46
+ .with("postal_code", () => (result.postalCode = short_name))
47
+ .with("country", () => (result.country = getCCA3forCCA2(short_name) ?? short_name))
48
+ .with("locality", () => (result.city = long_name))
49
+ .otherwise(() => { });
50
+ });
51
+ if (result.streetNumber != "") {
52
+ if (placeDetail?.name === `${result.completeAddress} ${result.streetNumber}`) {
53
+ result.completeAddress = `${result.completeAddress} ${result.streetNumber}`;
54
+ }
55
+ else {
56
+ result.completeAddress = `${result.streetNumber} ${result.completeAddress}`;
57
+ }
58
+ }
59
+ resolve(Result.Ok(result));
60
+ });
61
+ });
62
+ };
@@ -47,6 +47,11 @@
47
47
  "businessActivity.scientificActivities": "Scientific activities",
48
48
  "businessActivity.transportation": "Transportation",
49
49
  "common.form.invalidTaxIdentificationNumber": "This tax identification number is invalid",
50
+ "common.form.taxIdentificationNumber.label": "Tax identification number",
51
+ "common.form.taxIdentificationNumber.placeholder": "Tax ID",
52
+ "common.form.taxIdentificationNumber.tooltip.deu": "Identifikationsnummer (also Steuerliche Identifikationsnummer, Persönliche Identificationsnummer, Identifikationsnummer, Steuer-IdNr., IdNr or Steuer-ID, according to § 139b AO)",
53
+ "common.form.help.nbCharacters": "{nbCharacters} characters",
54
+ "common.form.help.nbDigits": "{nbDigits} digits",
50
55
  "common.noResult": "Empty result",
51
56
  "common.optional": "Optional",
52
57
  "common.skipToContent": "Skip to content",
@@ -0,0 +1,6 @@
1
+ export type AccountCountry = "DEU" | "ESP" | "FRA";
2
+ export declare const getIndividualTaxNumberPlaceholder: (accountCountry: AccountCountry) => string;
3
+ export declare const getCompanyTaxNumberPlaceholder: (accountCountry: AccountCountry) => string;
4
+ export declare const getTaxNumberTooltip: (accountCountry: AccountCountry) => string;
5
+ export declare const getCompanyTaxNumberHelp: (accountCountry: AccountCountry) => string;
6
+ export declare const getIndividualTaxNumberHelp: (accountCountry: AccountCountry) => string;
@@ -0,0 +1,22 @@
1
+ import { match } from "ts-pattern";
2
+ import { t } from "./i18n";
3
+ export const getIndividualTaxNumberPlaceholder = (accountCountry) => match(accountCountry)
4
+ .with("DEU", () => `${t('common.form.taxIdentificationNumber.placeholder')} (Steueridentifikationsnummer)`)
5
+ .with("ESP", () => `${t('common.form.taxIdentificationNumber.placeholder')} (Número de Identificación Fiscal)`)
6
+ .otherwise(() => t('common.form.taxIdentificationNumber.placeholder'));
7
+ export const getCompanyTaxNumberPlaceholder = (accountCountry) => match(accountCountry)
8
+ .with("DEU", () => `${t('common.form.taxIdentificationNumber.placeholder')} (Steuer-Nummer)`)
9
+ .with("ESP", () => `${t('common.form.taxIdentificationNumber.placeholder')} (Número de Identificación Fiscal)`)
10
+ .otherwise(() => t('common.form.taxIdentificationNumber.placeholder'));
11
+ export const getTaxNumberTooltip = (accountCountry) => match(accountCountry)
12
+ .with("DEU", () => t("common.form.taxIdentificationNumber.tooltip.deu"))
13
+ .with("ESP", () => "Número de Identificación Fiscal") // no need to translate
14
+ .otherwise(() => '');
15
+ export const getCompanyTaxNumberHelp = (accountCountry) => match(accountCountry)
16
+ .with("DEU", () => t("common.form.help.nbDigits", { nbDigits: '10-11' }))
17
+ .with("ESP", () => t("common.form.help.nbCharacters", { nbCharacters: '9' }))
18
+ .otherwise(() => '');
19
+ export const getIndividualTaxNumberHelp = (accountCountry) => match(accountCountry)
20
+ .with("DEU", () => t("common.form.help.nbDigits", { nbDigits: '11' }))
21
+ .with("ESP", () => t("common.form.help.nbCharacters", { nbCharacters: '9' }))
22
+ .otherwise(() => '');
@@ -1,7 +1,8 @@
1
1
  import { Validator } from "react-ux-form";
2
+ import { AccountCountry } from "./templateTranslations";
2
3
  export declare const isValidVatNumber: (maybeVat: string) => boolean;
3
4
  export declare const validateNullableRequired: Validator<string | undefined>;
4
5
  export declare const validateRequired: Validator<string>;
5
6
  export declare const validateBooleanRequired: Validator<boolean | undefined>;
6
- export declare const validateIndividualTaxNumber: Validator<string | undefined>;
7
- export declare const validateCompanyTaxNumber: Validator<string | undefined>;
7
+ export declare const validateIndividualTaxNumber: (accountCountry: AccountCountry) => Validator<string | undefined>;
8
+ export declare const validateCompanyTaxNumber: (accountCountry: AccountCountry) => Validator<string | undefined>;
@@ -1,3 +1,5 @@
1
+ import { noop } from "@swan-io/lake/src/utils/function";
2
+ import { match } from "ts-pattern";
1
3
  import { t } from "./i18n";
2
4
  const VAT_NUMBER_REGEX = /^((AT)?U[0-9]{8}|(BE)?0[0-9]{9}|(BG)?[0-9]{9,10}|(CY)?[0-9]{8}L|(CZ)?[0-9]{8,10}|(DE)?[0-9]{9}|(DK)?[0-9]{8}|(EE)?[0-9]{9}|(EL|GR)?[0-9]{9}|(ES)?[0-9A-Z][0-9]{7}[0-9A-Z]|(FI)?[0-9]{8}|(FR)?[0-9A-Z]{2}[0-9]{9}|(GB)?([0-9]{9}([0-9]{3})?|[A-Z]{2}[0-9]{3})|(HU)?[0-9]{8}|(IE)?[0-9]S[0-9]{5}L|(IT)?[0-9]{11}|(LT)?([0-9]{9}|[0-9]{12})|(LU)?[0-9]{8}|(LV)?[0-9]{11}|(MT)?[0-9]{8}|(NL)?[0-9]{9}B[0-9]{2}|(PL)?[0-9]{10}|(PT)?[0-9]{9}|(RO)?[0-9]{2,10}|(SE)?[0-9]{12}|(SI)?[0-9]{8}|(SK)?[0-9]{10})$/;
3
5
  // (AT)?U[0-9]{8} | # Austria
@@ -45,21 +47,41 @@ export const validateBooleanRequired = value => {
45
47
  return t("error.requiredField");
46
48
  }
47
49
  };
48
- export const validateIndividualTaxNumber = value => {
50
+ export const validateIndividualTaxNumber = (accountCountry) => value => {
49
51
  if (value == null || !value) {
50
52
  return;
51
53
  }
52
- // accept 11 digits
53
- if (!/^\d{11}$/.test(value)) {
54
- return t("common.form.invalidTaxIdentificationNumber");
55
- }
54
+ return match(accountCountry)
55
+ .with("DEU", () => {
56
+ // accept 11 digits
57
+ if (!/^\d{11}$/.test(value)) {
58
+ return t("common.form.invalidTaxIdentificationNumber");
59
+ }
60
+ })
61
+ .with("ESP", () => {
62
+ // accept 9 characters
63
+ if (!/^[a-zA-Z0-9]{9}$/.test(value)) {
64
+ return t("common.form.invalidTaxIdentificationNumber");
65
+ }
66
+ })
67
+ .otherwise(noop);
56
68
  };
57
- export const validateCompanyTaxNumber = value => {
69
+ export const validateCompanyTaxNumber = (accountCountry) => value => {
58
70
  if (value == null || !value) {
59
71
  return;
60
72
  }
61
- // accept 10 or 11 digits
62
- if (!/^\d{10,11}$/.test(value)) {
63
- return t("common.form.invalidTaxIdentificationNumber");
64
- }
73
+ return match(accountCountry)
74
+ .with("DEU", () => {
75
+ // accept 10 or 11 digits
76
+ if (!/^\d{10,11}$/.test(value)) {
77
+ return t("common.form.invalidTaxIdentificationNumber");
78
+ }
79
+ })
80
+ .with("ESP", () => {
81
+ // accept 9 characters
82
+ if (!/^[a-zA-Z0-9]{9}$/.test(value)) {
83
+ return t("common.form.invalidTaxIdentificationNumber");
84
+ }
85
+ })
86
+ .otherwise(noop);
65
87
  };
@@ -1,119 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { AsyncData, Future } from "@swan-io/boxed";
3
- import { AutoWidthImage } from "@swan-io/lake/src/components/AutoWidthImage";
4
- import { Box } from "@swan-io/lake/src/components/Box";
5
- import { LakeCombobox } from "@swan-io/lake/src/components/LakeCombobox";
6
- import { LakeTextInput } from "@swan-io/lake/src/components/LakeTextInput";
7
- import { colors } from "@swan-io/lake/src/constants/colors";
8
- import { typography } from "@swan-io/lake/src/constants/typography";
9
- import { useFirstMountState } from "@swan-io/lake/src/hooks/useFirstMountState";
10
- import { countriesWithMultipleCCA3, getCCA3forCCA2, } from "@swan-io/shared-business/src/constants/countries";
11
- import { useEffect, useMemo, useRef, useState } from "react";
12
- import { StyleSheet, Text } from "react-native";
13
- import { match } from "ts-pattern";
14
- import poweredByGoogle from "../assets/images/powered_by_google_on_white_hdpi.png";
15
- import { useGoogleMapSDK } from "../hooks/useGoogleMapSDK";
16
- const styles = StyleSheet.create({
17
- itemTitle: {
18
- ...typography.bodyLarge,
19
- lineHeight: typography.lineHeights.title,
20
- userSelect: "none",
21
- },
22
- itemSubtitle: {
23
- ...typography.bodySmall,
24
- color: colors.gray[50],
25
- userSelect: "none",
26
- },
27
- poweredByGoogle: {
28
- paddingVertical: 8,
29
- paddingHorizontal: 16,
30
- },
31
- });
32
- export const AddressSearchInput = ({ inputRef, id, country, disabled, error, value: initialValue, onValueChange, onSuggestion, language, placeholder, shouldDisplaySuggestions = true, emptyResultText, apiKey, }) => {
33
- const isFirstMount = useFirstMountState();
34
- const [state, setState] = useState(AsyncData.NotAsked());
35
- const [value, setValue] = useState(initialValue ?? "");
36
- const showTriggerSearchRef = useRef(true);
37
- const sdk = useGoogleMapSDK({
38
- language,
39
- apiKey,
40
- });
41
- const autocomplete = useMemo(() => sdk.map(google => new google.maps.places.AutocompleteService()), [sdk]);
42
- useEffect(() => {
43
- if (!isFirstMount && initialValue != null) {
44
- setValue(initialValue);
45
- }
46
- }, [isFirstMount, initialValue]);
47
- useEffect(() => {
48
- if (value.length <= 3 || !shouldDisplaySuggestions || showTriggerSearchRef.current === false) {
49
- return setState(AsyncData.NotAsked());
50
- }
51
- setState(AsyncData.Loading());
52
- const request = Future.make(resolve => {
53
- const timeoutId = setTimeout(() => {
54
- if (autocomplete.isDone()) {
55
- Future.fromPromise(autocomplete.get().getPlacePredictions({
56
- input: value,
57
- componentRestrictions: { country: countriesWithMultipleCCA3[country] ?? [country] },
58
- types: ["address"],
59
- })).onResolve(resolve);
60
- }
61
- }, 250);
62
- return () => clearTimeout(timeoutId);
63
- }).mapOk(({ predictions }) => predictions.map(p => ({ value: p.place_id, prediction: p })), true);
64
- request.onResolve(value => setState(AsyncData.Done(value)));
65
- return () => request.cancel();
66
- }, [country, value, autocomplete, shouldDisplaySuggestions]);
67
- useEffect(() => {
68
- showTriggerSearchRef.current = true;
69
- }, [value]);
70
- return sdk.match({
71
- NotAsked: () => null,
72
- Loading: () => null,
73
- Done: google => {
74
- if (shouldDisplaySuggestions) {
75
- const place = new google.maps.places.PlacesService(document.createElement("div"));
76
- const selectAddress = (suggestion) => {
77
- place.getDetails({ placeId: suggestion.value }, placeDetail => {
78
- const result = {
79
- completeAddress: "",
80
- streetNumber: "",
81
- postalCode: "",
82
- country: "",
83
- city: "",
84
- };
85
- placeDetail?.address_components?.forEach(({ types, short_name, long_name }) => {
86
- const type = types[0];
87
- match(type)
88
- .with("street_number", () => (result.streetNumber = long_name))
89
- .with("route", () => (result.completeAddress = long_name))
90
- .with("postal_code", () => (result.postalCode = short_name))
91
- .with("country", () => (result.country = getCCA3forCCA2(short_name) ?? short_name))
92
- .with("locality", () => (result.city = long_name))
93
- .otherwise(() => { });
94
- });
95
- if (result.streetNumber != "") {
96
- if (placeDetail?.name === `${result.completeAddress} ${result.streetNumber}`) {
97
- result.completeAddress = `${result.completeAddress} ${result.streetNumber}`;
98
- }
99
- else {
100
- result.completeAddress = `${result.streetNumber} ${result.completeAddress}`;
101
- }
102
- }
103
- showTriggerSearchRef.current = false;
104
- setValue(result.completeAddress);
105
- onSuggestion?.(result);
106
- });
107
- };
108
- return (_jsx(LakeCombobox, { inputRef: inputRef, id: id, placeholder: placeholder, value: value, items: state, icon: "search-filled", disabled: disabled, error: error, ListFooterComponent: _jsx(Box, { direction: "row", justifyContent: "end", style: styles.poweredByGoogle, children: _jsx(AutoWidthImage, { height: 14, sourceUri: poweredByGoogle }) }), onSelectItem: suggestion => selectAddress(suggestion), onValueChange: value => {
109
- setValue(value);
110
- onValueChange(value);
111
- }, keyExtractor: item => item.value, emptyResultText: emptyResultText, renderItem: item => (_jsxs(_Fragment, { children: [_jsx(Text, { numberOfLines: 1, style: styles.itemTitle, children: item.prediction.structured_formatting.main_text }), _jsx(Text, { numberOfLines: 1, style: styles.itemSubtitle, children: item.prediction.structured_formatting.secondary_text })] })) }));
112
- }
113
- return (_jsx(LakeTextInput, { ref: inputRef, id: id, placeholder: placeholder, value: value, icon: "search-filled", disabled: disabled, error: error, onChangeText: value => {
114
- setValue(value);
115
- onValueChange(value);
116
- } }));
117
- },
118
- });
119
- };
@@ -1,88 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { AsyncData, Future } from "@swan-io/boxed";
3
- import { AutoWidthImage } from "@swan-io/lake/src/components/AutoWidthImage";
4
- import { Box } from "@swan-io/lake/src/components/Box";
5
- import { LakeCombobox } from "@swan-io/lake/src/components/LakeCombobox";
6
- import { colors } from "@swan-io/lake/src/constants/design";
7
- import { typography } from "@swan-io/lake/src/constants/typography";
8
- import { isNotNullish, isNullish } from "@swan-io/lake/src/utils/nullish";
9
- import { useEffect, useMemo, useRef, useState } from "react";
10
- import { StyleSheet, Text } from "react-native";
11
- import { match } from "ts-pattern";
12
- import poweredByGoogle from "../assets/images/powered_by_google_on_white_hdpi.png";
13
- import { countriesWithMultipleCCA3 } from "../constants/countries";
14
- import { useGoogleMapSDK } from "../hooks/useGoogleMapSDK";
15
- import { t } from "../utils/i18n";
16
- const styles = StyleSheet.create({
17
- itemTitle: {
18
- ...typography.bodyLarge,
19
- lineHeight: typography.lineHeights.title,
20
- userSelect: "none",
21
- },
22
- itemSubtitle: {
23
- ...typography.bodySmall,
24
- color: colors.gray[400],
25
- userSelect: "none",
26
- },
27
- poweredByGoogle: {
28
- paddingVertical: 8,
29
- paddingHorizontal: 16,
30
- },
31
- });
32
- export const LakeCityInput = ({ value, onValueChange, country, disabled, placeholder, id, error, onSuggestion, onLoadError, apiKey, }) => {
33
- const [state, setState] = useState(AsyncData.NotAsked());
34
- const comboboxRef = useRef(null);
35
- const sdk = useGoogleMapSDK({ apiKey });
36
- const autocomplete = useMemo(() => sdk.map(google => new google.maps.places.AutocompleteService()), [sdk]);
37
- useEffect(() => {
38
- if (value.length <= 3) {
39
- return setState(AsyncData.NotAsked());
40
- }
41
- setState(AsyncData.Loading());
42
- if (isNotNullish(country)) {
43
- const request = Future.make(resolve => {
44
- const timeoutId = setTimeout(() => {
45
- if (autocomplete.isDone()) {
46
- Future.fromPromise(autocomplete.get().getPlacePredictions({
47
- input: value,
48
- componentRestrictions: {
49
- country: [...(countriesWithMultipleCCA3[country] ?? "")],
50
- },
51
- types: ["(cities)"],
52
- })).onResolve(resolve);
53
- }
54
- }, 250);
55
- return () => clearTimeout(timeoutId);
56
- }).mapOk(({ predictions }) => predictions.map(p => ({ value: p.place_id, prediction: p })), true);
57
- request.tapError(onLoadError).onResolve(value => setState(AsyncData.Done(value)));
58
- return () => request.cancel();
59
- }
60
- }, [country, value, autocomplete, onLoadError]);
61
- return sdk.match({
62
- NotAsked: () => null,
63
- Loading: () => null,
64
- Done: google => {
65
- const place = new google.maps.places.PlacesService(document.createElement("div"));
66
- const selectAddress = (suggestion) => {
67
- place.getDetails({ placeId: suggestion.value }, placeDetail => {
68
- const result = {
69
- postalCode: "",
70
- city: "",
71
- };
72
- placeDetail?.address_components?.forEach(({ types, short_name, long_name }) => {
73
- const type = types[0];
74
- match(type)
75
- .with("postal_code", () => (result.postalCode = short_name))
76
- .with("locality", () => (result.city = long_name))
77
- .otherwise(() => { });
78
- });
79
- onSuggestion?.(result);
80
- if (comboboxRef.current) {
81
- comboboxRef.current.close();
82
- }
83
- });
84
- };
85
- return (_jsx(LakeCombobox, { ref: comboboxRef, placeholder: placeholder, value: value, items: state, icon: "search-filled", disabled: isNullish(country) || disabled, error: error, onValueChange: onValueChange, id: id, ListFooterComponent: _jsx(Box, { direction: "row", justifyContent: "end", style: styles.poweredByGoogle, children: _jsx(AutoWidthImage, { height: 14, sourceUri: poweredByGoogle }) }), onSelectItem: selectAddress, emptyResultText: t("common.noResult"), keyExtractor: item => item.value, renderItem: item => (_jsxs(_Fragment, { children: [_jsx(Text, { numberOfLines: 1, style: styles.itemTitle, children: item.prediction.structured_formatting.main_text }), _jsx(Text, { numberOfLines: 1, style: styles.itemSubtitle, children: item.prediction.structured_formatting.secondary_text })] })) }));
86
- },
87
- });
88
- };