Browser location with Laravel Livewire
Tags: laravel, livewire, geolocation, iplocation
Published: 2021-08-08 11:37:48

So this past week at week at work has been a triumph like none other.  I was able to get my NativeScript app in the app store and google play store for testing.  Yes I'm sure one million bugs will come up, but literally that saying the light at the end of the tunnel, well yea I'm blinded by it lol.  That being said at the beginning of the week I had a bit of down time while management was getting me full access to the apple developer area.  So they've had this add campaign idea that I knocked out.  Basically we have charters in various cities and instead of creating 1500+ individual campaigns for specific locations, the idea is to create one campaign that will get the users location and return nearby charters and automatically sort them by nearest first.  

We also (and I say we and it's management / admins) wanted it to be as close to possible as our Search results as to feel like you are actually on the site and not just on a random one off web page.  Luckily for us we have some blade components that can be reused so I don't have to build out anything super custom.  We also already have a livewire Search component that if I can reuse, literally would save me a headache.  I know that I'll be needing a new livewire component for this single landing page that will be in charge of handling the most minor of tasks (honestly might be overkill but adding one more component to this monolith won't be doing any real harm at this point):
1. On page load check to see if locations services are turned on
2. Ask for permission to access them
3. Yes - return lat / lng and do a reverse geocode lookup (google API) to get location
4. No - get lat / lng / location based on IP address (ipstack)
5. Pass lat / lng / location to the Search component for results

So now I do my generic livewire CLI stuff to make a new component and I call it `SearchNearyBy`.  I add a few properties that I know I'll need:

```
    public $location;
    public $lat;
    public $lng;
    public $sortFilter;
    public $data;

    protected $queryString = ['location', 'lat', 'lng', 'sortFilter'];
```
*$data* is a fun one I'll explain later (and yes it's about as hacky as it gets but sometimes you just deal with it).
and in my render() method I extend that component I talked about earlier that will make this page use the header / footer of the normal site and make it seamless
```
    public function render()
    {
        return view('livewire.web.search-near-by')->extends('layouts.web-layout')->section('content');
    }
```

My `search-near-by.blade` file is pretty simple too.  The one thing I need to make sure of is before I load the Search livewire component I want to make sure that I have a lat, lng, and location otherwise it'll be all empty and my company will ask why they hired me.  I also realize that lat / lng are found immediately and with the browser location but for reverse geocoding I have to actually call an API.  So I decided to make everything dependent on `$location`:
```
<div>
    @if($location)
        <livewire:web.search :lat="$lat" :lng="$lng" :location="$location" :sortFilter="$sortFilter"/>
    @else
        <div class="mt-40 w-full m-auto text-center">
            <i class="fal fa-lg fa-spinner fa-spin text-6xl font-bold text-fa-blue-700"></i>
        </div>
    @endif
</div>
```

So now all that is left is the whole javascript stuff (that I thought I would never have to use again being that we chose livewire, but there are some things that you just cannot get away from and this is 100% one of them).
```
    document.addEventListener('livewire:load', function () {
            getLocation();
    });
```
So this is literally it!!!!!!!!!!!!!  
Yes I know you're wondering but how?!?!?!!?!?!   well getLocation() is where all the "real" magic happens.  


