react-native-radar 3.7.5 → 3.7.6-beta.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.
@@ -18,7 +18,7 @@ android {
18
18
  minSdkVersion 16
19
19
  targetSdkVersion 31
20
20
  versionCode 1
21
- versionName '3.7.5'
21
+ versionName '3.7.6-beta.1'
22
22
  }
23
23
  lintOptions {
24
24
  abortOnError false
package/ios/RNRadar.h CHANGED
@@ -1,4 +1,5 @@
1
1
  #import <RadarSDK/RadarSDK.h>
2
+ #import <RadarSDK/RadarSettings.h>
2
3
  #import <React/RCTBridgeModule.h>
3
4
  #import <React/RCTEventEmitter.h>
4
5
 
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(host:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
143
+ resolve([RadarSettings host]);
144
+ }
145
+
146
+ RCT_EXPORT_METHOD(publishableKey:(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
- if (nearDict == nil || ![nearDict isKindOfClass:[NSDictionary class]]) {
733
- if (reject) {
734
- reject([Radar stringForStatus:RadarStatusErrorBadRequest], [Radar stringForStatus:RadarStatusErrorBadRequest], nil);
735
- }
736
-
737
- return;
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.host()
9
+ );
10
+
11
+ const getPublishableKey = () => (
12
+ NativeModules.RNRadar.publishableKey()
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 = require('./index.web').default;
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';
@@ -0,0 +1,67 @@
1
+ ## Example usage
2
+
3
+ We provide UI elements for autocomplete & maps to make building easy.
4
+
5
+ If you're using the Map element, you'll need to install [Maplibre React Native](https://github.com/maplibre/maplibre-react-native).
6
+ ```
7
+ npm install @maplibre/maplibre-react-native
8
+ ```
9
+
10
+ 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.
11
+
12
+ ### Adding a map
13
+
14
+ 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>`
15
+
16
+ ```
17
+ import {StyleSheet, View} from 'react-native';
18
+ import Radar, { Map } from 'react-native-radar';
19
+
20
+ export default function App() {
21
+ Radar.initialize('prj_place_your_own_token_here');
22
+
23
+ // A quirk of Map Libre requires us to set their deprecated access token to null
24
+ MapLibreGL.setAccessToken(null);
25
+
26
+ return (
27
+ <View style={styles.page}>
28
+ <Map />
29
+ </View>
30
+ );
31
+ }
32
+ ```
33
+
34
+ TODO: fill in details on customization, including using MapLibre elements
35
+
36
+ ### Adding an address autocomplete
37
+
38
+ Adding an address search autocomplete is similarly easy. Our `<Autocomplete>` element is comprised of a TextInput and Flatlist with the results.
39
+
40
+ 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.
41
+
42
+ ```
43
+ import Radar, { Map } from 'react-native-radar';
44
+
45
+ export default function App() {
46
+ const [location, setLocation] = useState(null);
47
+
48
+ Radar.trackOnce().then((result) => {
49
+ setLocation({
50
+ latitude: result.location.latitude,
51
+ longitude: result.location.longitude,
52
+ });
53
+ });
54
+
55
+ const onSelect (selectedAddress) => {
56
+ // Do something with the selected address
57
+ }
58
+
59
+ return (
60
+ <View style={style.page}>
61
+ <Autocomplete location={location} onSelect={onSelect} />
62
+ </View>
63
+ );
64
+ }
65
+ ```
66
+
67
+ TODO: fill in details of customization
@@ -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
Binary file
@@ -0,0 +1,5 @@
1
+ export const BACK_ICON = require('./back.png');
2
+ export const SEARCH_ICON = require('./search.png');
3
+ export const RADAR_LOGO = require('./radar-logo.png');
4
+ export const MARKER_ICON = require('./marker.png');
5
+ export const CLOSE_ICON = require('./close.png');
Binary file
package/js/ui/map.jsx ADDED
@@ -0,0 +1,66 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { StyleSheet, View, Image } from 'react-native';
3
+ import MapLibreGL from '@maplibre/maplibre-react-native';
4
+ import { getHost, getPublishableKey } from '../helpers';
5
+
6
+ const DEFAULT_STYLE = 'radar-default-v1';
7
+
8
+ const styles = StyleSheet.create({
9
+ container: {
10
+ flex: 1,
11
+ },
12
+ map: {
13
+ flex: 1,
14
+ },
15
+ logo: {
16
+ position: 'absolute',
17
+ bottom: -10,
18
+ left: 5,
19
+ width: 50,
20
+ height: 50,
21
+ resizeMode: 'contain',
22
+ },
23
+ });
24
+
25
+ const createStyleURL = async (style = DEFAULT_STYLE) => {
26
+ const host = await getHost();
27
+ const publishableKey = await getPublishableKey();
28
+ return `${host}/maps/styles/${style}?publishableKey=${publishableKey}`;
29
+ };
30
+
31
+ const RadarMap = ({ mapOptions, children }) => {
32
+ const [styleURL, setStyleURL] = useState(null);
33
+
34
+ useEffect(() => {
35
+ createStyleURL(mapOptions?.mapStyle || DEFAULT_STYLE).then((result) => {
36
+ setStyleURL(result);
37
+ });
38
+ }, [mapOptions]);
39
+
40
+ if (!styleURL) {
41
+ return null;
42
+ }
43
+
44
+ return (
45
+ <View style={styles.container}>
46
+ <MapLibreGL.MapView
47
+ style={styles.map}
48
+ pitchEnabled={false}
49
+ compassEnabled={false}
50
+ logoEnabled={false}
51
+ attributionEnabled
52
+ onRegionDidChange={mapOptions.onRegionDidChange ? mapOptions.onRegionDidChange : null}
53
+ styleURL={styleURL}
54
+ >
55
+ <MapLibreGL.UserLocation />
56
+ {children}
57
+ </MapLibreGL.MapView>
58
+ <Image
59
+ source={require('./map-logo.png')}
60
+ style={styles.logo}
61
+ />
62
+ </View>
63
+ );
64
+ };
65
+
66
+ export default RadarMap;
Binary file
Binary file
Binary file
@@ -0,0 +1,112 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export default 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
+ color: 'white',
35
+ },
36
+ closeIcon: {
37
+ marginRight: 10,
38
+ height: 18,
39
+ width: 18,
40
+ backgroundColor: 'white',
41
+ color: 'white',
42
+ },
43
+ input: {
44
+ flex: 1,
45
+ backgroundColor: 'white',
46
+ height: 40,
47
+ fontSize: 14,
48
+ paddingHorizontal: 8,
49
+ borderRadius: 5,
50
+ },
51
+ resultListWrapper: {
52
+ width: '100%',
53
+ marginBottom: 30,
54
+ backgroundColor: 'white',
55
+ borderRadius: 5,
56
+ paddingVertical: 8,
57
+ },
58
+ resultList: {
59
+ width: '100%',
60
+ },
61
+ resultItem: {
62
+ paddingRight: 16,
63
+ paddingVertical: 8,
64
+ paddingHorizontal: 16,
65
+ fontSize: 12,
66
+ backgroundColor: 'white',
67
+ pressedBackgroundColor: '#F6FAFC',
68
+ },
69
+ addressContainer: {
70
+ flexDirection: 'row',
71
+ alignItems: 'center',
72
+ },
73
+ pinIconContainer: {
74
+ width: 28,
75
+ marginRight: 8,
76
+ },
77
+ pinIcon: {
78
+ height: 26,
79
+ width: 26,
80
+ },
81
+ addressTextContainer: {
82
+ flex: 1,
83
+ },
84
+ addressText: {
85
+ fontSize: 14,
86
+ lineHeight: 24,
87
+ color: '#000',
88
+ fontWeight: '600',
89
+ },
90
+ addressSubtext: {
91
+ fontSize: 14,
92
+ color: '#5A6872',
93
+ },
94
+ footerContainer: {
95
+ flexDirection: 'row',
96
+ alignItems: 'center',
97
+ paddingVertical: 10,
98
+ marginRight: 16,
99
+ alignSelf: 'flex-end',
100
+ },
101
+ footerText: {
102
+ marginTop: 2,
103
+ marginRight: 4,
104
+ fontSize: 10,
105
+ color: '#5A6872',
106
+ },
107
+ logo: {
108
+ width: 50,
109
+ height: 15,
110
+ resizeMode: 'contain',
111
+ },
112
+ });
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.7.5",
6
+ "version": "3.7.6-beta.2",
7
7
  "main": "js/index.js",
8
8
  "files": [
9
9
  "android",
@@ -25,7 +25,8 @@
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"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@babel/core": "^7.2.2",