capacitor-google-navigation 0.0.7 → 0.0.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/README.md CHANGED
@@ -57,30 +57,47 @@ The `GoogleNavigation ~> 9.0` pod is declared in the plugin's podspec and is pul
57
57
  <string>This app uses your location for navigation, including in the background.</string>
58
58
  ```
59
59
 
60
- ### 3. Register your API key in `AppDelegate.swift`
60
+ ### 3. Register your API key
61
61
 
62
- The iOS SDK requires the key to be provided before any map or navigator is created.
62
+ #### Production (recommended)
63
63
 
64
- ```swift
65
- import UIKit
66
- import Capacitor
67
- import CapacitorGoogleNavigation
68
- import GoogleNavigation // required for GMSServices
64
+ Store the key in `Info.plist` under the key `GoogleNavigationAPIKey`. The plugin reads it automatically — no key needs to be passed from JS.
69
65
 
70
- @UIApplicationMain
71
- class AppDelegate: UIResponder, UIApplicationDelegate {
66
+ ```xml
67
+ <key>GoogleNavigationAPIKey</key>
68
+ <string>YOUR_IOS_API_KEY</string>
69
+ ```
72
70
 
73
- func application(
74
- _ application: UIApplication,
75
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
76
- ) -> Bool {
77
- GMSServices.provideAPIKey("YOUR_IOS_API_KEY")
78
- return true
79
- }
80
- }
71
+ Then call `initialize()` with no key:
72
+
73
+ ```ts
74
+ await GoogleNavigation.initialize({});
75
+ ```
76
+
77
+ Keep the key out of source control by injecting it at build time via an `.xcconfig` file:
78
+
79
+ ```
80
+ // Config.xcconfig (gitignored)
81
+ GOOGLE_NAV_API_KEY = AIzaSy...
82
+ ```
83
+
84
+ ```xml
85
+ <!-- Info.plist -->
86
+ <key>GoogleNavigationAPIKey</key>
87
+ <string>$(GOOGLE_NAV_API_KEY)</string>
88
+ ```
89
+
90
+ #### Development only
91
+
92
+ You can pass the key directly from JS for quick local testing. **Do not ship this in production** — the key will be visible in your compiled JS bundle.
93
+
94
+ ```ts
95
+ await GoogleNavigation.initialize({ apiKey: 'YOUR_IOS_API_KEY' });
81
96
  ```
82
97
 
83
- > You can also pass the key via `GoogleNavigation.initialize({ apiKey })` at runtime — the plugin calls `GMSServices.provideAPIKey()` for you. Either approach works; calling it in `AppDelegate` is the earlier-initialization option.
98
+ #### Restrict your API key
99
+
100
+ In Google Cloud Console → APIs & Services → Credentials, add an **iOS app restriction** with your app's bundle ID. This ensures the key is rejected if extracted and used outside your app.
84
101
 
85
102
  ---
86
103
 
@@ -88,6 +105,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
88
105
 
89
106
  ### 1. Add the API key and permissions to `android/app/src/main/AndroidManifest.xml`
90
107
 
108
+ The Android Navigation SDK reads the key directly from the manifest at startup — it is never passed from JS.
109
+
91
110
  ```xml
92
111
  <manifest>
93
112
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
@@ -95,14 +114,32 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
95
114
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
96
115
 
97
116
  <application>
98
- <!-- Required: Navigation SDK reads this at startup -->
99
117
  <meta-data
100
118
  android:name="com.google.android.geo.API_KEY"
101
- android:value="YOUR_ANDROID_API_KEY" />
119
+ android:value="${GOOGLE_NAV_API_KEY}" />
102
120
  </application>
103
121
  </manifest>
104
122
  ```
105
123
 
