Ionic 8 and Angular 20 Geofencing with Capacitor and Google Places API

by Didin J. on Sep 18, 2025 Ionic 8 and Angular 20 Geofencing with Capacitor and Google Places API

Learn how to build a geofencing app using Ionic 8, Angular 20, and Capacitor with Google Places API. Step-by-step guide for Android & iOS.

Geofencing is a powerful feature for mobile applications that allows you to trigger events when a user enters or exits a specific geographic area. For example, you might send a notification when a customer arrives near your store, log location-based activity, or build safety alerts for restricted zones.

In this tutorial, we’ll build a geofencing mobile app using the latest Ionic 8, Angular 20, and Capacitor. We’ll integrate the Google Places API to search and select locations, then use Capacitor Geolocation together with a Geofencing plugin to monitor user movement in real time.

By the end of this tutorial, you’ll have a working Ionic app that can:

  • Search for locations using Google Places API.

  • Add and manage geofences around those places.

  • Trigger enter/exit events when crossing geofences.

  • Show results in a clean Ionic UI.

This is an update of our original Ionic 3 + Angular 5 geofence tutorial, modernized for today’s Ionic ecosystem with Capacitor replacing Cordova.


Project Setup (Ionic 8 + Angular 20 + Capacitor)

1. Install Ionic CLI

First, ensure you have the latest version of Node.js 20 or higher installed. Then install the Ionic CLI globally:

npm install -g @ionic/cli

Verify installation:

ionic -v

You should see a version 7.2.1.

2. Create a New Ionic Angular App

Generate a new Ionic Angular project using the blank starter:

ionic start ionic-geofence blank --type=angular

Navigate to the project:

cd ionic-geofence

3. Add Mobile Platforms

Let’s add Android and iOS platforms:

ionic cap add android
ionic cap add ios

This creates the native projects inside android/ and ios/ folders.

4. Run the App in Browser

To test everything is working:

ionic serve

Your app should open in the browser at http://localhost:8100/.

Ionic 8 and Angular 20 Geofencing with Capacitor and Google Places API - ionic blank

5. Build and Sync

Before working with plugins, build the app and sync with Capacitor:

ionic build
ionic cap sync

This ensures dependencies are installed in both the Angular and native layers.

✅ At this point, you have a working Ionic 8 + Angular 20 + Capacitor project ready for geofencing.


Installing Capacitor Plugins (Geolocation, Geofence, and Google Places API Integration)

Our app needs three key features:

  1. Geolocation – to track the device’s current position.

  2. Geofencing – to monitor when the device enters or exits defined geographic boundaries.

  3. Google Places API – to search and pick locations by name/address.

1. Install Capacitor Geolocation

The official Capacitor plugin provides accurate device GPS data.

npm install @capacitor/geolocation

No additional native setup is required, but on iOS and Android, we must add location permissions (we’ll cover that in the native config section later).

2. Install a Geofencing Plugin

The capacitor itself doesn’t ship with a geofencing plugin yet, so we’ll use a community-maintained one.

npm install @capacitor-community/background-geolocation
ionic cap sync

3. Install Google Maps / Places API

We’ll use Google Places API for searching and selecting locations. For that, install the Places JavaScript SDK:

npm install @types/google.maps --save-dev

In your index.html, load the Places API (replace YOUR_API_KEY with your real key):

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>

👉 You can create an API key from the Google Cloud Console. Enable Places API, Maps JavaScript API, and optionally Geocoding API.

4. Sync Capacitor

After installing plugins, sync them into the native platforms:

ionic cap sync

5. Verify Installations

Run the project in browser mode:

ionic serve

You won’t see geofencing working in the browser (only on device/emulator), but you can test that the Google Places autocomplete script loads by checking the browser console for errors.

✅ Now the project has the required Capacitor and Google APIs ready for building geolocation and geofencing features.


Configuring Android and iOS for Location and Geofence Permissions

Mobile platforms require explicit permission declarations for apps that access the user’s location. Without these, your geolocation or geofence logic will fail silently.

1. Android Permissions

Open your android/app/src/main/AndroidManifest.xml and add the following inside the <manifest> tag but outside <application>:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Why?

  • ACCESS_COARSE_LOCATION → Approximate location (city/block level).
  • ACCESS_FINE_LOCATION → Precise GPS location.
  • ACCESS_BACKGROUND_LOCATION → Required for monitoring geofences when the app is closed or in the background.