[Navigator.geolocation](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation) is what I'll be using along with [getCurrentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) which takes in a success / error and some optional parameters that you are more than welcomed to check out.  For my purposes I did not see a need for them.  
```
    function getLocation() {
        navigator.geolocation.getCurrentPosition(onSuccess, onError);
    }
```
Pretty straight forwards here, and all I need to do now is define those two methods and write some basic code with a fallback (you'll see).  The easier of the 2 is the onError.  If location services are not enabled or a user blocks then this is what is ran, and we get our lat,lng,location based off of IP address, which honestly on a computer / wifi is still pretty accurate.  It's when you are using a phone / on data where I'll be in Sarasota and it says I'm in Miami or Orlando, but anyways here it is:
```
    function onError() {
        window.axios.get('https://api.ipstack.com/check?access_key=YOUR_API_KEY')
        .then(function(response) {
            @this.lat = response.data.latitude;
            @this.lng = response.data.longitude;
            @this.location = response.data.city + ', ' + response.data.region_name;
        });
    }
```
[IPStack](https://ipstack.com/) check it out.  kinda cool, get an API key and you can play around.  But as you can see pretty basic code.  set the variables to the proper response from the API call, and as we see in the blade code, once that `@this.location` is set, the livewire search component will be displayed and the font awesome spinning wheel (loading state) shall no longer be there.  

So now onto the slightly more complicated code (well it shouldn't have been but I had to do some stuff that I'm not necessarily proud of).
```
    function onSuccess(position) {
        const {
            latitude,
            longitude
        } = position.coords;

        @this.lat = latitude;
        @this.lng = longitude;

        window.axios.get(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&sensor=false&key=YOUR_API_KEY')
            .then(function(response) {
                if(response.data.results[0]) {
                    @this.data = response.data.results[0];
                } else {
                    onError();
                }
            })
    }
```
So looking at the MDN Web Docs, the success function will have a position and we do some object destructuring to get the lat / lng.  I then make a simple google API call to get an address from lat/lng.  Realistically `response.data.results[0].formatted_address` would be 100% sufficient however it literally gives you the address of where you are at, kinda creepy, kinda big brothery, and well the admins over here thought that some of our clientele would get all aluminum foil wearing hats on us and not visit the site anymore, which cmon google / the government has been listening to us for years, who cares, right?  

But at the level of salary that I make I don't make decisions I just do what I'm told.  So I had to figure out a way to extract the city / state from the formatted address.  I tried regex with no real success (not to self I suck at regex), and yes I googled and based on randomness of addresses I personally could not find one that I felt comfortable using that hit all of the marks.  So this is the `$data` property I mentioned earlier.  Oh yea but before I do that you can see from the above `onSuccess` method that if there is no response data it call `onError()` and we just get it based on IP address.  And I've never once ran into a situation where the IP failed.  Yea if you are behind a VPN and set your location / server to China, well that's not really my problem.  

So before I go on a long tangent again let's finish up what I did here.  I looked at the google response (first array position only) and decided to look for a couple of instances that worked for me.  I also did this with various lats / lngs to make sure it would all play nicely, and came up with this (yes I'm sorry it's hacky not something a mid level would do but hey I'm all alone and we need to ship shit): 
```
    public function updatedData()
    {
        $city = '';
        $state = '';
        foreach ($this->data['address_components'] as $component) {
            if (in_array('locality', $component['types']) && in_array('political', $component['types'])) {
                $city = $component['long_name'];
            }
            if (in_array('administrative_area_level_1', $component['types']) && in_array('political', $component['types'])) {
                $state = $component['short_name'];
            }
        }
        if ($city && $state) {
            $this->location = $city.', '.$state;
        } else {
            $this->location = $this->data['formatted_address'];
        }
    }
```
First off I use the livewire lifecycle `updatedFoo()` where Foo is Data and if data is updated this method gets ran.  I loop through the address_components and after looking at the API responses carefully I came up with whenever  the types array has `political` and `locality` well that's the city and whenever it has `political` and `administrative_area_level_1` that's the state.  Yes hacky as all can be but they didn't want the full address, and yes you'll also see that if the two variables i created at the beginning of the method ($city, $state) are not set, it still 100% returns the formatted address.  I just didn't want them to sit there with a spinning wheel forever.  

I actually think the whole browser access / location services are rad as shit.  Pretty amazing and also pretty big brother that you can get my exact location without me knowing.  I mean if I have location services turned on and don't setup my preferences (which I'm going to assume a lot of people don't), then bam anyone can check in on me anytime (on any site that I visit).  So maybe that isn't rad as shit it's more the technology.  I just look at how cool it is and I know I would never use it for harm / bad / data mining, but not everyone is as cool as me or you.  Thanks for reading along and hope you enjoyed this.