Geolocation in Power Apps: Translating Addresses and Validating Check-Ins
Power Apps

Geolocation in Power Apps: Translating Addresses and Validating Check-Ins

Content type Blog Post
Author Temmy Wahyu Raharjo
Publication Date 10 Feb, 2026
Reading Time Less than 1 minute

Introduction

Geolocation in Power Apps

Hi Everyone, I’m back, and today, we will learn how to implement translating addresses to longitude and latitude (using Azure Maps API) and validating check-ins. For example, there will be a plan for the Salesperson to go to location X for Customer A. On the day itself, the Salesperson can check in on the Client Office, and the Sales Manager can verify if the visitation is valid.

Diagram Flow for validating the Salesperson’s visit

Based on the above scenario, what we need to do:

  • Create an Azure Maps Account (I don’t think I need to show how to create this, as the creation is very straightforward. Once the resource is created, go to Settings > Authentication > Shared Key Authentication > copy the Primary Key and keep it for later usage.
  • Create a custom table named Visit (Activity Table) with several attributes.
  • Create two Plugins.
  • Create a Canvas App to show the two points (shout out to Matthew Devaney with this blog post)

Without further ado, let’s go!

Visit Table

To let the Sales Manager upload/create records for Visit Planning, we need to create a custom table. But we will use the Activity table like the screenshot below:

Activity Table screenshot

Create an activity table to store location data

Once created, I created the following custom attributes:

Custom Attributes for Visit table

As you can see in the above screenshot, I created a lookup attribute and set it to System User (to allow the Sales Manager to set the Salesperson of the visit).

 

Environment Variables

I also created 3 Environment Variables for these purposes:

DisplayName Description Value
AzureMapUrl Azure Map URL that will be invoked for translating the Address to longitude and latitude.
As you can see in the Value, the text {address} will be replaced by the
Visit.Address on the plugin.
https://atlas.microsoft.com/search/address/json?subscription-key=YOUR-AZURE-MAP-PRIMARY-KEY&api-version=1.0&query={address}
CheckInValidationInKilometers To validate how many KM differences (planning vs actualization) 3 (on Kilometers)
VisitCanvasAppUrl The URL of the Canvas App. This will be used to embed the Canvas App into the Model Driven Apps Main Form.
As you can see in the value sample, the text {recordId} will be replaced by JavaScript’s function later.
https://apps.powerapps.com/play/…&recordId={recordId}

Plugin Code

I created this business logic code:


using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json.Serialization;
 
namespace MapDemoPlugin.Business
{
    public class TranslateAddress
    {
        public TranslateAddress(ILocalPluginContext context)
        {
            Context = context;
        }
 
        public void Execute()
        {
            var target = Context.PluginExecutionContext.InputParameters["Target"] as Entity;
            if (target == null) return;
 
            var azureMapUrl = Context.InitiatingUserService.GetEnvironmentVariableByName("AzureMapUrl");
            if (string.IsNullOrEmpty(azureMapUrl)) return;
 
            var address = target.GetAttributeValue("ins_address");
            if (string.IsNullOrEmpty(address)) return;
 
            var addressEncoded = Uri.EscapeDataString(address);
            azureMapUrl = azureMapUrl.Replace("{address}", addressEncoded);
 
            var client = new HttpClient();
            var request = new HttpRequestMessage(HttpMethod.Get, azureMapUrl);
            var response = client.SendAsync(request).GetAwaiter().GetResult();
            response.EnsureSuccessStatusCode();
            var responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
            if (string.IsNullOrEmpty(responseBody)) throw new InvalidOperationException("Response body is empty");
 
            Context.TracingService.Trace("URL: {0}", azureMapUrl);
            Context.TracingService.Trace("Response: {0}", responseBody);
 
            var data = System.Text.Json.JsonSerializer.Deserialize(responseBody);
            if (data == null || data.Results == null || !data.Results.Any()) throw new InvalidOperationException("No results found");
 
            target["ins_latitude"] = data.Results[0].Position.Latitude;
            target["ins_longitude"] = data.Results[0].Position.Longitude;
        }
 
        public class Response
        {
            [JsonPropertyName("results")]
            public Result[] Results { get; set; }
        }
 
        public class Result
        {
            [JsonPropertyName("position")]
            public Position Position { get; set; }
        }
 
        public class Position
        {
            [JsonPropertyName("lat")]
            public decimal Latitude { get; set; }
            [JsonPropertyName("lon")]
            public decimal Longitude { get; set; }
        }
 
        public ILocalPluginContext Context { get; }
    }
 
    public class ValidateCheckIn
    {
        public ValidateCheckIn(ILocalPluginContext context)
        {
            Context = context;
        }
 
        public void Execute()
        {
            var target = Context.PluginExecutionContext.InputParameters["Target"] as Entity;
            if (target == null) return;
 
            if (!double.TryParse(Context.InitiatingUserService.GetEnvironmentVariableByName("CheckInValidationInKilometers"), out var rangeValidation)) return;
 
            var currentRecord = Context.InitiatingUserService.Retrieve(target.LogicalName, target.Id, new ColumnSet("ins_latitude", "ins_longitude", "ins_actuallatitude", "ins_actuallongitude"));
            var actualLatitude = target.GetAttributeValue("ins_actuallatitude") ?? currentRecord.GetAttributeValue("ins_actuallatitude");
            var actualLongitude = target.GetAttributeValue("ins_actuallongitude") ?? currentRecord.GetAttributeValue("ins_actuallongitude"); 
            if (!actualLatitude.HasValue || !actualLongitude.HasValue) return;
 
            var latitude = target.GetAttributeValue("ins_latitude") ?? currentRecord.GetAttributeValue("ins_latitude");
            var longitude = target.GetAttributeValue("ins_longitude") ?? currentRecord.GetAttributeValue("ins_longitude");
            if (!latitude.HasValue || !longitude.HasValue) return;
 
            var result = IsWithinDistance((double)latitude.GetValueOrDefault(), (double)longitude.GetValueOrDefault(), (double)actualLatitude.GetValueOrDefault(),
                (double)actualLongitude.GetValueOrDefault(), rangeValidation, out var distance);
            target["ins_difference"] = distance;
            target["ins_validcheckin"] = result;
        }
 
 
        private static double CalculateDistance(double lat1, double lon1, double lat2, double lon2)
        {
            const double R = 6371.0; // Radius of Earth in kilometers
 
            double lat1Rad = DegreesToRadians(lat1);
            double lon1Rad = DegreesToRadians(lon1);
            double lat2Rad = DegreesToRadians(lat2);
            double lon2Rad = DegreesToRadians(lon2);
 
            double dlat = lat2Rad - lat1Rad;
            double dlon = lon2Rad - lon1Rad;
 
            double a = Math.Sin(dlat / 2) * Math.Sin(dlat / 2) +
                       Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
                       Math.Sin(dlon / 2) * Math.Sin(dlon / 2);
 
            double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
            return R * c;
        }
 
        private static bool IsWithinDistance(double lat1, double lon1, double lat2, double lon2, double thresholdKm, out double distance)
        {
            distance = CalculateDistance(lat1, lon1, lat2, lon2);
            return distance <= thresholdKm;
        }
 
        private static double DegreesToRadians(double degrees)
        {
            return degrees * Math.PI / 180.0;
        }
 
        public ILocalPluginContext Context { get; }
    }
 
    public static class OrganizationServiceExtensions
    {
        public static TValue GetEnvironmentVariableByName(this IOrganizationService service, string environmentVariableName)
        {
            var query = new QueryExpression("environmentvariabledefinition")
            {
                ColumnSet = new ColumnSet("defaultvalue", "schemaname"),
                TopCount = 1
            };
            query.Criteria.AddCondition("displayname", ConditionOperator.Equal, environmentVariableName);
 
            var childLink =
                query.AddLink("environmentvariablevalue", "environmentvariabledefinitionid", "environmentvariabledefinitionid", JoinOperator.LeftOuter);
            childLink.EntityAlias = "ev";
            childLink.Columns = new ColumnSet("value");
            childLink.Orders.Add(new OrderExpression("createdon", OrderType.Descending));
 
            var result = service.RetrieveMultiple(query);
 
            var environmentVariable = result.Entities.Any()
                ? result.Entities.FirstOrDefault()
                : new Entity();
 
            var value = environmentVariable.Contains("ev.value") ?
                 (TValue)environmentVariable.GetAttributeValue("ev.value").Value :
                 environmentVariable.GetAttributeValue("defaultvalue");
            return value;
        }
    }
}

The TranslateAddress business logic, basically, will call the Azure Maps API, and we will get the following sample response:


{
"summary": {
"query": "mall taman anggrek jakarta barat indonesia",
"queryType": "NON_NEAR",
"queryTime": 404,
"numResults": 10,
"offset": 0,
"totalResults": 11126,
"fuzzyLevel": 2
},
"results": [
{
"type": "Street",
"id": "HUMN44nok2oIrc1jG20fNA",
"score": 0.5431748232904767,
"matchConfidence": {
"score": 0.5431748232904767
},
"address": {
"streetName": "Jalan Taman Anggrek",
"municipality": "Jakarta",
"municipalitySecondarySubdivision": "Pegadungan",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"postalCode": "11830",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek, Jakarta, DKI Jakarta 11830",
"localName": "Jakarta"
},
"position": {
"lat": -6.136057,
"lon": 106.70806
},
"viewport": {
"topLeftPoint": {
"lat": -6.13557,
"lon": 106.70714
},
"btmRightPoint": {
"lat": -6.13729,
"lon": 106.70845
}
}
},
{
"type": "Street",
"id": "t0MH6wK2X9--I6Tp_RgEoA",
"score": 0.5255216875130431,
"matchConfidence": {
"score": 0.5255216875130431
},
"address": {
"streetName": "Jalan Apartemen Taman Anggrek",
"municipalitySubdivision": "Grogol Petamburan",
"municipality": "Jakarta",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Apartemen Taman Anggrek, Grogol Petamburan Sub District, Jakarta, DKI Jakarta",
"localName": "Jakarta"
},
"position": {
"lat": -6.1768413,
"lon": 106.7932977
},
"viewport": {
"topLeftPoint": {
"lat": -6.17672,
"lon": 106.79315
},
"btmRightPoint": {
"lat": -6.17701,
"lon": 106.79341
}
}
},
{
"type": "Street",
"id": "XLfQpcQQ-DJbx7GDwZPGbg",
"score": 0.5431748232904767,
"matchConfidence": {
"score": 0.5431748232904767
},
"address": {
"streetName": "Jalan Taman Anggrek 3",
"municipality": "Jakarta",
"municipalitySecondarySubdivision": "Pegadungan",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"postalCode": "11830",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek 3, Jakarta, DKI Jakarta 11830",
"localName": "Jakarta"
},
"position": {
"lat": -6.136128,
"lon": 106.707467
},
"viewport": {
"topLeftPoint": {
"lat": -6.13582,
"lon": 106.70725
},
"btmRightPoint": {
"lat": -6.13645,
"lon": 106.70772
}
}
},
{
"type": "Street",
"id": "jjrxkKez9DkDNFXBwHnJsw",
"score": 0.5431748232904767,
"matchConfidence": {
"score": 0.5431748232904767
},
"address": {
"streetName": "Jalan Taman Anggrek 1",
"municipality": "Jakarta",
"municipalitySecondarySubdivision": "Pegadungan",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"postalCode": "11830",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek 1, Jakarta, DKI Jakarta 11830",
"localName": "Jakarta"
},
"position": {
"lat": -6.135312,
"lon": 106.707003
},
"viewport": {
"topLeftPoint": {
"lat": -6.13506,
"lon": 106.70652
},
"btmRightPoint": {
"lat": -6.13606,
"lon": 106.70806
}
}
},
{
"type": "Street",
"id": "4PS1aeVhnKNSzI4sP6W9PA",
"score": 0.5431748232904767,
"matchConfidence": {
"score": 0.5431748232904767
},
"address": {
"streetName": "Jalan Taman Anggrek 4",
"municipality": "Jakarta",
"municipalitySecondarySubdivision": "Pegadungan",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"postalCode": "11830",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek 4, Jakarta, DKI Jakarta 11830",
"localName": "Jakarta"
},
"position": {
"lat": -6.135612,
"lon": 106.70671
},
"viewport": {
"topLeftPoint": {
"lat": -6.13546,
"lon": 106.70659
},
"btmRightPoint": {
"lat": -6.13577,
"lon": 106.7068
}
}
},
{
"type": "Street",
"id": "YfWtCSeeZElPofaoDjOWWw",
"score": 0.5431748232904767,
"matchConfidence": {
"score": 0.5431748232904767
},
"address": {
"streetName": "Jalan Taman Anggrek 6",
"municipality": "Jakarta",
"municipalitySecondarySubdivision": "Pegadungan",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"postalCode": "11830",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek 6, Jakarta, DKI Jakarta 11830",
"localName": "Jakarta"
},
"position": {
"lat": -6.13631,
"lon": 106.707352
},
"viewport": {
"topLeftPoint": {
"lat": -6.13577,
"lon": 106.7066
},
"btmRightPoint": {
"lat": -6.13631,
"lon": 106.70735
}
}
},
{
"type": "Street",
"id": "wyCGm7qcqxx3BIFHTk8pcA",
"score": 0.5431748232904767,
"matchConfidence": {
"score": 0.5431748232904767
},
"address": {
"streetName": "Jalan Taman Anggrek Timur",
"municipalitySubdivision": "Kembangan",
"municipality": "Jakarta",
"municipalitySecondarySubdivision": "Meruya Selatan",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"postalCode": "11610",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek Timur, Kembangan Sub District, Jakarta, DKI Jakarta 11610",
"localName": "Jakarta"
},
"position": {
"lat": -6.2103757,
"lon": 106.7235781
},
"viewport": {
"topLeftPoint": {
"lat": -6.20958,
"lon": 106.72352
},
"btmRightPoint": {
"lat": -6.21158,
"lon": 106.7238
}
}
},
{
"type": "Street",
"id": "DrVxOlIhsTKTNFNNLOpVcA",
"score": 0.5431748232904767,
"matchConfidence": {
"score": 0.5431748232904767
},
"address": {
"streetName": "Jalan Taman Anggrek 5",
"municipality": "Jakarta",
"municipalitySecondarySubdivision": "Pegadungan",
"countrySubdivision": "DKI Jakarta",
"countrySubdivisionName": "DKI Jakarta",
"countrySubdivisionCode": "JK",
"postalCode": "11830",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek 5, Jakarta, DKI Jakarta 11830",
"localName": "Jakarta"
},
"position": {
"lat": -6.136128,
"lon": 106.707467
},
"viewport": {
"topLeftPoint": {
"lat": -6.13561,
"lon": 106.70671
},
"btmRightPoint": {
"lat": -6.13613,
"lon": 106.70747
}
}
},
{
"type": "Street",
"id": "M1czdmduvKGkS8XruW-URQ",
"score": 0.49696142544179217,
"matchConfidence": {
"score": 0.49696142544179217
},
"address": {
"streetName": "Taman Anggrek",
"municipalitySubdivision": "Tambun Selatan",
"municipality": "Kota Bekasi",
"municipalitySecondarySubdivision": "Tridayasakti",
"countrySubdivision": "Jawa Barat",
"countrySubdivisionName": "Jawa Barat",
"countrySubdivisionCode": "JB",
"postalCode": "17510",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Taman Anggrek, Tambun Selatan Sub District, Kota Bekasi, Jawa Barat 17510",
"localName": "Kota Bekasi"
},
"position": {
"lat": -6.2513029,
"lon": 107.0749942
},
"viewport": {
"topLeftPoint": {
"lat": -6.25087,
"lon": 107.07424
},
"btmRightPoint": {
"lat": -6.25179,
"lon": 107.0759
}
}
},
{
"type": "Street",
"id": "rsaBWZdCA93fhEH5ZIKFng",
"score": 0.49696142544179217,
"matchConfidence": {
"score": 0.49696142544179217
},
"address": {
"streetName": "Jalan Taman Anggrek",
"municipalitySubdivision": "Bojongloa Kaler",
"municipality": "Bandung",
"municipalitySecondarySubdivision": "Suka Asih",
"countrySubdivision": "Jawa Barat",
"countrySubdivisionName": "Jawa Barat",
"countrySubdivisionCode": "JB",
"postalCode": "40231",
"countryCode": "ID",
"country": "Indonesia",
"countryCodeISO3": "IDN",
"freeformAddress": "Jalan Taman Anggrek, Bojongloa Kaler Sub District, Bandung, Jawa Barat 40231",
"localName": "Bandung"
},
"position": {
"lat": -6.9315985,
"lon": 107.5862828
},
"viewport": {
"topLeftPoint": {
"lat": -6.9299,
"lon": 107.58566
},
"btmRightPoint": {
"lat": -6.9339,
"lon": 107.5877
}
}
}
]
}

If you see in the above JSON data, we need to focus on the results > take the first highest possibility result (always the first result) > get the position, and use it for the latitude and longitude in the system.

Next, we will discuss the ValidateCheckIn business logic. Here, we will use the Latitude and Longitude (planning), and compare it with the Actual Latitude and Longitude. After the checking, we need to see if the distance is below the configuration (CheckInValidationInKilometers). If yes, then we will set the Valid Check In to Yes, and set the Difference for analysis purposes.

Once the plugins are ready, you also need to register the Plugin Steps!

Plugin Steps Geolocation

Canvas App

For the Canvas App, I created the below UI:

https://edge.aditude.io/safeframe/1-1-1/html/container.html

Canvas App UI

Next, on the ScreenMap.OnVisible, I put the code below:


Set(varRecordId, Param("RecordId"));
Set(RecordId, GUID(varRecordId));

Set(CurrentRecord, LookUp(Visits, 'Activity' = RecordId));

ClearCollect(Records, []);

If(
    !IsBlank(CurrentRecord.Longitude) && !IsBlank(CurrentRecord.Latitude),
    Collect(
        Records,
        {
            Long: CurrentRecord.Longitude,
            Lat: CurrentRecord.Latitude,
            Label: "Planning",
            Icon: "Flag-Triangle"
        }
    )
);

If(
    !IsBlank(CurrentRecord.'Actual Latitude') && !IsBlank(CurrentRecord.'Actual Longitude'),
    Collect(
        Records,
        {
            Long: CurrentRecord.'Actual Longitude',
            Lat: CurrentRecord.'Actual Latitude',
            Label: "Actual",
            Icon: "Market-Flat"
        }
    )
);
UpdateContext({showMap: true});

The above code will take the Param(“RecordId”) and get the record from Dataverse. Then, if the Longitude and Latitude data exist (planning), then we will append a new record in the Array that will be set as the data source of the map. And, the same goes for Actual Longitude and Actual Latitude. If the data exists, then we will push that data as “Actual”.

Next, we just need to set:

  • Map.Items to the “Records”
  • Map.ItemsIcons to “Icon”
  • Map.ItemsLabels to “Label”
  • Map.ItemsLatitudes to “Lat”
  • Map.ItemsLongitudes to “Long”

Once you’re done, you can save and publish the Canvas App. Go to details, and you can get the URL of the Canvas App and store it in an Environment Variable: VisitCanvasAppUrl.

Visit Form

This is the design of the Visit Form on the General tab:

General Tab

Next, I added a new section with an iframe control to show our Canvas App:

Map Section

 

JavaScript

I also created JS with the following logic:


var formVisit = formVisit || {};

(function () {
var getEnvironmentVariable = async function (formContext, name) {
var fetchXml = `

`;
var result = await Xrm.WebApi.retrieveMultipleRecords("environmentvariabledefinition", "?fetchXml=" + encodeURIComponent(fetchXml));
return result.entities.length > 0 ? (result.entities[0]["environmentvariablevalue.value"] || result.entities[0]["defaultvalue"]) : null;
};

var setIFrameSrc = async function (formContext) {
var canvasAppUrl = await getEnvironmentVariable(formContext, "VisitCanvasAppUrl");
if (!canvasAppUrl) return;

var recordId = formContext.data.entity.getId().replace("{", "").replace("}", "");
canvasAppUrl = canvasAppUrl.replace("{RecordId}", recordId);

var iframeControl = formContext.getControl("IFRAME_canvasapp");
if (iframeControl) {
iframeControl.setSrc(canvasAppUrl);
}
};

this.onLoad = async function (context) {
var formContext = context.getFormContext();
// Ensure longitude and latitude are always submitted
formContext.getAttribute("ins_actuallongitude").setSubmitMode("always");
formContext.getAttribute("ins_actuallatitude").setSubmitMode("always");

await setIFrameSrc(formContext);
};

this.checkInVisible = function (formContext) {
var currentUser = Xrm.Utility.getGlobalContext().userSettings.userId;
var userRef = formContext.getAttribute("ins_userid").getValue();
if (!userRef) return false;
if (userRef[0].id.toLowerCase() !== currentUser.toLowerCase()) return false;

var stateCode = formContext.getAttribute("statecode").getValue();
var longitude = formContext.getAttribute("ins_longitude").getValue();
var latitude = formContext.getAttribute("ins_latitude").getValue();
return stateCode === 0 && longitude && latitude;
};

this.checkInSelect = function (formContext) {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(function (position) {
formContext.getAttribute("ins_actuallongitude").setValue(position.coords.longitude);
formContext.getAttribute("ins_actuallatitude").setValue(position.coords.latitude);
formContext.data.entity.save();

setIFrameSrc(formContext);
});
};
}).apply(formVisit);

On the form.OnLoad, we will set the IFRAME_canvasapp with the combination of the Environment Variable (VisitCanvasAppUrl) with the current visit RecordId.

Then, we also have 2 other functions for visibility purposes (checkInVisible – will only show if the login user is the User in the Visit record, and we also add some conditions), and when a Salesperson clicks the Check In button (checkInSelect function).

The reason I created a custom ribbon instead of using the Canvas App button is that we are embedding the Canvas App into the MDA. In the eyes of Security, the function that is invoked from the Canvas App is considered CORS (Cross-Origin Resource Sharing). Hence, the easiest way to accomplish this is through JS instead!

Demo

Here is the demo:

Demo GIF

 

Source

Raharjo, T (04/02/2026) Geolocation in Power Apps: Translating Addresses and Validating Check-Ins. Geolocation in Power Apps: Translating Addresses and Validating Check-Ins – Temmy Wahyu Raharjo