⚡ Important: On Android 10+, background location requires special runtime permission prompts. We’ll handle that in code later.

2. iOS Permissions

Open ios/App/App/Info.plist and add the following keys (place them as children of the root <dict> element):

<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to monitor geofences around selected places.</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need background location access to notify you when entering or leaving a geofence.</string>

<key>UIBackgroundModes</key>
<array>
  <string>location</string>
</array>

Why?

  • NSLocationWhenInUseUsageDescription → Required for foreground location tracking.

  • NSLocationAlwaysAndWhenInUseUsageDescription → Required for background monitoring.

  • UIBackgroundModes → Enables background location updates on iOS.

3. Sync Native Projects

After modifying platform configs, always sync changes:

ionic cap sync

Then rebuild your platforms:

ionic cap open android
ionic cap open ios

This opens the projects in Android Studio and Xcode so you can confirm the settings.

4. Testing Permissions

  • On Android → the system will ask for location permission at runtime.

  • On iOS → you’ll see a prompt: Allow app to use your location? with options for While Using or Always.

If you deny these, geofencing won’t work.

✅ At this point, your Ionic 8 + Capacitor app is fully configured to request and use location permissions on both Android and iOS.


Implementing Geolocation and Geofence Logic in Angular (Custom Geofence with Capacitor APIs)

Since Capacitor doesn’t have an official geofence plugin, we’ll build our own logic:

  1. Track user location using @capacitor-community/background-geolocation.

  2. Store geofences in an array.

  3. Check the distance between the current location and the geofence centers using the Haversine formula.

  4. Emit enter/exit events manually.

1. Create a Geolocation Service

Generate a service to encapsulate location logic:

ionic g service services/geolocation.services

Now edit src/app/services/geolocation.service.ts:

import { Injectable } from '@angular/core';
import { Geolocation } from '@capacitor/geolocation';
import {
  BackgroundGeolocationPlugin,
  Location,
} from '@capacitor-community/background-geolocation';
import { registerPlugin } from '@capacitor/core';

const BackgroundGeolocation = registerPlugin<BackgroundGeolocationPlugin>("BackgroundGeolocation");

export interface Geofence {
  id: string;
  latitude: number;
  longitude: number;
  radius: number; // in meters
  inside?: boolean; // track state
}

@Injectable({
  providedIn: 'root',
})
export class GeolocationService {
  private watchId: string | null = null;
  private geofences: Geofence[] = [];
  private geofenceCallbacks: ((event: any) => void)[] = [];

  constructor() { }

  // Get current location once
  async getCurrentPosition(): Promise<Location | null> {
    try {
      const position = await Geolocation.getCurrentPosition();
      return {
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
        accuracy: position.coords.accuracy,
      } as Location;
    } catch (err) {
      console.error('Error getting current position:', err);
      return null;
    }
  }

  // Watch continuous location updates
  async startWatchingLocation(callback: (loc: Location) => void) {
    this.watchId = await BackgroundGeolocation.addWatcher(
      {
        backgroundMessage: 'Tracking your location in the background',
        backgroundTitle: 'Location Tracking',
        requestPermissions: true,
        stale: false,
      },
      (location, err) => {
        if (err) {
          if (err.code === 'NOT_AUTHORIZED') {
            alert(
              'This app needs location access. Please grant permission in settings.'
            );
          }
          console.error('BackgroundGeolocation error:', err);
          return;
        }
        if (location) {
          callback(location);
          this.checkGeofences(location);
        }
      }
    );
  }

  async stopWatchingLocation() {
    if (this.watchId) {
      await BackgroundGeolocation.removeWatcher({ id: this.watchId });
      this.watchId = null;
    }
  }

  // Add geofence
  addGeofence(id: string, latitude: number, longitude: number, radius: number) {
    const geofence: Geofence = { id, latitude, longitude, radius, inside: false };
    this.geofences.push(geofence);
    console.log(`Geofence ${id} added`);
  }

  // Register a callback for geofence events
  onGeofenceEvent(callback: (event: any) => void) {
    this.geofenceCallbacks.push(callback);
  }