124
+ Keep the key out of source control using `gradle.properties` (gitignored):
125
+
126
+ ```
127
+ # android/gradle.properties (gitignored)
128
+ GOOGLE_NAV_API_KEY=AIzaSy...
129
+ ```
130
+
131
+ Then in `android/app/build.gradle`, expose it to the manifest:
132
+
133
+ ```groovy
134
+ android {
135
+ defaultConfig {
136
+ manifestPlaceholders = [GOOGLE_NAV_API_KEY: project.findProperty("GOOGLE_NAV_API_KEY") ?: ""]
137
+ }
138
+ }
139
+ ```
140
+
141
+ Restrict the key in Google Cloud Console by adding your app's **SHA-1 certificate fingerprint** and **package name** under Android app restrictions.
142
+
106
143
  > **Important:** On Android the Navigation SDK reads the API key from `AndroidManifest.xml` — not from the `apiKey` parameter passed to `initialize()`. The `apiKey` parameter is used on iOS only.
107
144
 
108
145
  ### 2. Request location permission at runtime
@@ -1,5 +1,6 @@
1
1
  package com.attributeai.navigation;
2
2
 
3
+ import android.app.Activity;
3
4
  import android.graphics.Color;
4
5
  import android.graphics.Typeface;
5
6
  import android.os.Bundle;
