react-native-map-link 2.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Includable
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ ![React Native Map Link](https://lowcdn.com/2x/8f2/3ab63c0fe3f9-00fb302c20/banner.svg)
2
+
3
+ [![GitHub release](https://img.shields.io/npm/v/react-native-map-link.svg)](https://www.npmjs.com/package/react-native-map-link)
4
+ [![npm](https://img.shields.io/npm/dm/react-native-map-link.svg)](https://www.npmjs.com/package/react-native-map-link)
5
+ [![GitHub license](https://img.shields.io/github/license/flexible-agency/react-native-map-link.svg)](https://github.com/flexible-agency/react-native-map-link/blob/master/LICENSE)
6
+
7
+ ---
8
+
9
+ An easy way to open a location in a map app of the user's choice, based on the apps they have installed
10
+ on their device. The app supports Apple Maps, Google Maps, Citymapper, Uber, and a dozen other apps.
11
+
12
+ <details>
13
+ <summary>Full list of supported apps</summary>
14
+
15
+ - Apple Maps – `apple-maps`
16
+ - Google Maps – `google-maps`
17
+ - Citymapper – `citymapper`
18
+ - Uber – `uber`
19
+ - Lyft – `lyft`
20
+ - The Transit App – `transit`
21
+ - TruckMap – `truckmap`
22
+ - Waze – `waze`
23
+ - Yandex.Navi – `yandex`
24
+ - Moovit – `moovit`
25
+ - Yandex Taxi – `yandex-taxi`
26
+ - Yandex Maps – `yandex-maps`
27
+ - Kakao Map – `kakaomap`
28
+ - Mapy.cz – `mapycz`
29
+ - Maps.me – `maps-me`
30
+ - OsmAnd - `osmand`
31
+ - Gett - `gett`
32
+ - Naver Map - `navermap`
33
+ - 2GIS - `dgis`
34
+ - Liftago - `liftago`
35
+
36
+ </details>
37
+
38
+ ## Installation
39
+
40
+ ### 1. Install the package
41
+
42
+ ```shell
43
+ npm i -S react-native-map-link # or yarn add react-native-map-link
44
+ ```
45
+
46
+ ### 2. Post-install steps
47
+
48
+ Based on the platforms your app supports, you also need to:
49
+
50
+ <details>
51
+ <summary><strong>iOS – Update Info.plist</strong></summary>
52
+
53
+ To allow your app to detect if any of the directions apps are installed, an extra step is required on iOS. Your app needs to provide the `LSApplicationQueriesSchemes` key inside `ios/{my-project}/Info.plist` to specify the URL schemes with which the app can interact.
54
+
55
+ Just add this in your `Info.plist` depending on which apps you'd like to support. Omitting these might mean that the library can't detect some of the maps apps installed by the user.
56
+
57
+ ```xml
58
+ <key>LSApplicationQueriesSchemes</key>
59
+ <array>
60
+ <string>comgooglemaps</string>
61
+ <string>citymapper</string>
62
+ <string>uber</string>
63
+ <string>lyft</string>
64
+ <string>transit</string>
65
+ <string>truckmap</string>
66
+ <string>waze</string>
67
+ <string>yandexnavi</string>
68
+ <string>moovit</string>
69
+ <string>yandextaxi</string>
70
+ <string>yandexmaps</string>
71
+ <string>kakaomap</string>
72
+ <string>szn-mapy</string>
73
+ <string>mapsme</string>
74
+ <string>osmandmaps</string>
75
+ <string>gett</string>
76
+ <string>nmap</string>
77
+ <string>dgis</string>
78
+ <string>lftgpas</string>
79
+ </array>
80
+ ```
81
+
82
+ Using Expo? [Read the instructions](docs/expo.md) to make it work on iOS.
83
+
84
+ </details>
85
+
86
+ <details>
87
+ <summary><strong>Android – Update AndroidManifest.xml</strong></summary>
88
+
89
+ When switching to Android 11/Android SDK 30 (i.e. using Expo SDK 41), this library doesn't work out of the box anymore. The reason is the new [Package Visibilty](https://developer.android.com/training/package-visibility) security feature. We'll have to update our `AndroidManifest.xml` to explicitly allow querying for other apps.
90
+
91
+ You can do so by coping the `<queries>` statement below, and pasting it in the top level of your AndroidManifest (i.e. within the `<manifest> ... </manifest>`).
92
+
93
+ ```xml
94
+ <queries>
95
+ <intent>
96
+ <action android:name="android.intent.action.VIEW" />
97
+ <data android:scheme="http"/>
98
+ </intent>
99
+ <intent>
100
+ <action android:name="android.intent.action.VIEW" />
101
+ <data android:scheme="https"/>
102
+ </intent>
103
+ <intent>
104
+ <action android:name="android.intent.action.VIEW" />
105
+ <data android:scheme="geo" />
106
+ </intent>
107
+ <intent>
108
+ <action android:name="android.intent.action.VIEW" />
109
+ <data android:scheme="google.navigation" />
110
+ </intent>
111
+ <intent>
112
+ <action android:name="android.intent.action.VIEW" />
113
+ <data android:scheme="applemaps" />
114
+ </intent>
115
+ <intent>
116
+ <action android:name="android.intent.action.VIEW" />
117
+ <data android:scheme="citymapper" />
118
+ </intent>
119
+ <intent>
120
+ <action android:name="android.intent.action.VIEW" />
121
+ <data android:scheme="uber" />
122
+ </intent>
123
+ <intent>
124
+ <action android:name="android.intent.action.VIEW" />
125
+ <data android:scheme="lyft" />
126
+ </intent>
127
+ <intent>
128
+ <action android:name="android.intent.action.VIEW" />
129
+ <data android:scheme="transit" />
130
+ </intent>
131
+ <intent>
132
+ <action android:name="android.intent.action.VIEW" />
133
+ <data android:scheme="truckmap" />
134
+ </intent>
135
+ <intent>
136
+ <action android:name="android.intent.action.VIEW" />
137
+ <data android:scheme="waze" />
138
+ </intent>
139
+ <intent>
140
+ <action android:name="android.intent.action.VIEW" />
141
+ <data android:scheme="yandexnavi" />
142
+ </intent>
143
+ <intent>
144
+ <action android:name="android.intent.action.VIEW" />
145
+ <data android:scheme="moovit" />
146
+ </intent>
147
+ <intent>
148
+ <action android:name="android.intent.action.VIEW" />
149
+ <data android:scheme="yandexmaps://maps.yandex." />
150
+ </intent>
151
+ <intent>
152
+ <action android:name="android.intent.action.VIEW" />
153
+ <data android:scheme="yandextaxi" />
154
+ </intent>
155
+ <intent>
156
+ <action android:name="android.intent.action.VIEW" />
157
+ <data android:scheme="kakaomap" />
158
+ </intent>
159
+ <intent>
160
+ <action android:name="android.intent.action.VIEW" />
161
+ <data android:scheme="mapycz" />
162
+ </intent>
163
+ <intent>
164
+ <action android:name="android.intent.action.VIEW" />
165
+ <data android:scheme="mapsme" />
166
+ </intent>
167
+ <intent>
168
+ <action android:name="android.intent.action.VIEW" />
169
+ <data android:scheme="osmand.geo" />
170
+ </intent>
171
+ <intent>
172
+ <action android:name="android.intent.action.VIEW" />
173
+ <data android:scheme="gett" />
174
+ </intent>
175
+ <intent>
176
+ <action android:name="android.intent.action.VIEW" />
177
+ <data android:scheme="nmap" />
178
+ </intent>
179
+ <intent>
180
+ <action android:name="android.intent.action.VIEW" />
181
+ <data android:scheme="dgis" />
182
+ </intent>
183
+ <intent>
184
+ <action android:name="android.intent.action.VIEW" />
185
+ <data android:scheme="lftgpas" />
186
+ </intent>
187
+ </queries>
188
+ ```
189
+
190
+ If you're running into a 'unexpected element `<queries>` found in `<manifest>`' error, make sure you have an updated version of Gradle in your `android/build.gradle` file:
191
+
192
+ ```java
193
+ classpath("com.android.tools.build:gradle:3.5.4")
194
+ ```
195
+
196
+ More info [here](https://stackoverflow.com/a/67383641/1129689).
197
+
198
+ </details>
199
+
200
+ <details>
201
+ <summary><strong>Expo – Update app.json</strong></summary>
202
+
203
+ [Read the instructions here](docs/expo.md) to make it work on iOS.
204
+
205
+ </details>
206
+
207
+ ## Usage
208
+
209
+ Using the `showLocation` function will shown an action sheet on iOS and an alert on Android, without any custom styling:
210
+
211
+ ```js
212
+ import { showLocation } from 'react-native-map-link'
213
+
214
+ showLocation({
215
+ latitude: 38.8976763,
216
+ longitude: -77.0387185,
217
+ sourceLatitude: -8.0870631, // optionally specify starting location for directions
218
+ sourceLongitude: -34.8941619, // not optional if sourceLatitude is specified
219
+ title: 'The White House', // optional
220
+ googleForceLatLon: false, // optionally force GoogleMaps to use the latlon for the query instead of the title
221
+ googlePlaceId: 'ChIJGVtI4by3t4kRr51d_Qm_x58', // optionally specify the google-place-id
222
+ alwaysIncludeGoogle: true, // optional, true will always add Google Maps to iOS and open in Safari, even if app is not installed (default: false)
223
+ dialogTitle: 'This is the dialog Title', // optional (default: 'Open in Maps')
224
+ dialogMessage: 'This is the amazing dialog Message', // optional (default: 'What app would you like to use?')
225
+ cancelText: 'This is the cancel button text', // optional (default: 'Cancel')
226
+ appsWhiteList: ['google-maps'], // optionally you can set which apps to show (default: will show all supported apps installed on device)
227
+ naverCallerName: 'com.example.myapp' // to link into Naver Map You should provide your appname which is the bundle ID in iOS and applicationId in android.
228
+ // appTitles: { 'google-maps': 'My custom Google Maps title' } // optionally you can override default app titles
229
+ // app: 'uber' // optionally specify specific app to use
230
+ directionsMode: 'walk' // optional, accepted values are 'car', 'walk', 'public-transport' or 'bike'
231
+ })
232
+ ```
233
+
234
+ Notes:
235
+
236
+ - The `sourceLatitude/sourceLongitude` options only work if you specify both. Currently supports all apps except Waze.
237
+ - Works on google-maps and apple-maps (on the latter, `bike` mode will not work). Without setting `directionsMode`, the app will decide based on his own settings.
238
+
239
+ ## More information
240
+
241
+ - [Using this library with Expo](docs/expo.md)
242
+ - [Alternative usage: styled popup](docs/popup.md)
243
+ - [Adding support for new maps apps](docs/add-app.md)
244
+
245
+ <br /><br />
246
+
247
+ ---
248
+
249
+ <div align="center">
250
+ <b>
251
+ <a href="https://schof.co/consulting/?utm_source=flexible-agency/react-native-map-link">Get professional support for this package →</a>
252
+ </b>
253
+ <br>
254
+ <sub>
255
+ Custom consulting sessions availabe for implementation support or feature development.
256
+ </sub>
257
+ </div>
package/index.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ import * as React from 'react';
2
+ import {ViewStyle, StyleProp, ImageStyle, TextStyle} from 'react-native';
3
+
4
+ interface Options {
5
+ latitude: number | string;
6
+ longitude: number | string;
7
+ sourceLatitude?: number;
8
+ sourceLongitude?: number;
9
+ alwaysIncludeGoogle?: boolean;
10
+ googleForceLatLon?: boolean;
11
+ googlePlaceId?: string;
12
+ title?: string;
13
+ app?: string;
14
+ dialogTitle?: string;
15
+ dialogMessage?: string;
16
+ cancelText?: string;
17
+ appsWhiteList?: string[];
18
+ appTitles?: {[key: string]: string};
19
+ naverCallerName?: string;
20
+ directionsMode?: 'car' | 'walk' | 'public-transport' | 'bike';
21
+ }
22
+
23
+ interface PopupStyleProp {
24
+ container?: StyleProp<ViewStyle>;
25
+ itemContainer?: StyleProp<ViewStyle>;
26
+ image?: StyleProp<ImageStyle>;
27
+ itemText?: StyleProp<TextStyle>;
28
+ headerContainer?: StyleProp<ViewStyle>;
29
+ titleText?: StyleProp<TextStyle>;
30
+ subtitleText?: StyleProp<TextStyle>;
31
+ cancelButtonContainer?: StyleProp<ViewStyle>;
32
+ cancelButtonText?: StyleProp<TextStyle>;
33
+ separatorStyle?: StyleProp<ViewStyle>;
34
+ activityIndicatorContainer?: StyleProp<ViewStyle>;
35
+ }
36
+
37
+ interface PopupProps {
38
+ isVisible: boolean;
39
+ showHeader?: boolean;
40
+ customHeader?: React.ReactNode;
41
+ customFooter?: React.ReactNode;
42
+ onCancelPressed: () => void;
43
+ onBackButtonPressed: () => void;
44
+ onAppPressed: () => void;
45
+ style?: PopupStyleProp;
46
+ modalProps?: object;
47
+ options: Options;
48
+ appsWhiteList: string[];
49
+ appTitles?: {[key: string]: string};
50
+ }
51
+
52
+ export function showLocation(
53
+ options: Options,
54
+ ): Promise<string | undefined | null>;
55
+ export class Popup extends React.Component<PopupProps> {}
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * React Native Map Link
3
+ */
4
+
5
+ import Popup from './src/components/Popup'
6
+ import { showLocation } from './src'
7
+
8
+ export {
9
+ showLocation,
10
+ Popup
11
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "react-native-map-link",
3
+ "version": "2.8.0",
4
+ "description": "Open the map app of the user's choice with a specific location",
5
+ "main": "index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/includable/react-native-map-link.git"
9
+ },
10
+ "scripts": {
11
+ "lint": "eslint src --max-warnings=0 && eslint tests --max-warnings=0",
12
+ "test": "jest"
13
+ },
14
+ "keywords": [
15
+ "react-native",
16
+ "react-component",
17
+ "maps",
18
+ "link",
19
+ "linking",
20
+ "react",
21
+ "ios",
22
+ "android"
23
+ ],
24
+ "author": "Thomas Schoffelen <thomas@includable.com> (https://includable.com/)",
25
+ "license": "MIT",
26
+ "bugs": {
27
+ "url": "https://github.com/includable/react-native-map-link/issues"
28
+ },
29
+ "homepage": "https://github.com/includable/react-native-map-link#readme",
30
+ "dependencies": {
31
+ "prop-types": "^15.7.2",
32
+ "react-native-modal": "^10.0.0"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=16.8.0",
36
+ "react-native": ">=0.40.0"
37
+ },
38
+ "devDependencies": {
39
+ "@babel/core": "^7.8.4",
40
+ "@babel/runtime": "^7.8.4",
41
+ "@react-native-community/eslint-config": "^1.1.0",
42
+ "babel-jest": "^25.1.0",
43
+ "eslint": "^6.5.1",
44
+ "jest": "^25.1.0",
45
+ "metro-react-native-babel-preset": "^0.61.0",
46
+ "react-native": "^0.64.1"
47
+ },
48
+ "jest": {
49
+ "preset": "react-native",
50
+ "setupFiles": [
51
+ "./tests/setup.js"
52
+ ]
53
+ }
54
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * React Native Map Link
3
+ */
4
+
5
+ import React from 'react';
6
+ import {
7
+ StyleSheet,
8
+ View,
9
+ Text,
10
+ Image,
11
+ TouchableOpacity,
12
+ Dimensions,
13
+ FlatList,
14
+ ActivityIndicator,
15
+ } from 'react-native';
16
+ import PropTypes from 'prop-types';
17
+ import Modal from 'react-native-modal';
18
+
19
+ import {getAvailableApps, checkNotSupportedApps} from '../utils';
20
+ import {showLocation} from '../index';
21
+ import {generateTitles, icons, generatePrefixes} from '../constants';
22
+
23
+ const SCREEN_HEIGHT = Dimensions.get('screen').height;
24
+
25
+ const colors = {
26
+ black: '#464646',
27
+ gray: '#BBC4CC',
28
+ lightGray: '#ACBBCB',
29
+ lightBlue: '#ECF2F8',
30
+ };
31
+
32
+ export default class Popup extends React.Component {
33
+ constructor(props) {
34
+ super(props);
35
+
36
+ this.state = {
37
+ apps: [],
38
+ loading: true,
39
+ titles: generateTitles(props.appTitles),
40
+ };
41
+
42
+ this._renderAppItem = this._renderAppItem.bind(this);
43
+ }
44
+
45
+ componentDidMount() {
46
+ this.loadApps();
47
+ }
48
+
49
+ async loadApps() {
50
+ const {appsWhiteList, options} = this.props;
51
+ let apps = await getAvailableApps(generatePrefixes(options));
52
+ if (appsWhiteList && appsWhiteList.length) {
53
+ checkNotSupportedApps(appsWhiteList);
54
+ apps = apps.filter((appName) =>
55
+ this.props.appsWhiteList.includes(appName),
56
+ );
57
+ }
58
+
59
+ this.setState({apps, loading: false});
60
+ }
61
+
62
+ _renderHeader() {
63
+ const {showHeader, customHeader, options} = this.props;
64
+ if (!showHeader) {
65
+ return null;
66
+ }
67
+ if (customHeader) {
68
+ return customHeader;
69
+ }
70
+
71
+ const dialogTitle =
72
+ options.dialogTitle && options.dialogTitle.length
73
+ ? options.dialogTitle
74
+ : 'Open in Maps';
75
+ const dialogMessage =
76
+ options.dialogMessage && options.dialogMessage.length
77
+ ? options.dialogMessage
78
+ : 'What app would you like to use?';
79
+
80
+ return (
81
+ <View style={[styles.headerContainer, this.props.style.headerContainer]}>
82
+ <Text style={[styles.titleText, this.props.style.titleText]}>
83
+ {dialogTitle}
84
+ </Text>
85
+ {dialogMessage && dialogMessage.length ? (
86
+ <Text style={[styles.subtitleText, this.props.style.subtitleText]}>
87
+ {dialogMessage}
88
+ </Text>
89
+ ) : null}
90
+ </View>
91
+ );
92
+ }
93
+
94
+ _renderApps() {
95
+ return (
96
+ <FlatList
97
+ ItemSeparatorComponent={() => (
98
+ <View
99
+ style={[styles.separatorStyle, this.props.style.separatorStyle]}
100
+ />
101
+ )}
102
+ data={this.state.apps}
103
+ renderItem={this._renderAppItem}
104
+ keyExtractor={(item) => item}
105
+ />
106
+ );
107
+ }
108
+
109
+ _renderAppItem({item}) {
110
+ return (
111
+ <TouchableOpacity
112
+ key={item}
113
+ style={[styles.itemContainer, this.props.style.itemContainer]}
114
+ onPress={() => this._onAppPressed({app: item})}>
115
+ <View>
116
+ <Image
117
+ style={[styles.image, this.props.style.image]}
118
+ source={icons[item]}
119
+ />
120
+ </View>
121
+ <Text style={[styles.itemText, this.props.style.itemText]}>
122
+ {this.state.titles[item]}
123
+ </Text>
124
+ </TouchableOpacity>
125
+ );
126
+ }
127
+
128
+ _renderCancelButton() {
129
+ const {options} = this.props;
130
+ const cancelText =
131
+ options.cancelText && options.cancelText.length
132
+ ? options.cancelText
133
+ : 'Cancel';
134
+ return (
135
+ <TouchableOpacity
136
+ style={[
137
+ styles.cancelButtonContainer,
138
+ this.props.style.cancelButtonContainer,
139
+ ]}
140
+ onPress={this.props.onCancelPressed}>
141
+ <Text
142
+ style={[styles.cancelButtonText, this.props.style.cancelButtonText]}>
143
+ {cancelText}
144
+ </Text>
145
+ </TouchableOpacity>
146
+ );
147
+ }
148
+
149
+ _renderFooter() {
150
+ const {customFooter} = this.props;
151
+ if (customFooter) {
152
+ return customFooter;
153
+ }
154
+ return this._renderCancelButton();
155
+ }
156
+
157
+ _onAppPressed({app}) {
158
+ showLocation({...this.props.options, app});
159
+ this.props.onAppPressed(app);
160
+ }
161
+
162
+ render() {
163
+ const {loading} = this.state;
164
+ return (
165
+ <Modal
166
+ isVisible={this.props.isVisible}
167
+ backdropColor={colors.black}
168
+ animationIn="slideInUp"
169
+ hideModalContentWhileAnimating
170
+ useNativeDriver
171
+ onBackButtonPress={this.props.onBackButtonPressed}
172
+ {...this.props.modalProps}>
173
+ <View style={[styles.container, this.props.style.container]}>
174
+ {this._renderHeader()}
175
+ {loading ? (
176
+ <ActivityIndicator
177
+ style={[
178
+ styles.activityIndicatorContainer,
179
+ this.props.style.activityIndicatorContainer,
180
+ ]}
181
+ />
182
+ ) : (
183
+ this._renderApps()
184
+ )}
185
+ {this._renderFooter()}
186
+ </View>
187
+ </Modal>
188
+ );
189
+ }
190
+ }
191
+
192
+ Popup.propTypes = {
193
+ isVisible: PropTypes.bool,
194
+ showHeader: PropTypes.bool,
195
+ customHeader: PropTypes.element,
196
+ customFooter: PropTypes.element,
197
+ onBackButtonPressed: PropTypes.func,
198
+ onAppPressed: PropTypes.func,
199
+ onCancelPressed: PropTypes.func,
200
+ style: PropTypes.object,
201
+ modalProps: PropTypes.object,
202
+ options: PropTypes.object.isRequired,
203
+ appsWhiteList: PropTypes.array,
204
+ };
205
+
206
+ Popup.defaultProps = {
207
+ isVisible: false,
208
+ showHeader: true,
209
+ customHeader: null,
210
+ customFooter: null,
211
+ style: {
212
+ container: {},
213
+ itemContainer: {},
214
+ image: {},
215
+ itemText: {},
216
+ headerContainer: {},
217
+ titleText: {},
218
+ subtitleText: {},
219
+ cancelButtonContainer: {},
220
+ cancelButtonText: {},
221
+ separatorStyle: {},
222
+ activityIndicatorContainer: {},
223
+ },
224
+ modalProps: {},
225
+ options: {},
226
+ appsWhiteList: null,
227
+ onBackButtonPressed: () => {},
228
+ onCancelPressed: () => {},
229
+ onAppPressed: () => {},
230
+ };
231
+
232
+ const styles = StyleSheet.create({
233
+ container: {
234
+ backgroundColor: 'white',
235
+ borderRadius: 10,
236
+ overflow: 'hidden',
237
+ maxHeight: SCREEN_HEIGHT * 0.6,
238
+ },
239
+ itemContainer: {
240
+ flexDirection: 'row',
241
+ alignItems: 'center',
242
+ paddingTop: 10,
243
+ paddingBottom: 10,
244
+ paddingLeft: 20,
245
+ paddingRight: 20,
246
+ },
247
+ image: {
248
+ width: 50,
249
+ height: 50,
250
+ borderRadius: 25,
251
+ },
252
+ itemText: {
253
+ fontSize: 16,
254
+ fontWeight: 'bold',
255
+ color: colors.black,
256
+ marginLeft: 15,
257
+ },
258
+ headerContainer: {
259
+ borderWidth: 1,
260
+ borderColor: 'transparent',
261
+ borderBottomColor: colors.lightBlue,
262
+ padding: 15,
263
+ },
264
+ titleText: {
265
+ fontSize: 16,
266
+ textAlign: 'center',
267
+ color: colors.black,
268
+ },
269
+ subtitleText: {
270
+ fontSize: 12,
271
+ color: colors.lightGray,
272
+ textAlign: 'center',
273
+ marginTop: 10,
274
+ },
275
+ cancelButtonContainer: {
276
+ justifyContent: 'center',
277
+ alignItems: 'center',
278
+ padding: 20,
279
+ borderWidth: 1,
280
+ borderColor: 'transparent',
281
+ borderTopColor: colors.lightBlue,
282
+ },
283
+ cancelButtonText: {
284
+ fontSize: 16,
285
+ fontWeight: 'bold',
286
+ color: colors.gray,
287
+ },
288
+ separatorStyle: {
289
+ flex: 1,
290
+ height: 1,
291
+ backgroundColor: colors.lightBlue,
292
+ },
293
+ activityIndicatorContainer: {
294
+ height: 70,
295
+ justifyContent: 'center',
296
+ alignItems: 'center',
297
+ },
298
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * React Native Map Link
3
+ */
4
+
5
+ import {Platform} from 'react-native';
6
+
7
+ export const isIOS = Platform.OS === 'ios';
8
+
9
+ export function generatePrefixes(options) {
10
+ return {
11
+ 'apple-maps': isIOS ? 'maps://' : 'applemaps://',
12
+ 'google-maps': prefixForGoogleMaps(options.alwaysIncludeGoogle),
13
+ citymapper: 'citymapper://',
14
+ uber: 'uber://',
15
+ lyft: 'lyft://',
16
+ transit: 'transit://',
17
+ truckmap: 'truckmap://',
18
+ waze: 'waze://',
19
+ yandex: 'yandexnavi://',
20
+ moovit: 'moovit://',
21
+ 'yandex-maps': 'yandexmaps://maps.yandex.ru/',
22
+ 'yandex-taxi': 'yandextaxi://',
23
+ kakaomap: 'kakaomap://',
24
+ mapycz: isIOS ? 'szn-mapy://' : 'mapycz://',
25
+ 'maps-me': 'mapsme://',
26
+ osmand: isIOS ? 'osmandmaps://' : 'osmand.geo://',
27
+ gett: 'gett://',
28
+ navermap: options.naverCallerName ? 'nmap://' : 'nmap-disabled://',
29
+ dgis: 'dgis://2gis.ru/',
30
+ liftago: 'lftgpas://',
31
+ };
32
+ }
33
+
34
+ export function prefixForGoogleMaps(alwaysIncludeGoogle) {
35
+ return isIOS && !alwaysIncludeGoogle
36
+ ? 'comgooglemaps://'
37
+ : 'https://www.google.com/maps/';
38
+ }
39
+
40
+ export function generateTitles(titles) {
41
+ return {
42
+ 'apple-maps': 'Apple Maps',
43
+ 'google-maps': 'Google Maps',
44
+ citymapper: 'Citymapper',
45
+ uber: 'Uber',
46
+ lyft: 'Lyft',
47
+ transit: 'The Transit App',
48
+ truckmap: 'TruckMap',
49
+ waze: 'Waze',
50
+ yandex: 'Yandex.Navi',
51
+ moovit: 'Moovit',
52
+ 'yandex-taxi': 'Yandex Taxi',
53
+ 'yandex-maps': 'Yandex Maps',
54
+ kakaomap: 'Kakao Maps',
55
+ mapycz: 'Mapy.cz',
56
+ 'maps-me': 'Maps Me',
57
+ osmand: 'OsmAnd',
58
+ gett: 'Gett',
59
+ navermap: 'Naver Map',
60
+ dgis: '2GIS',
61
+ liftago: 'Liftago',
62
+ ...(titles || {}),
63
+ };
64
+ }
65
+
66
+ export const icons = {
67
+ 'apple-maps': require('./images/apple-maps.png'),
68
+ 'google-maps': require('./images/google-maps.png'),
69
+ citymapper: require('./images/citymapper.png'),
70
+ uber: require('./images/uber.png'),
71
+ lyft: require('./images/lyft.png'),
72
+ transit: require('./images/transit.png'),
73
+ truckmap: require('./images/truckmap.png'),
74
+ waze: require('./images/waze.png'),
75
+ yandex: require('./images/yandex.png'),
76
+ moovit: require('./images/moovit.png'),
77
+ 'yandex-taxi': require('./images/yandex-taxi.png'),
78
+ 'yandex-maps': require('./images/yandex-maps.png'),
79
+ kakaomap: require('./images/kakao-map.png'),
80
+ mapycz: require('./images/mapycz.png'),
81
+ 'maps-me': require('./images/maps-me.png'),
82
+ osmand: require('./images/osmand.png'),
83
+ gett: require('./images/gett.png'),
84
+ navermap: require('./images/naver-map.png'),
85
+ dgis: require('./images/dgis.png'),
86
+ liftago: require('./images/liftago.png'),
87
+ };
88
+
89
+ export const appKeys = Object.keys(icons);
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/index.js ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * React Native Map Link
3
+ */
4
+
5
+ import {Linking} from 'react-native';
6
+
7
+ import {generatePrefixes, generateTitles, isIOS} from './constants';
8
+ import {askAppChoice, checkOptions} from './utils';
9
+
10
+ /**
11
+ * Open a maps app, or let the user choose what app to open, with the given location.
12
+ *
13
+ * @param {{
14
+ * latitude: number | string,
15
+ * longitude: number | string,
16
+ * sourceLatitude: number | undefined | null,
17
+ * sourceLongitude: number | undefined | null,
18
+ * alwaysIncludeGoogle: boolean | undefined | null,
19
+ * googleForceLatLon: boolean | undefined | null,
20
+ * googlePlaceId: number | undefined | null,
21
+ * title: string | undefined | null,
22
+ * app: string | undefined | null
23
+ * dialogTitle: string | undefined | null
24
+ * dialogMessage: string | undefined | null
25
+ * cancelText: string | undefined | null
26
+ * appsWhiteList: array | undefined | null
27
+ * appTitles: object | undefined | null
28
+ * naverCallerName: string | undefined
29
+ * directionsMode: 'car' | 'walk' | 'public-transport' | 'bike' | undefined
30
+ * }} options
31
+ */
32
+ export async function showLocation(options) {
33
+ const prefixes = generatePrefixes(options);
34
+ checkOptions(options, prefixes);
35
+
36
+ let useSourceDestiny = false;
37
+ let sourceLat;
38
+ let sourceLng;
39
+ let sourceLatLng;
40
+
41
+ if ('sourceLatitude' in options && 'sourceLongitude' in options) {
42
+ useSourceDestiny = true;
43
+ sourceLat = parseFloat(options.sourceLatitude);
44
+ sourceLng = parseFloat(options.sourceLongitude);
45
+ sourceLatLng = `${sourceLat},${sourceLng}`;
46
+ }
47
+
48
+ const lat = parseFloat(options.latitude);
49
+ const lng = parseFloat(options.longitude);
50
+ const latlng = `${lat},${lng}`;
51
+ const title = options.title && options.title.length ? options.title : null;
52
+ const encodedTitle = encodeURIComponent(title);
53
+ let app = options.app && options.app.length ? options.app : null;
54
+ const dialogTitle =
55
+ options.dialogTitle && options.dialogTitle.length
56
+ ? options.dialogTitle
57
+ : 'Open in Maps';
58
+ const dialogMessage =
59
+ options.dialogMessage && options.dialogMessage.length
60
+ ? options.dialogMessage
61
+ : 'What app would you like to use?';
62
+ const cancelText =
63
+ options.cancelText && options.cancelText.length
64
+ ? options.cancelText
65
+ : 'Cancel';
66
+ const appsWhiteList =
67
+ options.appsWhiteList && options.appsWhiteList.length
68
+ ? options.appsWhiteList
69
+ : null;
70
+
71
+ if (!app) {
72
+ app = await askAppChoice({
73
+ dialogTitle,
74
+ dialogMessage,
75
+ cancelText,
76
+ appsWhiteList,
77
+ prefixes,
78
+ appTitles: generateTitles(options.appTitles),
79
+ });
80
+ }
81
+
82
+ let url = null;
83
+
84
+ const getDirectionsModeAppleMaps = () => {
85
+ switch (options.directionsMode) {
86
+ case 'car':
87
+ return 'd';
88
+
89
+ case 'walk':
90
+ return 'w';
91
+
92
+ case 'public-transport':
93
+ return 'r';
94
+
95
+ default:
96
+ return undefined;
97
+ }
98
+ };
99
+
100
+ const getDirectionsModeGoogleMaps = () => {
101
+ switch (options.directionsMode) {
102
+ case 'car':
103
+ return 'driving';
104
+
105
+ case 'walk':
106
+ return 'walking';
107
+
108
+ case 'public-transport':
109
+ return 'transit';
110
+
111
+ case 'bike':
112
+ return 'bicycling';
113
+
114
+ default:
115
+ return undefined;
116
+ }
117
+ };
118
+
119
+ switch (app) {
120
+ case 'apple-maps':
121
+ const appleDirectionMode = getDirectionsModeAppleMaps();
122
+ url = prefixes['apple-maps'];
123
+ url = useSourceDestiny
124
+ ? `${url}?saddr=${sourceLatLng}&daddr=${latlng}`
125
+ : `${url}?ll=${latlng}`;
126
+ url += `&q=${title ? encodedTitle : 'Location'}`;
127
+ url += appleDirectionMode ? `&dirflg=${appleDirectionMode}` : '';
128
+ break;
129
+ case 'google-maps':
130
+ const googleDirectionMode = getDirectionsModeGoogleMaps();
131
+ // Always using universal URL instead of URI scheme since the latter doesn't support all parameters (#155)
132
+ url = 'https://www.google.com/maps/dir/?api=1';
133
+ if (useSourceDestiny) {
134
+ url += `&origin=${sourceLatLng}`;
135
+ }
136
+ if (!options.googleForceLatLon && title) {
137
+ url += `&destination=${encodedTitle}`;
138
+ } else {
139
+ url += `&destination=${latlng}`;
140
+ }
141
+
142
+ url += options.googlePlaceId
143
+ ? `&destination_place_id=${options.googlePlaceId}`
144
+ : '';
145
+
146
+ url += googleDirectionMode ? `&travelmode=${googleDirectionMode}` : '';
147
+ break;
148
+ case 'citymapper':
149
+ url = `${prefixes.citymapper}directions?endcoord=${latlng}`;
150
+
151
+ if (title) {
152
+ url += `&endname=${encodedTitle}`;
153
+ }
154
+
155
+ if (useSourceDestiny) {
156
+ url += `&startcoord=${sourceLatLng}`;
157
+ }
158
+ break;
159
+ case 'uber':
160
+ url = `${prefixes.uber}?action=setPickup&dropoff[latitude]=${lat}&dropoff[longitude]=${lng}`;
161
+
162
+ if (title) {
163
+ url += `&dropoff[nickname]=${encodedTitle}`;
164
+ }
165
+
166
+ url += useSourceDestiny
167
+ ? `&pickup[latitude]=${sourceLat}&pickup[longitude]=${sourceLng}`
168
+ : '&pickup=my_location';
169
+
170
+ break;
171
+ case 'lyft':
172
+ url = `${prefixes.lyft}ridetype?id=lyft&destination[latitude]=${lat}&destination[longitude]=${lng}`;
173
+
174
+ if (useSourceDestiny) {
175
+ url += `&pickup[latitude]=${sourceLat}&pickup[longitude]=${sourceLng}`;
176
+ }
177
+
178
+ break;
179
+ case 'transit':
180
+ url = `${prefixes.transit}directions?to=${latlng}`;
181
+
182
+ if (useSourceDestiny) {
183
+ url += `&from=${sourceLatLng}`;
184
+ }
185
+ break;
186
+ case 'truckmap':
187
+ url = `http://truckmap.com/place/${lat},${lng}`;
188
+
189
+ if (useSourceDestiny) {
190
+ url = `http://truckmap.com/route/${sourceLat},${sourceLng}/${lat},${lng}`;
191
+ }
192
+ break;
193
+ case 'waze':
194
+ url = `${prefixes.waze}?ll=${latlng}&navigate=yes`;
195
+ if (title) {
196
+ url += `&q=${encodedTitle}`;
197
+ }
198
+ break;
199
+ case 'yandex':
200
+ url = `${prefixes.yandex}build_route_on_map?lat_to=${lat}&lon_to=${lng}`;
201
+
202
+ if (useSourceDestiny) {
203
+ url += `&lat_from=${sourceLat}&lon_from=${sourceLng}`;
204
+ }
205
+ break;
206
+ case 'moovit':
207
+ url = `${prefixes.moovit}directions?dest_lat=${lat}&dest_lon=${lng}`;
208
+
209
+ if (title) {
210
+ url += `&dest_name=${encodedTitle}`;
211
+ }
212
+
213
+ if (useSourceDestiny) {
214
+ url += `&orig_lat=${sourceLat}&orig_lon=${sourceLng}`;
215
+ }
216
+ break;
217
+ case 'yandex-taxi':
218
+ url = `${prefixes['yandex-taxi']}route?end-lat=${lat}&end-lon=${lng}&appmetrica_tracking_id=1178268795219780156`;
219
+
220
+ break;
221
+ case 'yandex-maps':
222
+ url = `${prefixes['yandex-maps']}?pt=${lng},${lat}`;
223
+
224
+ break;
225
+ case 'kakaomap':
226
+ url = `${prefixes.kakaomap}look?p=${latlng}`;
227
+
228
+ if (useSourceDestiny) {
229
+ url = `${prefixes.kakaomap}route?sp=${sourceLat},${sourceLng}&ep=${latlng}&by=CAR`;
230
+ }
231
+ break;
232
+ case 'mapycz':
233
+ url = `${prefixes.mapycz}www.mapy.cz/zakladni?x=${lng}&y=${lat}&source=coor&id=${lng},${lat}`;
234
+
235
+ break;
236
+ case 'maps-me':
237
+ url = `${prefixes['maps-me']}route?sll=${sourceLat},${sourceLng}&saddr= &dll=${lat},${lng}&daddr=${title}&type=vehicle`;
238
+
239
+ break;
240
+ case 'osmand':
241
+ url = isIOS
242
+ ? `${prefixes.osmand}?lat=${lat}&lon=${lng}`
243
+ : `${prefixes.osmand}?q=${lat},${lng}`;
244
+
245
+ break;
246
+ case 'gett':
247
+ url = `${prefixes.gett}order?pickup=my_location&dropoff_latitude=${lat}&dropoff_longitude=${lng}`;
248
+
249
+ break;
250
+ case 'navermap':
251
+ url = `${prefixes.navermap}map?lat=${lat}&lng=${lng}&appname=${options.naverCallerName}`;
252
+
253
+ if (useSourceDestiny) {
254
+ url = `${prefixes.navermap}route?slat=${sourceLat}&slng=${sourceLng}&dlat=${lat}&dlng=${lng}&appname=${options.naverCallerName}`;
255
+ }
256
+ break;
257
+ case 'dgis':
258
+ url = `${prefixes.dgis}routeSearch/to/${lng},${lat}/go`;
259
+
260
+ if (useSourceDestiny) {
261
+ url = `${prefixes.dgis}routeSearch/to/${lng},${lat}/from/${sourceLng},${sourceLat}/go`;
262
+ }
263
+ break;
264
+ case 'liftago':
265
+ url = `${prefixes.liftago}order?destinationLat=${lat}&destinationLon=${lng}`;
266
+
267
+ if (title) {
268
+ url += `&destinationName=${encodedTitle}`;
269
+ }
270
+
271
+ if (useSourceDestiny) {
272
+ url += `&pickupLat=${sourceLat}&pickupLon=${sourceLng}`;
273
+ }
274
+ break;
275
+ }
276
+
277
+ if (url) {
278
+ return Linking.openURL(url).then(() => Promise.resolve(app));
279
+ }
280
+ }
package/src/utils.js ADDED
@@ -0,0 +1,232 @@
1
+ /**
2
+ * React Native Map Link
3
+ */
4
+
5
+ import {Linking, ActionSheetIOS, Alert} from 'react-native';
6
+
7
+ import {appKeys, isIOS} from './constants';
8
+
9
+ /**
10
+ * Get available navigation apps.
11
+ */
12
+ export const getAvailableApps = async (prefixes) => {
13
+ const availableApps = [];
14
+ for (const app in prefixes) {
15
+ if (prefixes.hasOwnProperty(app)) {
16
+ const avail = await isAppInstalled(app, prefixes);
17
+ if (avail) {
18
+ availableApps.push(app);
19
+ }
20
+ }
21
+ }
22
+
23
+ return availableApps;
24
+ };
25
+
26
+ /**
27
+ * Check if a given map app is installed.
28
+ *
29
+ * @param {string} app
30
+ * @param {object} prefixes
31
+ * @returns {Promise<boolean>}
32
+ */
33
+ export function isAppInstalled(app, prefixes) {
34
+ return new Promise((resolve) => {
35
+ if (!(app in prefixes)) {
36
+ return resolve(false);
37
+ }
38
+
39
+ Linking.canOpenURL(prefixes[app])
40
+ .then((result) => {
41
+ resolve(!!result);
42
+ })
43
+ .catch(() => resolve(false));
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Check if a given app is supported by this library
49
+ *
50
+ * @param {string} app
51
+ * @returns {boolean}
52
+ */
53
+ function isSupportedApp(app) {
54
+ return appKeys.includes(app);
55
+ }
56
+
57
+ /**
58
+ * Get a list of not supported apps from a given array of apps
59
+ *
60
+ * @param {array} apps
61
+ * @returns {array}
62
+ */
63
+ function getNotSupportedApps(apps) {
64
+ return apps.filter((app) => !isSupportedApp(app));
65
+ }
66
+
67
+ /**
68
+ * Throws an exception if some of the given apps is not supported
69
+ *
70
+ * @param {array} apps
71
+ */
72
+ export function checkNotSupportedApps(apps) {
73
+ const notSupportedApps = getNotSupportedApps(apps);
74
+ if (notSupportedApps.length) {
75
+ throw new MapsException(
76
+ `appsWhiteList [${notSupportedApps}] are not supported apps, please provide some of the supported apps [${appKeys}]`,
77
+ );
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Ask the user to choose one of the available map apps.
83
+ * @param {{
84
+ * title: string | undefined | null
85
+ * message: string | undefined | null
86
+ * cancelText: string | undefined | null
87
+ * appsWhiteList: string[] | null
88
+ * prefixes: string[],
89
+ * appTitles: object | undefined | null
90
+ * }} options
91
+ * @returns {Promise}
92
+ */
93
+ export function askAppChoice({
94
+ dialogTitle,
95
+ dialogMessage,
96
+ cancelText,
97
+ appsWhiteList,
98
+ prefixes,
99
+ appTitles,
100
+ }) {
101
+ return new Promise(async (resolve) => {
102
+ let availableApps = await getAvailableApps(prefixes);
103
+
104
+ if (appsWhiteList && appsWhiteList.length) {
105
+ availableApps = availableApps.filter((appName) =>
106
+ appsWhiteList.includes(appName),
107
+ );
108
+ }
109
+
110
+ if (availableApps.length < 2) {
111
+ return resolve(availableApps[0] || null);
112
+ }
113
+
114
+ if (isIOS) {
115
+ const options = availableApps.map((app) => appTitles[app]);
116
+ options.push(cancelText);
117
+
118
+ ActionSheetIOS.showActionSheetWithOptions(
119
+ {
120
+ title: dialogTitle,
121
+ message: dialogMessage,
122
+ options: options,
123
+ cancelButtonIndex: options.length - 1,
124
+ },
125
+ (buttonIndex) => {
126
+ if (buttonIndex === options.length - 1) {
127
+ return resolve(null);
128
+ }
129
+ return resolve(availableApps[buttonIndex]);
130
+ },
131
+ );
132
+
133
+ return;
134
+ }
135
+
136
+ const options = availableApps.map((app) => ({
137
+ text: appTitles[app],
138
+ onPress: () => resolve(app),
139
+ }));
140
+ options.unshift({
141
+ text: cancelText,
142
+ onPress: () => resolve(null),
143
+ style: 'cancel',
144
+ });
145
+
146
+ return Alert.alert(dialogTitle, dialogMessage, options, {
147
+ cancelable: true,
148
+ onDismiss: () => resolve(null),
149
+ });
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Check if options are valid and well passed
155
+ *
156
+ * @param {{
157
+ * latitude: number | string,
158
+ * longitude: number | string,
159
+ * sourceLatitude: number | undefined | null,
160
+ * sourceLongitude: number | undefined | null,
161
+ * googleForceLatLon: boolean | undefined | null,
162
+ * googlePlaceId: number | undefined | null,
163
+ * title: string | undefined | null,
164
+ * app: string | undefined | null
165
+ * dialogTitle: string | undefined | null
166
+ * dialogMessage: string | undefined | null
167
+ * cancelText: string | undefined | null
168
+ * naverCallerName: string | undefined
169
+ * }} options
170
+ * @param {object} prefixes
171
+ */
172
+ export function checkOptions(options, prefixes) {
173
+ if (!options || typeof options !== 'object') {
174
+ throw new MapsException(
175
+ 'First parameter of `showLocation` should contain object with options.',
176
+ );
177
+ }
178
+ if (!('latitude' in options) || !('longitude' in options)) {
179
+ throw new MapsException(
180
+ 'First parameter of `showLocation` should contain object with at least keys `latitude` and `longitude`.',
181
+ );
182
+ }
183
+ if (
184
+ 'title' in options &&
185
+ options.title &&
186
+ typeof options.title !== 'string'
187
+ ) {
188
+ throw new MapsException('Option `title` should be of type `string`.');
189
+ }
190
+ if (
191
+ 'googleForceLatLon' in options &&
192
+ options.googleForceLatLon &&
193
+ typeof options.googleForceLatLon !== 'boolean'
194
+ ) {
195
+ throw new MapsException(
196
+ 'Option `googleForceLatLon` should be of type `boolean`.',
197
+ );
198
+ }
199
+ if (
200
+ 'googlePlaceId' in options &&
201
+ options.googlePlaceId &&
202
+ typeof options.googlePlaceId !== 'string'
203
+ ) {
204
+ throw new MapsException(
205
+ 'Option `googlePlaceId` should be of type `string`.',
206
+ );
207
+ }
208
+ if ('app' in options && options.app && !(options.app in prefixes)) {
209
+ throw new MapsException(
210
+ 'Option `app` should be undefined, null, or one of the following: "' +
211
+ Object.keys(prefixes).join('", "') +
212
+ '".',
213
+ );
214
+ }
215
+ if ('appsWhiteList' in options && options.appsWhiteList) {
216
+ checkNotSupportedApps(options.appsWhiteList);
217
+ }
218
+ if (
219
+ 'appTitles' in options &&
220
+ options.appTitles &&
221
+ typeof options.appTitles !== 'object'
222
+ ) {
223
+ throw new MapsException('Option `appTitles` should be of type `object`.');
224
+ }
225
+ }
226
+
227
+ class MapsException {
228
+ constructor(message) {
229
+ this.message = message;
230
+ this.name = 'MapsException';
231
+ }
232
+ }