  // Check geofences against current location
  private checkGeofences(location: Location) {
    this.geofences.forEach((geofence) => {
      const distance = this.getDistanceFromLatLonInM(
        location.latitude,
        location.longitude,
        geofence.latitude,
        geofence.longitude
      );

      const wasInside = geofence.inside;
      const isInside = distance <= geofence.radius;

      if (!wasInside && isInside) {
        this.triggerGeofenceEvent({
          id: geofence.id,
          type: 'enter',
          location,
        });
      } else if (wasInside && !isInside) {
        this.triggerGeofenceEvent({
          id: geofence.id,
          type: 'exit',
          location,
        });
      }

      geofence.inside = isInside;
    });
  }

  // Trigger event to listeners
  private triggerGeofenceEvent(event: any) {
    console.log('Geofence event:', event);
    this.geofenceCallbacks.forEach((cb) => cb(event));
  }

  // Haversine formula: distance in meters
  private getDistanceFromLatLonInM(
    lat1: number,
    lon1: number,
    lat2: number,
    lon2: number
  ): number {
    const R = 6371000; // Earth radius in m
    const dLat = this.deg2rad(lat2 - lat1);
    const dLon = this.deg2rad(lon2 - lon1);
    const a =
      Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(this.deg2rad(lat1)) *
      Math.cos(this.deg2rad(lat2)) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
  }

  private deg2rad(deg: number): number {
    return deg * (Math.PI / 180);
  }
}

2. Update Home Component

In src/app/home/home.page.ts:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel } from '@ionic/angular/standalone';
import { GeolocationService } from '../services/geolocation.services';
import { JsonPipe } from '@angular/common';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
  imports: [IonLabel, IonItem, IonList, IonHeader, IonToolbar, IonTitle, IonContent, JsonPipe],
})
export class HomePage implements OnInit, OnDestroy {
  currentLocation: any = null;
  geofenceEvents: any[] = [];

  constructor(private geoService: GeolocationService) { }

  async ngOnInit() {
    // Get current location
    this.currentLocation = await this.geoService.getCurrentPosition();

    // Start watching location
    await this.geoService.startWatchingLocation((loc) => {
      this.currentLocation = loc;
    });

    // Listen for geofence triggers
    this.geoService.onGeofenceEvent((event) => {
      this.geofenceEvents.push(event);
    });

    // Add a sample geofence (Google HQ in Mountain View)
    this.geoService.addGeofence('GoogleHQ', 37.422, -122.084, 200);
  }

  async ngOnDestroy() {
    await this.geoService.stopWatchingLocation();
  }
}

3. Update the Template

Same as before (src/app/home/home.page.html):

<ion-header>
  <ion-toolbar>
    <ion-title>Geofence Demo</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <h2>Current Location</h2>
  <pre>{{ currentLocation | json }}</pre>

  <h2>Geofence Events</h2>
  <ion-list>
    <ion-item *ngFor="let event of geofenceEvents">
      <ion-label>
        <strong>{{ event.id }}</strong>
        <p>{{ event.type }}</p>
      </ion-label>
    </ion-item>
  </ion-list>
</ion-content>

✅ Now you have working geofence detection without relying on any outdated/unpublished plugins. It’s all done in TypeScript with Capacitor’s background geolocation as the engine.


Integrating Google Places API to Select Geofence Locations

We’ll allow users to:

  1. Search for places using the Google Places Autocomplete API.

  2. Select a place and create a geofence around it.

1. Enable Places API in Google Cloud Console

  1. Go to Google Cloud Console.

  2. Create (or select) a project.

  3. Enable Places API.

  4. Generate an API Key and restrict it to your app’s package name (Android) and bundle ID (iOS).

2. Install HTTP Client

We’ll call the Places REST API using Angular’s HttpClient.

npm install @angular/common@latest

3. Create a Places Service

Generate a service:

ionic g service services/places.service

Then edit src/app/services/places.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class PlacesService {
  private apiKey = 'YOUR_GOOGLE_PLACES_API_KEY'; // 🔑 Replace this

  constructor(private http: HttpClient) {}

  // Search place suggestions
  searchPlaces(query: string): Observable<any> {
    const url = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(
      query
    )}&key=${this.apiKey}`;
    return this.http.get(url);
  }

  // Get place details (lat/lng)
  getPlaceDetails(placeId: string): Observable<any> {
    const url = `https://maps.googleapis.com/maps/api/place/details/json?placeid=${placeId}&key=${this.apiKey}`;
    return this.http.get(url);
  }
}

