ferns-ui 0.37.7 → 0.37.9
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/dist/Common.d.ts +2 -0
- package/dist/Constants.d.ts +12681 -0
- package/dist/Constants.js +3256 -0
- package/dist/Constants.js.map +1 -1
- package/dist/Field.d.ts +5 -1
- package/dist/Field.js +11 -4
- package/dist/Field.js.map +1 -1
- package/dist/MobileAddressAutoComplete.d.ts +12 -0
- package/dist/MobileAddressAutoComplete.js +55 -0
- package/dist/MobileAddressAutoComplete.js.map +1 -0
- package/dist/TapToEdit.js +8 -5
- package/dist/TapToEdit.js.map +1 -1
- package/dist/UnifiedAddressAutoComplete.d.ts +12 -0
- package/dist/UnifiedAddressAutoComplete.js +22 -0
- package/dist/UnifiedAddressAutoComplete.js.map +1 -0
- package/dist/Unifier.d.ts +5 -0
- package/dist/Unifier.js.map +1 -1
- package/dist/Utilities.d.ts +19 -0
- package/dist/Utilities.js +79 -0
- package/dist/Utilities.js.map +1 -1
- package/dist/WebAddressAutocomplete.d.ts +10 -0
- package/dist/WebAddressAutocomplete.js +57 -0
- package/dist/WebAddressAutocomplete.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/useStoredState.d.ts +1 -1
- package/dist/useStoredState.js.map +1 -1
- package/package.json +2 -1
- package/src/Common.ts +2 -0
- package/src/Constants.ts +3258 -0
- package/src/Field.tsx +41 -6
- package/src/MobileAddressAutoComplete.tsx +129 -0
- package/src/TapToEdit.tsx +11 -2
- package/src/UnifiedAddressAutoComplete.tsx +71 -0
- package/src/Unifier.ts +6 -0
- package/src/Utilities.tsx +120 -0
- package/src/WebAddressAutocomplete.tsx +90 -0
- package/src/index.tsx +3 -0
- package/src/useStoredState.ts +3 -3
package/src/Field.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import {Styles} from "react-native-google-places-autocomplete";
|
|
2
3
|
|
|
3
4
|
import {Box} from "./Box";
|
|
4
5
|
import {CheckBox} from "./CheckBox";
|
|
@@ -11,6 +12,7 @@ import {Switch} from "./Switch";
|
|
|
11
12
|
import {Text} from "./Text";
|
|
12
13
|
import {TextArea} from "./TextArea";
|
|
13
14
|
import {TextField} from "./TextField";
|
|
15
|
+
import {UnifiedAddressAutoCompleteField} from "./UnifiedAddressAutoComplete";
|
|
14
16
|
|
|
15
17
|
export interface FieldProps extends FieldWithLabelsProps {
|
|
16
18
|
name?: string;
|
|
@@ -41,6 +43,9 @@ export interface FieldProps extends FieldWithLabelsProps {
|
|
|
41
43
|
placeholder?: string;
|
|
42
44
|
disabled?: boolean;
|
|
43
45
|
useCheckbox?: boolean;
|
|
46
|
+
includeCounty?: boolean;
|
|
47
|
+
googleMapsApiKey?: string;
|
|
48
|
+
googlePlacesMobileStyles?: Styles;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
export const Field = ({
|
|
@@ -59,11 +64,18 @@ export const Field = ({
|
|
|
59
64
|
errorMessageColor,
|
|
60
65
|
helperText,
|
|
61
66
|
helperTextColor,
|
|
67
|
+
includeCounty = false,
|
|
68
|
+
googleMapsApiKey,
|
|
69
|
+
googlePlacesMobileStyles,
|
|
62
70
|
}: FieldProps) => {
|
|
63
71
|
const handleAddressChange = (field: string, newValue: string) => {
|
|
64
72
|
onChange({...value, [field]: newValue});
|
|
65
73
|
};
|
|
66
74
|
|
|
75
|
+
const handleAutoCompleteChange = (newValue: AddressInterface) => {
|
|
76
|
+
onChange({...value, ...newValue});
|
|
77
|
+
};
|
|
78
|
+
|
|
67
79
|
const handleSwitchChange = (switchValue: boolean) => {
|
|
68
80
|
onChange(switchValue);
|
|
69
81
|
};
|
|
@@ -169,16 +181,19 @@ export const Field = ({
|
|
|
169
181
|
city = "",
|
|
170
182
|
state = "",
|
|
171
183
|
zipcode = "",
|
|
184
|
+
countyName = "",
|
|
185
|
+
countyCode = "",
|
|
172
186
|
}: AddressInterface = addressValue;
|
|
173
187
|
return (
|
|
174
188
|
<>
|
|
175
|
-
<
|
|
189
|
+
<UnifiedAddressAutoCompleteField
|
|
176
190
|
disabled={disabled}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
googleMapsApiKey={googleMapsApiKey}
|
|
192
|
+
googlePlacesMobileStyles={googlePlacesMobileStyles}
|
|
193
|
+
handleAddressChange={(result) => handleAddressChange("address1", result.value)}
|
|
194
|
+
handleAutoCompleteChange={(result) => handleAutoCompleteChange(result)}
|
|
195
|
+
includeCounty={includeCounty}
|
|
196
|
+
inputValue={address1}
|
|
182
197
|
/>
|
|
183
198
|
<TextField
|
|
184
199
|
disabled={disabled}
|
|
@@ -214,6 +229,26 @@ export const Field = ({
|
|
|
214
229
|
value={zipcode}
|
|
215
230
|
onChange={(result) => handleAddressChange("zipcode", result.value)}
|
|
216
231
|
/>
|
|
232
|
+
{includeCounty && (
|
|
233
|
+
<>
|
|
234
|
+
<TextField
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
id="countyName"
|
|
237
|
+
label="County Name"
|
|
238
|
+
type="text"
|
|
239
|
+
value={countyName}
|
|
240
|
+
onChange={(result) => handleAddressChange("countyName", result.value)}
|
|
241
|
+
/>
|
|
242
|
+
<TextField
|
|
243
|
+
disabled={disabled}
|
|
244
|
+
id="countyCode"
|
|
245
|
+
label="County Code"
|
|
246
|
+
type="number"
|
|
247
|
+
value={countyCode}
|
|
248
|
+
onChange={(result) => handleAddressChange("countyCode", result.value)}
|
|
249
|
+
/>
|
|
250
|
+
</>
|
|
251
|
+
)}
|
|
217
252
|
</>
|
|
218
253
|
);
|
|
219
254
|
} else if (type === "customSelect") {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, {useContext, useEffect, useRef, useState} from "react";
|
|
2
|
+
import {TextStyle, TouchableOpacity, View} from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
GooglePlacesAutocomplete,
|
|
5
|
+
GooglePlacesAutocompleteRef,
|
|
6
|
+
Styles,
|
|
7
|
+
} from "react-native-google-places-autocomplete";
|
|
8
|
+
|
|
9
|
+
import {AddressInterface, OnChangeCallback} from "./Common";
|
|
10
|
+
import {GOOGLE_PLACES_API_RESTRICTIONS} from "./Constants";
|
|
11
|
+
import {TextField} from "./TextField";
|
|
12
|
+
import {ThemeContext} from "./Theme";
|
|
13
|
+
import {processAddressComponents} from "./Utilities";
|
|
14
|
+
|
|
15
|
+
export const MobileAddressAutocomplete = ({
|
|
16
|
+
disabled,
|
|
17
|
+
googleMapsApiKey,
|
|
18
|
+
includeCounty,
|
|
19
|
+
inputValue,
|
|
20
|
+
// More on react-native-google-places-autocomplete styles here: https://github.com/FaridSafi/react-native-google-places-autocomplete#styling
|
|
21
|
+
styles,
|
|
22
|
+
handleAddressChange,
|
|
23
|
+
handleAutoCompleteChange,
|
|
24
|
+
}: {
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
googleMapsApiKey?: string;
|
|
27
|
+
includeCounty?: boolean;
|
|
28
|
+
inputValue: string;
|
|
29
|
+
styles?: Styles;
|
|
30
|
+
handleAddressChange: OnChangeCallback;
|
|
31
|
+
handleAutoCompleteChange: (value: AddressInterface) => void;
|
|
32
|
+
}) => {
|
|
33
|
+
const {theme} = useContext(ThemeContext);
|
|
34
|
+
const ref = useRef<GooglePlacesAutocompleteRef | null>(null);
|
|
35
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!googleMapsApiKey) return;
|
|
39
|
+
if (ref?.current) {
|
|
40
|
+
ref.current.setAddressText(inputValue);
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const textInputContainerStyles = {
|
|
46
|
+
backgroundColor: theme.white,
|
|
47
|
+
borderColor: isFocused ? theme.blue : theme.gray,
|
|
48
|
+
borderWidth: isFocused ? 5 : 1,
|
|
49
|
+
borderRadius: 16,
|
|
50
|
+
paddingHorizontal: isFocused ? 10 : 14,
|
|
51
|
+
paddingVertical: isFocused ? 0 : 4,
|
|
52
|
+
...(styles?.textInputContainer as object),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const textInputStyles = {
|
|
56
|
+
backgroundColor: theme.white,
|
|
57
|
+
borderRadius: 16,
|
|
58
|
+
color: theme.darkGray,
|
|
59
|
+
fontFamily: theme.primaryFont,
|
|
60
|
+
fontSize: (styles?.textInput as TextStyle)?.fontSize ?? 14,
|
|
61
|
+
height: 40,
|
|
62
|
+
marginBottom: 0,
|
|
63
|
+
paddingHorizontal: 0,
|
|
64
|
+
paddingVertical: 4,
|
|
65
|
+
...(styles?.textInput as object),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (!googleMapsApiKey) {
|
|
69
|
+
return (
|
|
70
|
+
<TextField
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
id="address1"
|
|
73
|
+
label="Street Address"
|
|
74
|
+
type="text"
|
|
75
|
+
value={inputValue}
|
|
76
|
+
onChange={(result) => handleAddressChange(result)}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<TouchableOpacity activeOpacity={1} style={{flex: 1}} onPress={() => setIsFocused(false)}>
|
|
83
|
+
<View>
|
|
84
|
+
<GooglePlacesAutocomplete
|
|
85
|
+
ref={ref}
|
|
86
|
+
GooglePlacesDetailsQuery={{
|
|
87
|
+
fields: Object.values(GOOGLE_PLACES_API_RESTRICTIONS.fields).join(","),
|
|
88
|
+
}}
|
|
89
|
+
disableScroll
|
|
90
|
+
fetchDetails
|
|
91
|
+
placeholder="Street Address"
|
|
92
|
+
query={{
|
|
93
|
+
key: googleMapsApiKey,
|
|
94
|
+
language: "en",
|
|
95
|
+
components: `country:${GOOGLE_PLACES_API_RESTRICTIONS.components.country}`,
|
|
96
|
+
}}
|
|
97
|
+
styles={{
|
|
98
|
+
textInputContainer: {
|
|
99
|
+
...textInputContainerStyles,
|
|
100
|
+
},
|
|
101
|
+
textInput: {
|
|
102
|
+
...textInputStyles,
|
|
103
|
+
},
|
|
104
|
+
...styles,
|
|
105
|
+
}}
|
|
106
|
+
textInputProps={{
|
|
107
|
+
onFocus: () => setIsFocused(true),
|
|
108
|
+
onBlur: () => setIsFocused(false),
|
|
109
|
+
onChange: (event) => {
|
|
110
|
+
handleAddressChange({value: event.nativeEvent.text});
|
|
111
|
+
},
|
|
112
|
+
}}
|
|
113
|
+
onPress={(data, details = null) => {
|
|
114
|
+
const addressComponents = details?.address_components;
|
|
115
|
+
const formattedAddressObject = processAddressComponents(addressComponents, {
|
|
116
|
+
includeCounty,
|
|
117
|
+
});
|
|
118
|
+
const {address1} = formattedAddressObject;
|
|
119
|
+
handleAutoCompleteChange(formattedAddressObject);
|
|
120
|
+
if (ref.current) {
|
|
121
|
+
ref.current.setAddressText(address1);
|
|
122
|
+
}
|
|
123
|
+
setIsFocused(false);
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</View>
|
|
127
|
+
</TouchableOpacity>
|
|
128
|
+
);
|
|
129
|
+
};
|
package/src/TapToEdit.tsx
CHANGED
|
@@ -21,19 +21,28 @@ export function formatAddress(address: any, asString = false): string {
|
|
|
21
21
|
|
|
22
22
|
const zip = address?.zipcode || "";
|
|
23
23
|
|
|
24
|
+
const countyName = address?.countyName ?? "";
|
|
25
|
+
|
|
26
|
+
const countyCode = address?.countyCode ?? "";
|
|
27
|
+
|
|
24
28
|
const addressLineOne = address?.address1 ?? "";
|
|
25
29
|
const addressLineTwo = address?.address2 ?? "";
|
|
26
30
|
const addressLineThree = `${city}${state}${zip}`;
|
|
31
|
+
const addressLineFour = `${countyName}${address?.countyCode ? ` [${countyCode}]` : ""}`;
|
|
27
32
|
|
|
28
33
|
if (!asString) {
|
|
29
34
|
// Only add new lines if lines before and after are not empty to avoid awkward whitespace
|
|
30
35
|
return `${addressLineOne}${
|
|
31
36
|
addressLineOne && (addressLineTwo || addressLineThree) ? `\n` : ""
|
|
32
|
-
}${addressLineTwo}${addressLineTwo && addressLineThree ? `\n` : ""}${addressLineThree}
|
|
37
|
+
}${addressLineTwo}${addressLineTwo && addressLineThree ? `\n` : ""}${addressLineThree}${
|
|
38
|
+
addressLineThree && addressLineFour ? `\n` : ""
|
|
39
|
+
}${addressLineFour}`;
|
|
33
40
|
} else {
|
|
34
41
|
return `${addressLineOne}${
|
|
35
42
|
addressLineOne && (addressLineTwo || addressLineThree) ? `, ` : ""
|
|
36
|
-
}${addressLineTwo}${addressLineTwo && addressLineThree ? `, ` : ""}${addressLineThree}
|
|
43
|
+
}${addressLineTwo}${addressLineTwo && addressLineThree ? `, ` : ""}${addressLineThree}${
|
|
44
|
+
addressLineThree && addressLineFour ? `, ` : ""
|
|
45
|
+
}${addressLineFour}`;
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, {useMemo} from "react";
|
|
2
|
+
import {Styles} from "react-native-google-places-autocomplete";
|
|
3
|
+
|
|
4
|
+
import {AddressInterface, OnChangeCallback} from "./Common";
|
|
5
|
+
import {isMobileDevice} from "./MediaQuery";
|
|
6
|
+
import {MobileAddressAutocomplete} from "./MobileAddressAutoComplete";
|
|
7
|
+
import {TextField} from "./TextField";
|
|
8
|
+
import {isNative, isValidGoogleApiKey} from "./Utilities";
|
|
9
|
+
import {WebAddressAutocomplete} from "./WebAddressAutocomplete";
|
|
10
|
+
|
|
11
|
+
export const UnifiedAddressAutoCompleteField = ({
|
|
12
|
+
disabled,
|
|
13
|
+
googleMapsApiKey,
|
|
14
|
+
googlePlacesMobileStyles,
|
|
15
|
+
includeCounty,
|
|
16
|
+
inputValue,
|
|
17
|
+
handleAddressChange,
|
|
18
|
+
handleAutoCompleteChange,
|
|
19
|
+
}: {
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
googleMapsApiKey?: string;
|
|
22
|
+
googlePlacesMobileStyles?: Styles;
|
|
23
|
+
includeCounty?: boolean;
|
|
24
|
+
inputValue: string;
|
|
25
|
+
handleAddressChange: OnChangeCallback;
|
|
26
|
+
handleAutoCompleteChange: (value: AddressInterface) => void;
|
|
27
|
+
}) => {
|
|
28
|
+
const isWeb = typeof document !== "undefined";
|
|
29
|
+
const isValidatedGoogleApiKey = useMemo(
|
|
30
|
+
() => (googleMapsApiKey ? isValidGoogleApiKey(googleMapsApiKey) : false),
|
|
31
|
+
[googleMapsApiKey]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (isWeb && isValidatedGoogleApiKey) {
|
|
35
|
+
return (
|
|
36
|
+
<WebAddressAutocomplete
|
|
37
|
+
disabled={disabled}
|
|
38
|
+
googleMapsApiKey={googleMapsApiKey}
|
|
39
|
+
handleAddressChange={handleAddressChange}
|
|
40
|
+
handleAutoCompleteChange={handleAutoCompleteChange}
|
|
41
|
+
includeCounty={includeCounty}
|
|
42
|
+
inputValue={inputValue}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
} else if (isMobileDevice() && isNative() && isValidatedGoogleApiKey) {
|
|
46
|
+
return (
|
|
47
|
+
<MobileAddressAutocomplete
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
googleMapsApiKey={googleMapsApiKey}
|
|
50
|
+
handleAddressChange={handleAddressChange}
|
|
51
|
+
handleAutoCompleteChange={handleAutoCompleteChange}
|
|
52
|
+
includeCounty={includeCounty}
|
|
53
|
+
inputValue={inputValue}
|
|
54
|
+
styles={googlePlacesMobileStyles}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
return (
|
|
59
|
+
<TextField
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
label="Street Address"
|
|
62
|
+
placeholder="Enter an address"
|
|
63
|
+
type="text"
|
|
64
|
+
value={inputValue}
|
|
65
|
+
onChange={({value}): void => {
|
|
66
|
+
handleAddressChange({value});
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
};
|
package/src/Unifier.ts
CHANGED
|
@@ -9,6 +9,12 @@ import {Dimensions, Keyboard, Linking, Platform, Vibration} from "react-native";
|
|
|
9
9
|
import {PermissionKind, UnifiedTheme} from "./Common";
|
|
10
10
|
import {requestPermissions} from "./Permissions";
|
|
11
11
|
|
|
12
|
+
declare global {
|
|
13
|
+
interface Window {
|
|
14
|
+
google: any;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
export type PlatformOS = "ios" | "android" | "web";
|
|
13
19
|
|
|
14
20
|
type Luminance = "light" | "lighter" | "dark" | "darker";
|
package/src/Utilities.tsx
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import get from "lodash/get";
|
|
4
4
|
import {Platform} from "react-native";
|
|
5
5
|
|
|
6
|
+
import {COUNTY_AND_COUNTY_EQUIVALENT_ENTITIES} from "./Constants";
|
|
7
|
+
|
|
6
8
|
export function mergeInlineStyles(inlineStyle?: any, newStyle?: any) {
|
|
7
9
|
const inline = get(inlineStyle, "__style");
|
|
8
10
|
return {
|
|
@@ -162,3 +164,121 @@ export const union =
|
|
|
162
164
|
export const isNative = (): boolean => {
|
|
163
165
|
return ["android", "ios"].includes(Platform.OS);
|
|
164
166
|
};
|
|
167
|
+
|
|
168
|
+
// Find more information about the address component types here: https://developers.google.com/maps/documentation/javascript/place-autocomplete
|
|
169
|
+
export type AddressComponentType = {
|
|
170
|
+
long_name: string;
|
|
171
|
+
short_name: string;
|
|
172
|
+
types: string[];
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const findAddressComponent = (components: AddressComponentType[], type: string): string => {
|
|
176
|
+
return (
|
|
177
|
+
components.find((component: AddressComponentType) => component.types.includes(type))
|
|
178
|
+
?.long_name ?? ""
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
interface ProcessAddressComponentOptions {
|
|
183
|
+
includeCounty?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const processAddressComponents = (
|
|
187
|
+
addressComponents: AddressComponentType[] | undefined,
|
|
188
|
+
options?: ProcessAddressComponentOptions
|
|
189
|
+
) => {
|
|
190
|
+
let processedAddressComponents: {
|
|
191
|
+
address1: string;
|
|
192
|
+
city: string;
|
|
193
|
+
state: string;
|
|
194
|
+
zipcode: string;
|
|
195
|
+
countyName?: string;
|
|
196
|
+
countyCode?: string;
|
|
197
|
+
} = {
|
|
198
|
+
address1: "",
|
|
199
|
+
city: "",
|
|
200
|
+
state: "",
|
|
201
|
+
zipcode: "",
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (!addressComponents || addressComponents.length === 0) {
|
|
205
|
+
console.warn("Invalid address components");
|
|
206
|
+
if (options?.includeCounty) {
|
|
207
|
+
return {
|
|
208
|
+
...processedAddressComponents,
|
|
209
|
+
countyName: "",
|
|
210
|
+
countyCode: "",
|
|
211
|
+
};
|
|
212
|
+
} else {
|
|
213
|
+
return processedAddressComponents;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const streetNumber = findAddressComponent(addressComponents, "street_number");
|
|
218
|
+
const streetName = findAddressComponent(addressComponents, "route");
|
|
219
|
+
const city = findAddressComponent(addressComponents, "locality");
|
|
220
|
+
const state = findAddressComponent(addressComponents, "administrative_area_level_1");
|
|
221
|
+
const zipcode = findAddressComponent(addressComponents, "postal_code");
|
|
222
|
+
|
|
223
|
+
processedAddressComponents = {
|
|
224
|
+
address1: `${streetNumber} ${streetName}`.trim(),
|
|
225
|
+
city,
|
|
226
|
+
state,
|
|
227
|
+
zipcode,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (options?.includeCounty) {
|
|
231
|
+
const countyName = findAddressComponent(addressComponents, "administrative_area_level_2");
|
|
232
|
+
if (state && countyName) {
|
|
233
|
+
const countyCode = formattedCountyCode(state, countyName);
|
|
234
|
+
processedAddressComponents = {
|
|
235
|
+
...processedAddressComponents,
|
|
236
|
+
countyName,
|
|
237
|
+
countyCode,
|
|
238
|
+
};
|
|
239
|
+
} else {
|
|
240
|
+
processedAddressComponents = {
|
|
241
|
+
...processedAddressComponents,
|
|
242
|
+
countyName,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return processedAddressComponents;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Google does not provide a way to validate API keys, so we have to do it ourselves
|
|
250
|
+
export const isValidGoogleApiKey = (apiKey: string): boolean => {
|
|
251
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
252
|
+
console.warn("Google API key validation failed: key is not a string or is empty");
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
// Typical Google API keys are around 39 characters
|
|
256
|
+
if (apiKey.length < 30 || apiKey.length > 50) {
|
|
257
|
+
console.warn("Google API key validation failed: key is invalid length");
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
// Check the presence of alphanumeric characters and dashes
|
|
261
|
+
const apiKeyRegex = /^[A-Za-z0-9-_]+$/;
|
|
262
|
+
if (!apiKeyRegex.test(apiKey)) {
|
|
263
|
+
console.warn("Google API key validation failed: key contains invalid characters");
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export function formattedCountyCode(state: string, countyName: string): string {
|
|
270
|
+
// Remove whitespace and convert to lowercase for comparison
|
|
271
|
+
const stateKey = state.replace(/\s+/g, "").toLowerCase();
|
|
272
|
+
// Remove whitespace, periods, apostrophes, and dashes for comparison
|
|
273
|
+
const countyKey = countyName
|
|
274
|
+
.trim()
|
|
275
|
+
.toLowerCase()
|
|
276
|
+
.replace(/[\s.'-]/g, "");
|
|
277
|
+
|
|
278
|
+
const countyData = COUNTY_AND_COUNTY_EQUIVALENT_ENTITIES[stateKey]?.[countyKey];
|
|
279
|
+
if (!countyData) {
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return `${countyData.stateFP}${countyData.countyFP}`;
|
|
284
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React, {ReactElement, useEffect, useRef, useState} from "react";
|
|
2
|
+
|
|
3
|
+
import {AddressInterface, OnChangeCallback} from "./Common";
|
|
4
|
+
import {GOOGLE_PLACES_API_RESTRICTIONS} from "./Constants";
|
|
5
|
+
import {TextField} from "./TextField";
|
|
6
|
+
import {processAddressComponents} from "./Utilities";
|
|
7
|
+
|
|
8
|
+
const loadGooglePlacesScript = (googleMapsApiKey: string, callbackName: any): Promise<void> => {
|
|
9
|
+
return new Promise<void>((resolve, reject): undefined => {
|
|
10
|
+
if (window.google && window.google.maps && window.google.maps.places) {
|
|
11
|
+
resolve();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
(window as any)[callbackName] = (): void => resolve();
|
|
15
|
+
const script: HTMLScriptElement = document.createElement("script");
|
|
16
|
+
|
|
17
|
+
script.src = `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}&libraries=places&callback=${callbackName}`;
|
|
18
|
+
script.async = true;
|
|
19
|
+
script.defer = true;
|
|
20
|
+
script.onerror = (): any => reject(new Error("Google Maps script failed to load"));
|
|
21
|
+
document.head.appendChild(script);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const WebAddressAutocomplete = ({
|
|
26
|
+
disabled,
|
|
27
|
+
googleMapsApiKey,
|
|
28
|
+
includeCounty,
|
|
29
|
+
inputValue,
|
|
30
|
+
handleAddressChange,
|
|
31
|
+
handleAutoCompleteChange,
|
|
32
|
+
}: {
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
googleMapsApiKey?: string;
|
|
35
|
+
includeCounty?: boolean;
|
|
36
|
+
inputValue: string;
|
|
37
|
+
handleAddressChange: OnChangeCallback;
|
|
38
|
+
handleAutoCompleteChange: (value: AddressInterface) => void;
|
|
39
|
+
}): ReactElement => {
|
|
40
|
+
const [scriptLoaded, setScriptLoaded] = useState(true);
|
|
41
|
+
const autocompleteInputRef = useRef(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const callbackName = "initAutocomplete";
|
|
45
|
+
if (!googleMapsApiKey) {
|
|
46
|
+
setScriptLoaded(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
loadGooglePlacesScript(googleMapsApiKey, callbackName)
|
|
50
|
+
.then(() => {
|
|
51
|
+
const autocomplete = new window.google.maps.places.Autocomplete(
|
|
52
|
+
autocompleteInputRef.current,
|
|
53
|
+
{
|
|
54
|
+
componentRestrictions: {country: GOOGLE_PLACES_API_RESTRICTIONS.components.country},
|
|
55
|
+
fields: Object.values(GOOGLE_PLACES_API_RESTRICTIONS.fields),
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
autocomplete.addListener("place_changed", () => {
|
|
59
|
+
const place = autocomplete.getPlace();
|
|
60
|
+
const addressComponents = place?.address_components;
|
|
61
|
+
const formattedAddressObject = processAddressComponents(addressComponents, {
|
|
62
|
+
includeCounty,
|
|
63
|
+
});
|
|
64
|
+
handleAutoCompleteChange(formattedAddressObject);
|
|
65
|
+
});
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
console.warn(error);
|
|
69
|
+
setScriptLoaded(false);
|
|
70
|
+
});
|
|
71
|
+
// Cleanup
|
|
72
|
+
return () => {
|
|
73
|
+
(window as any)[callbackName] = null;
|
|
74
|
+
};
|
|
75
|
+
}, [googleMapsApiKey, includeCounty, handleAutoCompleteChange]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<TextField
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
inputRef={scriptLoaded ? (ref: any): void => (autocompleteInputRef.current = ref) : undefined}
|
|
81
|
+
label="Street Address"
|
|
82
|
+
placeholder="Enter an address"
|
|
83
|
+
type="text"
|
|
84
|
+
value={inputValue}
|
|
85
|
+
onChange={({value}): void => {
|
|
86
|
+
handleAddressChange({value});
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
};
|
package/src/index.tsx
CHANGED
|
@@ -30,6 +30,7 @@ export * from "./InfoTooltipButton";
|
|
|
30
30
|
export * from "./Link";
|
|
31
31
|
export * from "./Mask";
|
|
32
32
|
export * from "./Meta";
|
|
33
|
+
export * from "./MobileAddressAutoComplete";
|
|
33
34
|
export * from "./Modal";
|
|
34
35
|
|
|
35
36
|
export * from "./Page";
|
|
@@ -48,6 +49,7 @@ export * from "./Tooltip";
|
|
|
48
49
|
export * from "./Toast";
|
|
49
50
|
export * from "./UnifiedScreens";
|
|
50
51
|
export * from "./Unifier";
|
|
52
|
+
export * from "./UnifiedAddressAutoComplete";
|
|
51
53
|
export * from "./WithLabel";
|
|
52
54
|
export * from "./DecimalRangeActionSheet";
|
|
53
55
|
export * from "./HeightActionSheet";
|
|
@@ -63,6 +65,7 @@ export * from "./TableHeaderCell";
|
|
|
63
65
|
export * from "./TableRow";
|
|
64
66
|
export * from "./Theme";
|
|
65
67
|
export * from "./useStoredState";
|
|
68
|
+
export * from "./WebAddressAutocomplete";
|
|
66
69
|
|
|
67
70
|
// Lifted from react-native
|
|
68
71
|
type ImageRequireSource = number;
|
package/src/useStoredState.ts
CHANGED
|
@@ -5,8 +5,8 @@ import {Unifier} from "./Unifier";
|
|
|
5
5
|
export const useStoredState = <T>(
|
|
6
6
|
key: string,
|
|
7
7
|
initialValue?: T
|
|
8
|
-
): [T | undefined
|
|
9
|
-
const [state, setState] = useState<T | undefined
|
|
8
|
+
): [T | undefined, (value: T | undefined) => Promise<void>] => {
|
|
9
|
+
const [state, setState] = useState<T | undefined>(initialValue);
|
|
10
10
|
|
|
11
11
|
// Function to fetch data from AsyncStorage
|
|
12
12
|
const fetchData = useCallback(async (): Promise<T | undefined> => {
|
|
@@ -26,7 +26,7 @@ export const useStoredState = <T>(
|
|
|
26
26
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
27
|
}, []);
|
|
28
28
|
|
|
29
|
-
const setAsyncStorageState = async (newValue: T | undefined
|
|
29
|
+
const setAsyncStorageState = async (newValue: T | undefined): Promise<void> => {
|
|
30
30
|
try {
|
|
31
31
|
await Unifier.storage.setItem(key, newValue);
|
|
32
32
|
setState(newValue);
|