react-native-radar 3.8.0 → 3.8.2
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/android/build.gradle +2 -2
- package/android/src/main/java/io/radar/react/RNRadarModule.java +18 -0
- package/ios/Cartfile.resolved +1 -1
- package/ios/RNRadar.h +1 -0
- package/ios/RNRadar.m +15 -10
- package/js/helpers.js +15 -0
- package/js/index.js +7 -4
- package/js/ui/README.md +157 -0
- package/js/ui/autocomplete.jsx +312 -0
- package/js/ui/back.png +0 -0
- package/js/ui/close.png +0 -0
- package/js/ui/images.js +5 -0
- package/js/ui/map-logo.png +0 -0
- package/js/ui/map.jsx +105 -0
- package/js/ui/marker.png +0 -0
- package/js/ui/radar-logo.png +0 -0
- package/js/ui/search.png +0 -0
- package/js/ui/styles.js +144 -0
- package/package.json +8 -2
- package/react-native-radar.podspec +1 -1
package/android/build.gradle
CHANGED
|
@@ -18,7 +18,7 @@ android {
|
|
|
18
18
|
minSdkVersion 16
|
|
19
19
|
targetSdkVersion 31
|
|
20
20
|
versionCode 1
|
|
21
|
-
versionName '3.8.
|
|
21
|
+
versionName '3.8.2'
|
|
22
22
|
}
|
|
23
23
|
lintOptions {
|
|
24
24
|
abortOnError false
|
|
@@ -45,5 +45,5 @@ repositories {
|
|
|
45
45
|
|
|
46
46
|
dependencies {
|
|
47
47
|
api 'com.facebook.react:react-native:+'
|
|
48
|
-
api 'io.radar:sdk:3.8.
|
|
48
|
+
api 'io.radar:sdk:3.8.5'
|
|
49
49
|
}
|
|
@@ -148,6 +148,24 @@ public class RNRadarModule extends ReactContextBaseJavaModule implements Permiss
|
|
|
148
148
|
Radar.setAnonymousTrackingEnabled(enabled);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
@ReactMethod
|
|
152
|
+
public void getHost(final Promise promise) {
|
|
153
|
+
if (promise == null) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
promise.resolve(Radar.getHost());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@ReactMethod
|
|
161
|
+
public void getPublishableKey(final Promise promise) {
|
|
162
|
+
if (promise == null) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
promise.resolve(Radar.getPublishableKey());
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
@ReactMethod
|
|
152
170
|
public void getPermissionsStatus(final Promise promise) {
|
|
153
171
|
if (promise == null) {
|
package/ios/Cartfile.resolved
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
github "radarlabs/radar-sdk-ios" "3.8.
|
|
1
|
+
github "radarlabs/radar-sdk-ios" "3.8.4"
|
package/ios/RNRadar.h
CHANGED
package/ios/RNRadar.m
CHANGED
|
@@ -139,6 +139,14 @@ RCT_EXPORT_METHOD(setAnonymousTrackingEnabled:(BOOL)enabled) {
|
|
|
139
139
|
[Radar setAnonymousTrackingEnabled:enabled];
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
RCT_EXPORT_METHOD(getHost:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
143
|
+
resolve([RadarSettings host]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
RCT_EXPORT_METHOD(getPublishableKey:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
147
|
+
resolve([RadarSettings publishableKey]);
|
|
148
|
+
}
|
|
149
|
+
|
|
142
150
|
RCT_REMAP_METHOD(getPermissionsStatus, getPermissionsStatusWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
|
143
151
|
CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
|
|
144
152
|
NSString *statusStr;
|
|
@@ -729,19 +737,16 @@ RCT_EXPORT_METHOD(autocomplete:(NSDictionary *)optionsDict resolve:(RCTPromiseRe
|
|
|
729
737
|
}
|
|
730
738
|
|
|
731
739
|
NSDictionary *nearDict = optionsDict[@"near"];
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
740
|
+
CLLocation *near = nil;
|
|
741
|
+
if (nearDict && [nearDict isKindOfClass:[NSDictionary class]]) {
|
|
742
|
+
double latitude = [RCTConvert double:nearDict[@"latitude"]];
|
|
743
|
+
double longitude = [RCTConvert double:nearDict[@"longitude"]];
|
|
744
|
+
NSDate *timestamp = [NSDate new];
|
|
745
|
+
near = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
|
|
738
746
|
}
|
|
739
747
|
|
|
748
|
+
|
|
740
749
|
NSString *query = optionsDict[@"query"];
|
|
741
|
-
double latitude = [RCTConvert double:nearDict[@"latitude"]];
|
|
742
|
-
double longitude = [RCTConvert double:nearDict[@"longitude"]];
|
|
743
|
-
NSDate *timestamp = [NSDate new];
|
|
744
|
-
CLLocation *near = [[CLLocation alloc] initWithCoordinate:CLLocationCoordinate2DMake(latitude, longitude) altitude:-1 horizontalAccuracy:5 verticalAccuracy:-1 timestamp:timestamp];
|
|
745
750
|
NSNumber *limitNumber = optionsDict[@"limit"];
|
|
746
751
|
int limit;
|
|
747
752
|
if (limitNumber != nil && [limitNumber isKindOfClass:[NSNumber class]]) {
|
package/js/helpers.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
if (!NativeModules.RNRadar && (Platform.OS === 'ios' || Platform.OS === 'android')) {
|
|
4
|
+
throw new Error('NativeModules.RNRadar is undefined');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const getHost = () => (
|
|
8
|
+
NativeModules.RNRadar.getHost()
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const getPublishableKey = () => (
|
|
12
|
+
NativeModules.RNRadar.getPublishableKey()
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export { getHost, getPublishableKey };
|
package/js/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
|
|
3
3
|
let module = {};
|
|
4
|
-
if (Platform.OS === 'web')
|
|
5
|
-
module =
|
|
6
|
-
else
|
|
4
|
+
if (Platform.OS === 'web') {
|
|
5
|
+
module = require('./index.web').default;
|
|
6
|
+
} else {
|
|
7
7
|
module = require('./index.native').default;
|
|
8
|
-
|
|
8
|
+
}
|
|
9
9
|
export default module;
|
|
10
|
+
|
|
11
|
+
export { default as Autocomplete } from './ui/autocomplete';
|
|
12
|
+
export { default as Map } from './ui/map';
|
package/js/ui/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
## Example usage
|
|
2
|
+
|
|
3
|
+
We provide UI elements for autocomplete & maps to make building easy.
|
|
4
|
+
|
|
5
|
+
### Adding an address autocomplete
|
|
6
|
+
|
|
7
|
+
Adding an address search autocomplete is straightforward. Our `<Autocomplete>` element is comprised of a TextInput and Flatlist with the results.
|
|
8
|
+
|
|
9
|
+
The example below provides optional `location` and `onSelect` props to the component. Providing a location will improve autocomplete result quality. Without it, the API utilizes the IP address location to rank results.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
import { View } from 'react-native';
|
|
13
|
+
import { useState, useEffect } from 'react';
|
|
14
|
+
import Radar, { Autocomplete } from 'react-native-radar';
|
|
15
|
+
|
|
16
|
+
export default function App() {
|
|
17
|
+
const [location, setLocation] = useState(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
Radar.initialize('prj_live_pk_...');
|
|
21
|
+
|
|
22
|
+
Radar.trackOnce().then((result) => {
|
|
23
|
+
setLocation({
|
|
24
|
+
latitude: result.location.latitude,
|
|
25
|
+
longitude: result.location.longitude,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const onSelect = (selectedAddress) => {
|
|
31
|
+
// Do something with the selected address
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={{ marginTop: 50}}>
|
|
36
|
+
<Autocomplete location={location} onSelect={onSelect} />
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Adding a map
|
|
44
|
+
|
|
45
|
+
If you're using the Map element, you'll need to install [Maplibre React Native](https://github.com/maplibre/maplibre-react-native), which `react-native-radar` has an optional peer dependency.
|
|
46
|
+
```
|
|
47
|
+
npm install @maplibre/maplibre-react-native
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then make sure to complete required [platform specific installation steps](https://github.com/maplibre/maplibre-react-native/blob/main/docs/GettingStarted.md#review-platform-specific-info) as well.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
We've taken care of linking the map tile server to the map, so all you need to do is make sure you've initialized the Radar SDK and use `<Map>`. Here's a minimal example:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
import {View} from 'react-native';
|
|
57
|
+
import Radar, { Map } from 'react-native-radar';
|
|
58
|
+
import MapLibreGL from '@maplibre/maplibre-react-native';
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// A quirk of Map Libre requires us to set their deprecated access token to null
|
|
62
|
+
MapLibreGL.setAccessToken(null);
|
|
63
|
+
|
|
64
|
+
export default function App() {
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
Radar.initialize('prj_live_pk_...');
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<View style={{ width: '100%', height: '95%'}}>
|
|
72
|
+
<Map />
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
And here's how you might add a custom pin to the map and control the camera:
|
|
79
|
+
```
|
|
80
|
+
// ... rest of your file
|
|
81
|
+
|
|
82
|
+
const [cameraConfig, setCameraConfig] = useState({
|
|
83
|
+
triggerKey: Date.now(),
|
|
84
|
+
centerCoordinate: [-73.9911, 40.7342],
|
|
85
|
+
animationMode: 'flyTo',
|
|
86
|
+
animationDuration: 600,
|
|
87
|
+
zoomLevel: 12,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const onRegionDidChange = (event) => {
|
|
91
|
+
// handle region change
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mapOptions = {
|
|
95
|
+
onRegionDidChange: onRegionDidChange,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const onSelect = (selectedAddress) => {
|
|
99
|
+
// Do something with the selected address
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pointsCollection = {
|
|
103
|
+
type: "FeatureCollection",
|
|
104
|
+
features: [{
|
|
105
|
+
type: "Feature",
|
|
106
|
+
properties: {
|
|
107
|
+
_id: '123',
|
|
108
|
+
},
|
|
109
|
+
geometry: {
|
|
110
|
+
type: "Point",
|
|
111
|
+
coordinates: [-73.9911, 40.7342]
|
|
112
|
+
}
|
|
113
|
+
}]
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onPressIcon = (event) => {
|
|
117
|
+
// do something with the symbol, such as scrolling to the geofence
|
|
118
|
+
// associated with the icon in the list
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View style={{ width: '100%', marginTop: '10%', height: '90%'}}>
|
|
123
|
+
<Map mapOptions={mapOptions}>
|
|
124
|
+
<MapLibreGL.Camera
|
|
125
|
+
{...cameraConfig}
|
|
126
|
+
/>
|
|
127
|
+
<MapLibreGL.Images
|
|
128
|
+
images={{
|
|
129
|
+
icon: require('./assets/marker.png'),
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
<MapLibreGL.ShapeSource
|
|
133
|
+
id="points"
|
|
134
|
+
shape={pointsCollection}
|
|
135
|
+
onPress={onPressIcon}
|
|
136
|
+
>
|
|
137
|
+
|
|
138
|
+
<MapLibreGL.SymbolLayer
|
|
139
|
+
id="symbol"
|
|
140
|
+
style={{
|
|
141
|
+
iconImage: 'icon',
|
|
142
|
+
iconSize: [
|
|
143
|
+
'interpolate',
|
|
144
|
+
['linear'],
|
|
145
|
+
['zoom'],
|
|
146
|
+
0, 0.2, // Adjust the icon size for zoom level 0
|
|
147
|
+
12, 0.4, // Adjust the icon size for zoom level 12
|
|
148
|
+
22, 0.8, // Adjust the icon size for zoom level 22
|
|
149
|
+
],
|
|
150
|
+
iconAllowOverlap: true,
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</MapLibreGL.ShapeSource>
|
|
154
|
+
</Map>
|
|
155
|
+
</View>
|
|
156
|
+
);
|
|
157
|
+
```
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// Autocomplete.js
|
|
2
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
View,
|
|
5
|
+
TextInput,
|
|
6
|
+
FlatList,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
Text,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
Image,
|
|
11
|
+
Modal,
|
|
12
|
+
KeyboardAvoidingView,
|
|
13
|
+
Animated,
|
|
14
|
+
Dimensions,
|
|
15
|
+
Easing,
|
|
16
|
+
Keyboard,
|
|
17
|
+
SafeAreaView,
|
|
18
|
+
Pressable,
|
|
19
|
+
} from 'react-native';
|
|
20
|
+
import Radar from '../index.native';
|
|
21
|
+
import {
|
|
22
|
+
BACK_ICON,
|
|
23
|
+
SEARCH_ICON,
|
|
24
|
+
RADAR_LOGO,
|
|
25
|
+
MARKER_ICON,
|
|
26
|
+
CLOSE_ICON,
|
|
27
|
+
} from "./images";
|
|
28
|
+
import { default as defaultStyles } from './styles';
|
|
29
|
+
|
|
30
|
+
const defaultAutocompleteOptions = {
|
|
31
|
+
debounceMS: 200,
|
|
32
|
+
threshold: 3,
|
|
33
|
+
limit: 8,
|
|
34
|
+
placeholder: "Search address",
|
|
35
|
+
showPin: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const autocompleteUI = ({ options = {}, onSelect, location, style = {} }) => {
|
|
39
|
+
const [query, setQuery] = useState("");
|
|
40
|
+
const [results, setResults] = useState([]);
|
|
41
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
42
|
+
const animationValue = useRef(new Animated.Value(0)).current; // animation value
|
|
43
|
+
const timerRef = useRef(null);
|
|
44
|
+
const textInputRef = useRef(null);
|
|
45
|
+
|
|
46
|
+
const config = { ...defaultAutocompleteOptions, ...options };
|
|
47
|
+
|
|
48
|
+
const fetchResults = useCallback(
|
|
49
|
+
async (searchQuery) => {
|
|
50
|
+
if (searchQuery.length < config.threshold) return;
|
|
51
|
+
|
|
52
|
+
const params = { query: searchQuery, limit: config.limit };
|
|
53
|
+
|
|
54
|
+
if (location && location.latitude && location.longitude) {
|
|
55
|
+
params.near = location;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await Radar.autocomplete(params);
|
|
60
|
+
setResults(result.addresses);
|
|
61
|
+
setIsOpen(true);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (config.onError && typeof config.onError === "function") {
|
|
64
|
+
config.onError(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[config]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleInput = useCallback(
|
|
72
|
+
(text) => {
|
|
73
|
+
setQuery(text);
|
|
74
|
+
|
|
75
|
+
// Clear the existing timer
|
|
76
|
+
if (timerRef.current) {
|
|
77
|
+
clearTimeout(timerRef.current);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (text.length < config.threshold) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Set the new timer
|
|
85
|
+
timerRef.current = setTimeout(() => {
|
|
86
|
+
fetchResults(text);
|
|
87
|
+
}, config.debounceMS);
|
|
88
|
+
},
|
|
89
|
+
[config, fetchResults]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const handleSelect = (item) => {
|
|
93
|
+
setQuery(item.formattedAddress);
|
|
94
|
+
setIsOpen(false);
|
|
95
|
+
|
|
96
|
+
if (typeof onSelect === "function") {
|
|
97
|
+
onSelect(item);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const renderFooter = () => {
|
|
102
|
+
if (results.length === 0) return null;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<View style={styles.footerContainer}>
|
|
106
|
+
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
|
107
|
+
<Text style={styles.footerText}>Powered by</Text>
|
|
108
|
+
<Image
|
|
109
|
+
source={RADAR_LOGO}
|
|
110
|
+
resizeMode="contain"
|
|
111
|
+
style={defaultStyles.logo}
|
|
112
|
+
/>
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const renderItem = ({ item }) => (
|
|
119
|
+
<Pressable
|
|
120
|
+
style={({ pressed }) => [
|
|
121
|
+
{
|
|
122
|
+
...styles.resultItem,
|
|
123
|
+
backgroundColor: pressed
|
|
124
|
+
? styles.resultItem.pressedBackgroundColor
|
|
125
|
+
: styles.resultItem.backgroundColor,
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
onPress={() => handleSelect(item)}
|
|
129
|
+
>
|
|
130
|
+
<View style={styles.addressContainer}>
|
|
131
|
+
<View style={styles.pinIconContainer}>
|
|
132
|
+
{config.showPin ? (
|
|
133
|
+
<Image source={MARKER_ICON} style={styles.pinIcon} />
|
|
134
|
+
) : null}
|
|
135
|
+
</View>
|
|
136
|
+
<View style={styles.addressTextContainer}>
|
|
137
|
+
<Text numberOfLines={1} style={styles.addressText}>
|
|
138
|
+
{item.addressLabel || item?.placeLabel}
|
|
139
|
+
</Text>
|
|
140
|
+
{item?.formattedAddress.length > 0 && (
|
|
141
|
+
<Text numberOfLines={1} style={styles.addressSubtext}>
|
|
142
|
+
{item?.formattedAddress?.replace(
|
|
143
|
+
`${item?.addressLabel || item?.placeLabel}, `,
|
|
144
|
+
""
|
|
145
|
+
)}
|
|
146
|
+
</Text>
|
|
147
|
+
)}
|
|
148
|
+
</View>
|
|
149
|
+
</View>
|
|
150
|
+
</Pressable>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const styles = {
|
|
154
|
+
...defaultStyles,
|
|
155
|
+
container: StyleSheet.compose(defaultStyles.container, style.container),
|
|
156
|
+
input: StyleSheet.compose(defaultStyles.input, style.input),
|
|
157
|
+
inputContainer: StyleSheet.compose(
|
|
158
|
+
defaultStyles.inputContainer,
|
|
159
|
+
style.inputContainer
|
|
160
|
+
),
|
|
161
|
+
modalInputContainer: StyleSheet.compose(
|
|
162
|
+
defaultStyles.modalInputContainer,
|
|
163
|
+
style.modalInputContainer
|
|
164
|
+
),
|
|
165
|
+
resultList: StyleSheet.compose(defaultStyles.resultList, style.resultList),
|
|
166
|
+
resultItem: StyleSheet.compose(defaultStyles.resultItem, style.resultItem),
|
|
167
|
+
addressContainer: StyleSheet.compose(
|
|
168
|
+
defaultStyles.addressContainer,
|
|
169
|
+
style.addressContainer
|
|
170
|
+
),
|
|
171
|
+
pinIconContainer: StyleSheet.compose(
|
|
172
|
+
defaultStyles.pinIconContainer,
|
|
173
|
+
style.pinIconContainer
|
|
174
|
+
),
|
|
175
|
+
pinIcon: StyleSheet.compose(defaultStyles.pinIcon, style.pinIcon),
|
|
176
|
+
addressTextContainer: StyleSheet.compose(
|
|
177
|
+
defaultStyles.addressTextContainer,
|
|
178
|
+
style.addressTextContainer
|
|
179
|
+
),
|
|
180
|
+
addressText: StyleSheet.compose(
|
|
181
|
+
defaultStyles.addressText,
|
|
182
|
+
style.addressText
|
|
183
|
+
),
|
|
184
|
+
addressSubtext: StyleSheet.compose(
|
|
185
|
+
defaultStyles.addressSubtext,
|
|
186
|
+
style.addressSubtext
|
|
187
|
+
),
|
|
188
|
+
footerContainer: StyleSheet.compose(
|
|
189
|
+
defaultStyles.footerContainer,
|
|
190
|
+
style.footerContainer
|
|
191
|
+
),
|
|
192
|
+
footerText: StyleSheet.compose(defaultStyles.footerText, style.footerText),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
Animated.timing(animationValue, {
|
|
197
|
+
toValue: isOpen ? 1 : 0,
|
|
198
|
+
duration: 300,
|
|
199
|
+
easing: Easing.inOut(Easing.ease),
|
|
200
|
+
useNativeDriver: false,
|
|
201
|
+
}).start();
|
|
202
|
+
}, [isOpen]);
|
|
203
|
+
|
|
204
|
+
const screenHeight = Dimensions.get("window").height;
|
|
205
|
+
|
|
206
|
+
const inputHeight = animationValue.interpolate({
|
|
207
|
+
inputRange: [0, 1],
|
|
208
|
+
outputRange: [40, screenHeight],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const modalOpacity = animationValue.interpolate({
|
|
212
|
+
inputRange: [0, 0.5, 1],
|
|
213
|
+
outputRange: [0, 0, 1],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<View style={styles.container}>
|
|
218
|
+
<Animated.View style={{ height: inputHeight }}>
|
|
219
|
+
<TouchableOpacity
|
|
220
|
+
style={styles.inputContainer}
|
|
221
|
+
onPress={() => {
|
|
222
|
+
setIsOpen(true);
|
|
223
|
+
// Set the focus on the other textinput after it opens
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
textInputRef.current.focus();
|
|
226
|
+
}, 100);
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<Image source={SEARCH_ICON} style={styles.inputIcon} />
|
|
230
|
+
<TextInput
|
|
231
|
+
style={styles.input}
|
|
232
|
+
onFocus={() => {
|
|
233
|
+
setIsOpen(true);
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
textInputRef.current.focus();
|
|
236
|
+
}, 100);
|
|
237
|
+
}}
|
|
238
|
+
value={query}
|
|
239
|
+
returnKeyType="done"
|
|
240
|
+
placeholder={config.placeholder}
|
|
241
|
+
placeholderTextColor="#acbdc8"
|
|
242
|
+
/>
|
|
243
|
+
</TouchableOpacity>
|
|
244
|
+
</Animated.View>
|
|
245
|
+
<Modal
|
|
246
|
+
animationType="slide"
|
|
247
|
+
transparent={false}
|
|
248
|
+
visible={isOpen}
|
|
249
|
+
onRequestClose={() => setIsOpen(false)}
|
|
250
|
+
>
|
|
251
|
+
<Animated.View style={{ flex: 1, opacity: modalOpacity }}>
|
|
252
|
+
<SafeAreaView>
|
|
253
|
+
<KeyboardAvoidingView
|
|
254
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
255
|
+
keyboardVerticalOffset={50}
|
|
256
|
+
style={styles.container}
|
|
257
|
+
>
|
|
258
|
+
<View style={styles.modalInputContainer}>
|
|
259
|
+
<TouchableOpacity
|
|
260
|
+
onPress={() => {
|
|
261
|
+
setIsOpen(false);
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<Image source={BACK_ICON} style={styles.inputIcon} />
|
|
265
|
+
</TouchableOpacity>
|
|
266
|
+
<TextInput
|
|
267
|
+
ref={textInputRef}
|
|
268
|
+
style={styles.input}
|
|
269
|
+
onChangeText={handleInput}
|
|
270
|
+
value={query}
|
|
271
|
+
placeholder={config.placeholder}
|
|
272
|
+
returnKeyType="done"
|
|
273
|
+
onSubmitEditing={() => {
|
|
274
|
+
setIsOpen(false);
|
|
275
|
+
}}
|
|
276
|
+
placeholderTextColor="#acbdc8"
|
|
277
|
+
/>
|
|
278
|
+
<TouchableOpacity
|
|
279
|
+
onPress={() => {
|
|
280
|
+
setQuery("");
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
<Image source={CLOSE_ICON} style={styles.closeIcon} />
|
|
284
|
+
</TouchableOpacity>
|
|
285
|
+
</View>
|
|
286
|
+
{results.length > 0 && (
|
|
287
|
+
<View style={styles.resultListWrapper}>
|
|
288
|
+
<FlatList
|
|
289
|
+
style={styles.resultList}
|
|
290
|
+
data={results}
|
|
291
|
+
onScroll={() => {
|
|
292
|
+
textInputRef.current.blur();
|
|
293
|
+
Keyboard.dismiss();
|
|
294
|
+
}}
|
|
295
|
+
keyboardShouldPersistTaps="handled"
|
|
296
|
+
renderItem={renderItem}
|
|
297
|
+
keyExtractor={(item) =>
|
|
298
|
+
item.formattedAddress + item.postalCode
|
|
299
|
+
}
|
|
300
|
+
/>
|
|
301
|
+
{renderFooter()}
|
|
302
|
+
</View>
|
|
303
|
+
)}
|
|
304
|
+
</KeyboardAvoidingView>
|
|
305
|
+
</SafeAreaView>
|
|
306
|
+
</Animated.View>
|
|
307
|
+
</Modal>
|
|
308
|
+
</View>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export default autocompleteUI;
|
package/js/ui/back.png
ADDED
|
Binary file
|
package/js/ui/close.png
ADDED
|
Binary file
|
package/js/ui/images.js
ADDED
|
Binary file
|
package/js/ui/map.jsx
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { View, Image } from 'react-native';
|
|
3
|
+
import Radar from '../index.native';
|
|
4
|
+
import { getHost, getPublishableKey } from '../helpers';
|
|
5
|
+
import styles from './styles';
|
|
6
|
+
|
|
7
|
+
let MapLibreGL;
|
|
8
|
+
try {
|
|
9
|
+
MapLibreGL = require('@maplibre/maplibre-react-native');
|
|
10
|
+
} catch (e) {
|
|
11
|
+
MapLibreGL = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_STYLE = 'radar-default-v1';
|
|
15
|
+
|
|
16
|
+
const createStyleURL = async (style = DEFAULT_STYLE) => {
|
|
17
|
+
const host = await getHost();
|
|
18
|
+
const publishableKey = await getPublishableKey();
|
|
19
|
+
return `${host}/maps/styles/${style}?publishableKey=${publishableKey}`;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const RadarMap = ({ mapOptions, children }) => {
|
|
23
|
+
const [styleURL, setStyleURL] = useState(null);
|
|
24
|
+
const [userLocation, setUserLocation] = useState(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
createStyleURL(mapOptions?.mapStyle || DEFAULT_STYLE).then((result) => {
|
|
28
|
+
setStyleURL(result);
|
|
29
|
+
});
|
|
30
|
+
}, [mapOptions]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
Radar.getLocation().then((result) => {
|
|
34
|
+
setUserLocation({
|
|
35
|
+
latitude: result.location.latitude,
|
|
36
|
+
longitude: result.location.longitude,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}, [mapOptions]);
|
|
40
|
+
|
|
41
|
+
if (!styleURL) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!MapLibreGL) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const geoJSONUserLocation = {
|
|
50
|
+
type: 'FeatureCollection',
|
|
51
|
+
features: userLocation?.longitude !== undefined ? [
|
|
52
|
+
{
|
|
53
|
+
type: 'Feature',
|
|
54
|
+
geometry: {
|
|
55
|
+
type: 'Point',
|
|
56
|
+
coordinates: [userLocation.longitude, userLocation.latitude],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
] : [],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const userLocationMapIndicator = (
|
|
63
|
+
<MapLibreGL.ShapeSource
|
|
64
|
+
id="user-location"
|
|
65
|
+
shape={geoJSONUserLocation}
|
|
66
|
+
>
|
|
67
|
+
<MapLibreGL.CircleLayer
|
|
68
|
+
id="user-location-inner"
|
|
69
|
+
style={styles.userLocation.pulse}
|
|
70
|
+
/>
|
|
71
|
+
<MapLibreGL.CircleLayer
|
|
72
|
+
id="user-location-middle"
|
|
73
|
+
style={styles.userLocation.background}
|
|
74
|
+
/>
|
|
75
|
+
<MapLibreGL.CircleLayer
|
|
76
|
+
id="user-location-outer"
|
|
77
|
+
style={styles.userLocation.foreground}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
</MapLibreGL.ShapeSource>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<View style={styles.mapContainer}>
|
|
85
|
+
<MapLibreGL.MapView
|
|
86
|
+
style={styles.map}
|
|
87
|
+
pitchEnabled={false}
|
|
88
|
+
compassEnabled={false}
|
|
89
|
+
logoEnabled={false}
|
|
90
|
+
attributionEnabled
|
|
91
|
+
onRegionDidChange={mapOptions?.onRegionDidChange ? mapOptions.onRegionDidChange : null}
|
|
92
|
+
styleURL={styleURL}
|
|
93
|
+
>
|
|
94
|
+
{userLocationMapIndicator}
|
|
95
|
+
{children}
|
|
96
|
+
</MapLibreGL.MapView>
|
|
97
|
+
<Image
|
|
98
|
+
source={require('./map-logo.png')}
|
|
99
|
+
style={styles.mapLogo}
|
|
100
|
+
/>
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default RadarMap;
|
package/js/ui/marker.png
ADDED
|
Binary file
|
|
Binary file
|
package/js/ui/search.png
ADDED
|
Binary file
|
package/js/ui/styles.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
const styles = StyleSheet.create({
|
|
4
|
+
container: {
|
|
5
|
+
width: '100%',
|
|
6
|
+
height: '100%',
|
|
7
|
+
alignItems: 'center',
|
|
8
|
+
paddingTop: 8,
|
|
9
|
+
},
|
|
10
|
+
inputContainer: {
|
|
11
|
+
flexDirection: 'row',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
marginHorizontal: 16,
|
|
14
|
+
backgroundColor: 'white',
|
|
15
|
+
borderRadius: 5,
|
|
16
|
+
borderColor: '#DBE5EB',
|
|
17
|
+
borderWidth: 1,
|
|
18
|
+
width: '95%', // only difference between this and the modalInputContainer
|
|
19
|
+
},
|
|
20
|
+
modalInputContainer: {
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
marginHorizontal: 16,
|
|
24
|
+
backgroundColor: 'white',
|
|
25
|
+
borderRadius: 5,
|
|
26
|
+
borderColor: '#DBE5EB',
|
|
27
|
+
borderWidth: 1,
|
|
28
|
+
},
|
|
29
|
+
inputIcon: {
|
|
30
|
+
marginLeft: 10,
|
|
31
|
+
height: 18,
|
|
32
|
+
width: 18,
|
|
33
|
+
backgroundColor: 'white',
|
|
34
|
+
},
|
|
35
|
+
closeIcon: {
|
|
36
|
+
marginRight: 10,
|
|
37
|
+
height: 18,
|
|
38
|
+
width: 18,
|
|
39
|
+
backgroundColor: 'white',
|
|
40
|
+
},
|
|
41
|
+
input: {
|
|
42
|
+
flex: 1,
|
|
43
|
+
backgroundColor: 'white',
|
|
44
|
+
height: 40,
|
|
45
|
+
fontSize: 14,
|
|
46
|
+
paddingHorizontal: 8,
|
|
47
|
+
borderRadius: 5,
|
|
48
|
+
},
|
|
49
|
+
resultListWrapper: {
|
|
50
|
+
width: '100%',
|
|
51
|
+
marginBottom: 30,
|
|
52
|
+
backgroundColor: 'white',
|
|
53
|
+
borderRadius: 5,
|
|
54
|
+
paddingVertical: 8,
|
|
55
|
+
},
|
|
56
|
+
resultList: {
|
|
57
|
+
width: '100%',
|
|
58
|
+
},
|
|
59
|
+
resultItem: {
|
|
60
|
+
paddingRight: 16,
|
|
61
|
+
paddingVertical: 8,
|
|
62
|
+
paddingHorizontal: 16,
|
|
63
|
+
fontSize: 12,
|
|
64
|
+
backgroundColor: 'white',
|
|
65
|
+
pressedBackgroundColor: '#F6FAFC',
|
|
66
|
+
},
|
|
67
|
+
addressContainer: {
|
|
68
|
+
flexDirection: 'row',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
},
|
|
71
|
+
pinIconContainer: {
|
|
72
|
+
width: 28,
|
|
73
|
+
marginRight: 8,
|
|
74
|
+
},
|
|
75
|
+
pinIcon: {
|
|
76
|
+
height: 26,
|
|
77
|
+
width: 26,
|
|
78
|
+
},
|
|
79
|
+
addressTextContainer: {
|
|
80
|
+
flex: 1,
|
|
81
|
+
},
|
|
82
|
+
addressText: {
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
lineHeight: 24,
|
|
85
|
+
color: '#000',
|
|
86
|
+
fontWeight: '600',
|
|
87
|
+
},
|
|
88
|
+
addressSubtext: {
|
|
89
|
+
fontSize: 14,
|
|
90
|
+
color: '#5A6872',
|
|
91
|
+
},
|
|
92
|
+
footerContainer: {
|
|
93
|
+
flexDirection: 'row',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
paddingVertical: 10,
|
|
96
|
+
marginRight: 16,
|
|
97
|
+
alignSelf: 'flex-end',
|
|
98
|
+
},
|
|
99
|
+
footerText: {
|
|
100
|
+
marginTop: 2,
|
|
101
|
+
marginRight: 4,
|
|
102
|
+
fontSize: 10,
|
|
103
|
+
color: '#5A6872',
|
|
104
|
+
},
|
|
105
|
+
logo: {
|
|
106
|
+
width: 50,
|
|
107
|
+
height: 15,
|
|
108
|
+
resizeMode: 'contain',
|
|
109
|
+
},
|
|
110
|
+
mapContainer: {
|
|
111
|
+
flex: 1,
|
|
112
|
+
},
|
|
113
|
+
map: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
},
|
|
116
|
+
mapLogo: {
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
bottom: -10,
|
|
119
|
+
left: 5,
|
|
120
|
+
width: 50,
|
|
121
|
+
height: 50,
|
|
122
|
+
resizeMode: 'contain',
|
|
123
|
+
},
|
|
124
|
+
userLocation: {
|
|
125
|
+
pulse: {
|
|
126
|
+
circleRadius: 15,
|
|
127
|
+
circleColor: '#000257',
|
|
128
|
+
circleOpacity: 0.2,
|
|
129
|
+
circlePitchAlignment: 'map',
|
|
130
|
+
},
|
|
131
|
+
background: {
|
|
132
|
+
circleRadius: 9,
|
|
133
|
+
circleColor: '#fff',
|
|
134
|
+
circlePitchAlignment: 'map',
|
|
135
|
+
},
|
|
136
|
+
foreground: {
|
|
137
|
+
circleRadius: 6,
|
|
138
|
+
circleColor: '#000257',
|
|
139
|
+
circlePitchAlignment: 'map',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export default styles;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "React Native module for Radar, the leading geofencing and location tracking platform",
|
|
4
4
|
"homepage": "https://radar.com",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"version": "3.8.
|
|
6
|
+
"version": "3.8.2",
|
|
7
7
|
"main": "js/index.js",
|
|
8
8
|
"files": [
|
|
9
9
|
"android",
|
|
@@ -25,7 +25,13 @@
|
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": ">= 16.8.6",
|
|
28
|
-
"react-native": ">= 0.60.0"
|
|
28
|
+
"react-native": ">= 0.60.0",
|
|
29
|
+
"@maplibre/maplibre-react-native": "^9.0.1"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"@maplibre/maplibre-react-native": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
29
35
|
},
|
|
30
36
|
"devDependencies": {
|
|
31
37
|
"@babel/core": "^7.2.2",
|