⚡ Don’t forget to replace YOUR_GOOGLE_PLACES_API_KEY with your actual key.

4. Update Home Page to Use Places Service

Edit src/app/home/home.page.ts:

import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel, IonInput } from '@ionic/angular/standalone';
import { GeolocationService } from '../services/geolocation.services';
import { JsonPipe } from '@angular/common';
import { PlacesService } from '../services/places.service';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
  imports: [IonInput, IonLabel, IonItem, IonList, IonHeader, IonToolbar, IonTitle, IonContent, JsonPipe, FormsModule],
})
export class HomePage implements OnInit, OnDestroy {
  currentLocation: any = null;
  geofenceEvents: any[] = [];
  searchQuery: string = '';
  searchResults: any[] = [];

  constructor(
    private geoService: GeolocationService,
    private placesService: PlacesService
  ) { }

  async ngOnInit() {
    // Get current location
    this.currentLocation = await this.geoService.getCurrentPosition();

    // Start watching location
    await this.geoService.startWatchingLocation((loc) => {
      this.currentLocation = loc;
    });

    // Listen for geofence triggers
    this.geoService.onGeofenceEvent((event) => {
      this.geofenceEvents.push(event);
    });

    // Add a sample geofence (Google HQ in Mountain View)
    this.geoService.addGeofence('GoogleHQ', 37.422, -122.084, 200);
  }

  async ngOnDestroy() {
    await this.geoService.stopWatchingLocation();
  }

  // Search places
  search() {
    if (this.searchQuery.trim().length === 0) {
      this.searchResults = [];
      return;
    }
    this.placesService.searchPlaces(this.searchQuery).subscribe((res: any) => {
      this.searchResults = res.predictions;
    });
  }

  // Select a place and add geofence
  selectPlace(place: any) {
    this.placesService.getPlaceDetails(place.place_id).subscribe((res: any) => {
      const location = res.result.geometry.location;
      this.geoService.addGeofence(
        place.description,
        location.lat,
        location.lng,
        200
      );
      alert(`Geofence added for ${place.description}`);
      this.searchResults = [];
      this.searchQuery = '';
    });
  }
}

5. Update Home Template

Replace src/app/home/home.page.html with:

<ion-header>
  <ion-toolbar>
    <ion-title>Geofence Demo</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <h2>Current Location</h2>
  <pre>{{ currentLocation | json }}</pre>

  <h2>Add Geofence by Place</h2>
  <ion-item>
    <ion-input
      placeholder="Search a place..."
      [(ngModel)]="searchQuery"
      (ionInput)="search()"
    ></ion-input>
  </ion-item>

  <ion-list *ngIf="searchResults.length > 0">
    <ion-item
      *ngFor="let result of searchResults"
      (click)="selectPlace(result)"
    >
      <ion-label>{{ result.description }}</ion-label>
    </ion-item>
  </ion-list>

  <h2>Geofence Events</h2>
  <ion-list>
    <ion-item *ngFor="let event of geofenceEvents">
      <ion-label>
        <strong>{{ event.id }}</strong>
        <p>{{ event.type }}</p>
      </ion-label>
    </ion-item>
  </ion-list>
</ion-content>

6. Allow API Requests (Dev Mode)

During local dev, add this to ionic.config.json for CORS proxy:

{
  "name": "ionic-geofence",
  "integrations": {
    "capacitor": {}
  },
  "type": "angular-standalone",
  "proxies": [
    {
      "path": "/googleapi",
      "proxyUrl": "https://maps.googleapis.com"
    }
  ]
}

And update your URLs in places.service.ts to start with /googleapi/....

✅ Now users can search places with Google Places API, select one, and instantly create a geofence around it.


Testing the App (Simulating Geofences in Android Studio & Xcode)

1. Testing on Android (Android Studio)

a. Run the App on Emulator

  1. Open Android Studio.

  2. Select Device Manager > Pixel Emulator (API 34+).

  3. Run the app:

ionic cap run android -l --external

b. Simulate Location

  1. While the app is running, open More (⋮) → Location in the emulator controls.

  2. Enter latitude and longitude near one of your test geofences.

  3. Click Set Location.

    • Example: 37.422, -122.084 (Googleplex).

