
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:

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&pi-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 {RecordId} text will be replaced by JavaScript’s function later. | https://apps.powerapps.com/play/e/7a7fc595-a6c2-ed5d-8e9b-25c76c890a21/a/3abc51ef-ca18-4513-8c7f-92beae0e20d8?tenantId=hehehe80-8fd4-4c0e-9880-36d856fd75e7&hint=yoloca26-f5d8-4ef5-8072-1571433f1ef2&sourcetime=1760183137268&RecordId={RecordId} |
Plugin Code
I created this business logic code:
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 | usingMicrosoft.Xrm.Sdk;usingMicrosoft.Xrm.Sdk.Query;usingSystem;usingSystem.Linq;usingSystem.Net.Http;usingSystem.Text.Json.Serialization;namespaceMapDemoPlugin.Business{ publicclassTranslateAddress { publicTranslateAddress(ILocalPluginContext context) { Context = context; } publicvoidExecute() { vartarget = Context.PluginExecutionContext.InputParameters["Target"] asEntity; if(target == null) return; varazureMapUrl = Context.InitiatingUserService.GetEnvironmentVariableByName<string>("AzureMapUrl"); if(string.IsNullOrEmpty(azureMapUrl)) return; varaddress = target.GetAttributeValue<string>("ins_address"); if(string.IsNullOrEmpty(address)) return; varaddressEncoded = Uri.EscapeDataString(address); azureMapUrl = azureMapUrl.Replace("{address}", addressEncoded); varclient = newHttpClient(); varrequest = newHttpRequestMessage(HttpMethod.Get, azureMapUrl); varresponse = client.SendAsync(request).GetAwaiter().GetResult(); response.EnsureSuccessStatusCode(); varresponseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); if(string.IsNullOrEmpty(responseBody)) thrownewInvalidOperationException("Response body is empty"); Context.TracingService.Trace("URL: {0}", azureMapUrl); Context.TracingService.Trace("Response: {0}", responseBody); vardata = System.Text.Json.JsonSerializer.Deserialize<Response>(responseBody); if(data == null|| data.Results == null|| !data.Results.Any()) thrownewInvalidOperationException("No results found"); target["ins_latitude"] = data.Results[0].Position.Latitude; target["ins_longitude"] = data.Results[0].Position.Longitude; } publicclassResponse { [JsonPropertyName("results")] publicResult[] Results { get; set; } } publicclassResult { [JsonPropertyName("position")] publicPosition Position { get; set; } } publicclassPosition { [JsonPropertyName("lat")] publicdecimalLatitude { get; set; } [JsonPropertyName("lon")] publicdecimalLongitude { get; set; } } publicILocalPluginContext Context { get; } } publicclassValidateCheckIn { publicValidateCheckIn(ILocalPluginContext context) { Context = context; } publicvoidExecute() { vartarget = Context.PluginExecutionContext.InputParameters["Target"] asEntity; if(target == null) return; if(!double.TryParse(Context.InitiatingUserService.GetEnvironmentVariableByName<string>("CheckInValidationInKilometers"), outvarrangeValidation)) return; varcurrentRecord = Context.InitiatingUserService.Retrieve(target.LogicalName, target.Id, newColumnSet("ins_latitude", "ins_longitude", "ins_actuallatitude", "ins_actuallongitude")); varactualLatitude = target.GetAttributeValue<decimal?>("ins_actuallatitude") ?? currentRecord.GetAttributeValue<decimal?>("ins_actuallatitude"); varactualLongitude = target.GetAttributeValue<decimal?>("ins_actuallongitude") ?? currentRecord.GetAttributeValue<decimal?>("ins_actuallongitude"); if(!actualLatitude.HasValue || !actualLongitude.HasValue) return; varlatitude = target.GetAttributeValue<decimal?>("ins_latitude") ?? currentRecord.GetAttributeValue<decimal?>("ins_latitude"); varlongitude = target.GetAttributeValue<decimal?>("ins_longitude") ?? currentRecord.GetAttributeValue<decimal?>("ins_longitude"); if(!latitude.HasValue || !longitude.HasValue) return; varresult = IsWithinDistance((double)latitude.GetValueOrDefault(), (double)longitude.GetValueOrDefault(), (double)actualLatitude.GetValueOrDefault(), (double)actualLongitude.GetValueOrDefault(), rangeValidation, outvardistance); target["ins_difference"] = distance; target["ins_validcheckin"] = result; } privatestaticdoubleCalculateDistance(doublelat1, doublelon1, doublelat2, doublelon2) { constdoubleR = 6371.0; // Radius of Earth in kilometers doublelat1Rad = DegreesToRadians(lat1); doublelon1Rad = DegreesToRadians(lon1); doublelat2Rad = DegreesToRadians(lat2); doublelon2Rad = DegreesToRadians(lon2); doubledlat = lat2Rad - lat1Rad; doubledlon = lon2Rad - lon1Rad; doublea = Math.Sin(dlat / 2) * Math.Sin(dlat / 2) + Math.Cos(lat1Rad) * Math.Cos(lat2Rad) * Math.Sin(dlon / 2) * Math.Sin(dlon / 2); doublec = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); returnR * c; } privatestaticboolIsWithinDistance(doublelat1, doublelon1, doublelat2, doublelon2, doublethresholdKm, outdoubledistance) { distance = CalculateDistance(lat1, lon1, lat2, lon2); returndistance <= thresholdKm; } privatestaticdoubleDegreesToRadians(doubledegrees) { returndegrees * Math.PI / 180.0; } publicILocalPluginContext Context { get; } } publicstaticclassOrganizationServiceExtensions { publicstaticTValue GetEnvironmentVariableByName<TValue>(thisIOrganizationService service, stringenvironmentVariableName) { varquery = newQueryExpression("environmentvariabledefinition") { ColumnSet = newColumnSet("defaultvalue", "schemaname"), TopCount = 1 }; query.Criteria.AddCondition("displayname", ConditionOperator.Equal, environmentVariableName); varchildLink = query.AddLink("environmentvariablevalue", "environmentvariabledefinitionid", "environmentvariabledefinitionid", JoinOperator.LeftOuter); childLink.EntityAlias = "ev"; childLink.Columns = newColumnSet("value"); childLink.Orders.Add(newOrderExpression("createdon", OrderType.Descending)); varresult = service.RetrieveMultiple(query); varenvironmentVariable = result.Entities.Any() ? result.Entities.FirstOrDefault() : newEntity(); varvalue = environmentVariable.Contains("ev.value") ? (TValue)environmentVariable.GetAttributeValue<AliasedValue>("ev.value").Value : environmentVariable.GetAttributeValue<TValue>("defaultvalue"); returnvalue; } }} |
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
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
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:
| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 | varformVisit = formVisit || {};(function() { vargetEnvironmentVariable = asyncfunction(formContext, name) { varfetchXml = `<fetch xmlns:generator='MarkMpn.SQL4CDS'> <entity name='environmentvariabledefinition'> <attribute name='defaultvalue'/> <attribute name='environmentvariabledefinitionid'/> <link-entity name='environmentvariablevalue'to='environmentvariabledefinitionid'from='environmentvariabledefinitionid'alias='environmentvariablevalue'link-type='outer'> <attribute name='value'/> <attribute name='environmentvariablevalueid'/> <order attribute='environmentvariablevalueid'/> </link-entity> <filter> <condition attribute='displayname'operator='eq'value='${name}'/> </filter> <order attribute='environmentvariabledefinitionid'/> </entity> </fetch>`; varresult = awaitXrm.WebApi.retrieveMultipleRecords("environmentvariabledefinition", "?fetchXml="+ encodeURIComponent(fetchXml)); returnresult.entities.length > 0 ? (result.entities[0]["environmentvariablevalue.value"] || result.entities[0]["defaultvalue"]) : null; }; varsetIFrameSrc = asyncfunction(formContext) { varcanvasAppUrl = awaitgetEnvironmentVariable(formContext, "VisitCanvasAppUrl"); if(!canvasAppUrl) return; varrecordId = formContext.data.entity.getId().replace("{", "").replace("}", ""); canvasAppUrl = canvasAppUrl.replace("{RecordId}", recordId); variframeControl = formContext.getControl("IFRAME_canvasapp"); if(iframeControl) { iframeControl.setSrc(canvasAppUrl); } }; this.onLoad = asyncfunction(context) { varformContext = context.getFormContext(); // Ensure longitude and latitude are always submitted formContext.getAttribute("ins_actuallongitude").setSubmitMode("always"); formContext.getAttribute("ins_actuallatitude").setSubmitMode("always"); awaitsetIFrameSrc(formContext); }; this.checkInVisible = function(formContext) { varcurrentUser = Xrm.Utility.getGlobalContext().userSettings.userId; varuserRef = formContext.getAttribute("ins_userid").getValue(); if(!userRef) returnfalse; if(userRef[0].id.toLowerCase() !== currentUser.toLowerCase()) returnfalse; varstateCode = formContext.getAttribute("statecode").getValue(); varlongitude = formContext.getAttribute("ins_longitude").getValue(); varlatitude = formContext.getAttribute("ins_latitude").getValue(); returnstateCode === 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!
Happy CRM-ing
About The Author

Dynamics CRM CE Technical Consultant | Power Platform | Dataverse | Blogger | MVP Business Applications | Loves the concept of Test Driven Development | Certified MCP | Certified Scrum Master
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