@@ -25,6 +26,7 @@ import com.google.android.libraries.navigation.NavigationView;
25
26
  public class NavigationFragment extends Fragment {
26
27
 
27
28
  private NavigationView navigationView;
29
+ private Button closeButton;
28
30
  private Runnable onCloseListener;
29
31
 
30
32
  public static NavigationFragment newInstance() {
@@ -44,57 +46,29 @@ public class NavigationFragment extends Fragment {
44
46
  ) {
45
47
  navigationView = new NavigationView(requireContext());
46
48
  navigationView.onCreate(savedInstanceState);
47
-
48
- FrameLayout root = new FrameLayout(requireContext());
49
- root.addView(navigationView, new FrameLayout.LayoutParams(
50
- FrameLayout.LayoutParams.MATCH_PARENT,
51
- FrameLayout.LayoutParams.MATCH_PARENT
52
- ));
53
-
54
- Button closeButton = new Button(requireContext());
55
- closeButton.setText("✕");
56
- closeButton.setTextColor(Color.WHITE);
57
- closeButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
58
- closeButton.setTypeface(null, Typeface.BOLD);
59
- closeButton.setBackgroundColor(Color.argb(153, 0, 0, 0)); // 60% black
60
- closeButton.setOnClickListener(v -> {
61
- if (onCloseListener != null) onCloseListener.run();
62
- });
63
-
64
- int sizePx = (int) TypedValue.applyDimension(
65
- TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics()
66
- );
67
- int marginPx = (int) TypedValue.applyDimension(
68
- TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
69
- );
70
-
71
- FrameLayout.LayoutParams btnParams = new FrameLayout.LayoutParams(sizePx, sizePx);
72
- btnParams.gravity = Gravity.TOP | Gravity.START;
73
- btnParams.topMargin = marginPx;
74
- btnParams.leftMargin = marginPx;
75
- root.addView(closeButton, btnParams);
76
-
77
- return root;
78
- }
79
-
80
- @Override
81
- public void onStart() {
82
- super.onStart();
83
- if (navigationView != null) navigationView.onStart();
49
+ return navigationView;
84
50
  }
85
51
 
86
52
  @Override
87
53
  public void onResume() {
88
54
  super.onResume();
89
55
  if (navigationView != null) navigationView.onResume();
56
+ attachCloseButtonToDecorView();
90
57
  }
91
58
 
92
59
  @Override
93
60
  public void onPause() {
61
+ detachCloseButtonFromDecorView();
94
62
  if (navigationView != null) navigationView.onPause();
95
63
  super.onPause();
96
64
  }
97
65
 
66
+ @Override
67
+ public void onStart() {
68
+ super.onStart();
69
+ if (navigationView != null) navigationView.onStart();
70
+ }
71
+
98
72
  @Override
99
73
  public void onStop() {
100
74
  if (navigationView != null) navigationView.onStop();
@@ -103,6 +77,7 @@ public class NavigationFragment extends Fragment {
103
77
 
104
78
  @Override
105
79
  public void onDestroyView() {
80
+ detachCloseButtonFromDecorView();
106
81
  if (navigationView != null) navigationView.onDestroy();
107
82
  super.onDestroyView();
108
83
  }
@@ -112,4 +87,51 @@ public class NavigationFragment extends Fragment {
112
87
  super.onSaveInstanceState(outState);
113
88
  if (navigationView != null) navigationView.onSaveInstanceState(outState);
114
89
  }
90
+
91
+ // MARK: - Close button attached to the Activity decor view, above all SDK UI
92
+
93
+ private void attachCloseButtonToDecorView() {
94
+ Activity activity = getActivity();
95
+ if (activity == null || closeButton != null) return;
96
+
97
+ int sizePx = (int) TypedValue.applyDimension(
98
+ TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics()
99
+ );
100
+ int marginPx = (int) TypedValue.applyDimension(
101
+ TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
102
+ );
103
+ int statusBarHeight = getStatusBarHeight(activity);
104
+
105
+ Button button = new Button(requireContext());
106
+ button.setText("✕");
107
+ button.setTextColor(Color.WHITE);
108
+ button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
109
+ button.setTypeface(null, Typeface.BOLD);
110
+ button.setBackgroundColor(Color.argb(153, 0, 0, 0));
111
+ button.setOnClickListener(v -> {
112
+ if (onCloseListener != null) onCloseListener.run();
113
+ });
114
+
115
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(sizePx, sizePx);
116
+ params.gravity = Gravity.TOP | Gravity.START;
117
+ params.topMargin = statusBarHeight + marginPx;
118
+ params.leftMargin = marginPx;
119
+
120
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
121
+ decorView.addView(button, params);
122
+ this.closeButton = button;
123
+ }
124
+
125
+ private void detachCloseButtonFromDecorView() {
126
+ Activity activity = getActivity();
127
+ if (activity == null || closeButton == null) return;
128
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
129
+ decorView.removeView(closeButton);
130
+ closeButton = null;
131
+ }
132
+
133
+ private int getStatusBarHeight(Activity activity) {
134
+ int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
135
+ return resourceId > 0 ? activity.getResources().getDimensionPixelSize(resourceId) : 0;
136
+ }
115
137
  }
@@ -14,11 +14,17 @@ public class GoogleNavigationPlugin: CAPPlugin, CAPBridgedPlugin {
14
14
  private lazy var implementation = GoogleNavigation(plugin: self)
15
15
 
16
16
  @objc func initialize(_ call: CAPPluginCall) {
17
- guard let apiKey = call.getString("apiKey"), !apiKey.isEmpty else {
18
- call.reject("apiKey is required")
17
+ let jsKey = call.getString("apiKey") ?? ""
18
+ let resolvedKey = !jsKey.isEmpty
19
+ ? jsKey
20
+ : Bundle.main.object(forInfoDictionaryKey: "GoogleNavigationAPIKey") as? String ?? ""
21
+
22
+ guard !resolvedKey.isEmpty else {
23
+ call.reject("No API key found. Add GoogleNavigationAPIKey to Info.plist (recommended) or pass apiKey to initialize().")
19
24
  return
20
25
  }
21
- implementation.initialize(apiKey: apiKey) { success, error in
26
+
27
+ implementation.initialize(apiKey: resolvedKey) { success, error in
22
28
  if let error = error {
23
29
  call.reject(error)
24
30
  } else {
@@ -5,6 +5,7 @@ import GoogleNavigation
5
5
  class NavigationMapViewController: UIViewController {
6
6
  private let session: GMSNavigationSession
7
7
  private var mapView: GMSMapView?
8
+ private var overlayWindow: UIWindow?
8
9
  var onDismiss: (() -> Void)?
9
10
 
10
11
  init(session: GMSNavigationSession) {
@@ -26,18 +27,41 @@ class NavigationMapViewController: UIViewController {
26
27
  container.addSubview(mapView)
27
28
  self.mapView = mapView
28
29
 
29
- // Overlay sits above the map and all Google-rendered UI
30
- let overlay = UIView()
31
- overlay.translatesAutoresizingMaskIntoConstraints = false
32
- overlay.isUserInteractionEnabled = true
33
- overlay.backgroundColor = .clear
34
- container.addSubview(overlay)
35
- NSLayoutConstraint.activate([
36
- overlay.topAnchor.constraint(equalTo: container.topAnchor),
37
- overlay.bottomAnchor.constraint(equalTo: container.bottomAnchor),
38
- overlay.leadingAnchor.constraint(equalTo: container.leadingAnchor),
39
- overlay.trailingAnchor.constraint(equalTo: container.trailingAnchor),
40
- ])
30
+ self.view = container
31
+ }
32
+
33
+ override func viewDidLoad() {
34
+ super.viewDidLoad()
35
+ guard let mapView = mapView else { return }
36
+ let enabled = mapView.enableNavigation(with: session)
37
+ if enabled {
38
+ mapView.cameraMode = .following
39
+ }
40
+ }
41
+
42
+ override func viewDidAppear(_ animated: Bool) {
43
+ super.viewDidAppear(animated)
44
+ addCloseButtonWindow()
45
+ }
46
+
47
+ override func viewDidDisappear(_ animated: Bool) {
48
+ super.viewDidDisappear(animated)
49
+ tearDownCloseButtonWindow()
50
+ }
51
+
52
+ // MARK: - Close button in a top-level UIWindow above all SDK UI
53
+
54
+ private func addCloseButtonWindow() {
55
+ guard let scene = view.window?.windowScene else { return }
56
+
57
+ let window = UIWindow(windowScene: scene)
58
+ window.windowLevel = .alert + 1
59
+ window.backgroundColor = .clear
60
+ window.isHidden = false
61
+
62
+ let overlayVC = UIViewController()
63
+ overlayVC.view.backgroundColor = .clear
64
+ window.rootViewController = overlayVC
41
65
 
42
66
  let button = UIButton(type: .system)
43
67
  button.setImage(UIImage(systemName: "xmark"), for: .normal)
@@ -46,27 +70,25 @@ class NavigationMapViewController: UIViewController {
46
70
  button.layer.cornerRadius = 20
47
71
  button.translatesAutoresizingMaskIntoConstraints = false
48
72
  button.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
49
- overlay.addSubview(button)
73
+ overlayVC.view.addSubview(button)
74
+
50
75
  NSLayoutConstraint.activate([
51
- button.topAnchor.constraint(equalTo: overlay.safeAreaLayoutGuide.topAnchor, constant: 16),
52
- button.leadingAnchor.constraint(equalTo: overlay.leadingAnchor, constant: 16),
76
+ button.topAnchor.constraint(equalTo: overlayVC.view.safeAreaLayoutGuide.topAnchor, constant: 16),
77
+ button.leadingAnchor.constraint(equalTo: overlayVC.view.leadingAnchor, constant: 16),
53
78
  button.widthAnchor.constraint(equalToConstant: 40),
54
79
  button.heightAnchor.constraint(equalToConstant: 40),
55
80
  ])
56
81
 
57
- self.view = container
82
+ self.overlayWindow = window
58
83
  }
59
84
 
60
- override func viewDidLoad() {
61
- super.viewDidLoad()
62
- guard let mapView = mapView else { return }
63
- let enabled = mapView.enableNavigation(with: session)
64
- if enabled {
65
- mapView.cameraMode = .following
66
- }
85
+ private func tearDownCloseButtonWindow() {
86
+ overlayWindow?.isHidden = true
87
+ overlayWindow = nil
67
88
  }
68
89
 
69
90
  @objc private func closeTapped() {
91
+ tearDownCloseButtonWindow()
70
92
  dismiss(animated: true) { [weak self] in
71
93
  self?.onDismiss?()
72
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-google-navigation",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Google maps turn by turn navigation for capcitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",