c. Move Across Geofence Boundary

  • Start with a location outside the radius.

  • Change the location inside the geofence.

  • Observe logs in Android Studio Logcat:

Geofence Transition: ENTER
Geofence ID: Googleplex
  • Move outside again → should trigger EXIT.

2. Testing on iOS (Xcode + Simulator)

a. Run the App on iOS

ionic cap run ios -l --external

Open the project in Xcode and launch the simulator (e.g., iPhone 15 Pro).

b. Simulate Location

  1. In the top menu, select:
    Features → Location → Custom Location…

  2. Enter coordinates near your geofence.

  3. Example: 37.3318, -122.0312 (Apple HQ).

c. Test Enter & Exit

  • First, set a location outside the geofence.

  • Then switch to a location inside.

  • Check the app’s console logs for:

Geofence Transition: ENTER

Switch back outside → should show EXIT.

3. Debugging Tips

  • No events firing?

    • Ensure you gave Location Always permission in the app.

    • On iOS, background geofencing only works on a real device, not simulator.

  • API errors?

    • Make sure the Google Places API is enabled and API key restrictions are correct.

  • Radius too small?

    • Some simulators have precision limits — try 200m+ radius.

✅ At this point, you should be able to simulate entering/exiting geofences in both Android and iOS without leaving your desk.


Conclusion and Next Steps

✅ What We Built

In this tutorial, we successfully built a Geofencing-enabled mobile app using the latest stack:

  • Ionic 8 + Angular 20 + Capacitor for modern hybrid app development.

  • Capacitor Geolocation & Geofence plugins to monitor user location and trigger events.

  • Google Places API integration for searching and selecting geofence locations.

  • Cross-platform support (Android & iOS) with permissions and simulator testing.

With this foundation, the app can now:

  • Detect when users enter or exit defined areas.

  • Trigger actions like push notifications, UI updates, or background tasks.

  • Be extended into real-world use cases like delivery tracking, attendance apps, or location-based reminders.

📱 Preparing for Publishing

When you’re ready to ship to Google Play or Apple App Store, don’t forget:

  1. App Signing & Builds

    • Android:

      ionic cap build android --prod

      Generate a signed APK/AAB in Android Studio.

    • iOS:

      ionic cap build ios --prod

      Archive and upload via Xcode Organizer.

  2. API Key Security

    • Restrict your Google API key to:

      • Android package name (e.g., com.yourapp.geofence).

      • iOS bundle ID (e.g., com.yourapp.geofence).

  3. Permissions & Privacy Policy

    • Android: Update AndroidManifest.xml with ACCESS_FINE_LOCATION and background permissions.

    • iOS: Ensure Info.plist includes NSLocationWhenInUseUsageDescription and NSLocationAlwaysAndWhenInUseUsageDescription.

    • Publish a Privacy Policy page if collecting user data.

🔋 Battery Optimizations

Geofencing apps can drain battery if not optimized. Follow these best practices:

  • Use larger geofence radii → fewer transitions and GPS checks.

  • Limit active geofences → Google Play recommends ≤ 100 geofences.

  • Rely on system-level geofencing APIs → Capacitor’s plugin already delegates to native Android/iOS geofencing, which is optimized for battery.

  • Throttle background updates → Avoid continuous GPS polling, rely on geofence triggers.

🚀 Next Steps

Here are some features you can add next:

  • Push Notifications → Trigger a notification when entering/exiting a geofence.

  • Server Syncing → Store geofence events in a backend (e.g., Firebase, Node.js, or Go).

  • Multiple Geofences → Allow users to add and manage several geofences at once.

  • Map Integration → Show geofences visually using Google Maps or Leaflet.

🎯 Final Thoughts

By upgrading from the old Ionic 3 + Cordova tutorial to Ionic 8 + Angular 20 + Capacitor, we’ve built a modern, forward-compatible app that leverages the latest web and mobile technologies.

This foundation gives you everything you need to create location-aware apps that are powerful, efficient, and ready for production.

You can find the working source code on our GitHub.

We know that building beautifully designed Ionic apps from scratch can be frustrating and very time-consuming. Check Ionic 6 - Full Starter App and save development and design time. Android, iOS, and PWA, 100+ Screens and Components, the most complete and advanced Ionic Template.

That's just the basics. If you need more deep learning about Ionic, Angular, and TypeScript, you can take the following cheap course:

Thanks!