PowerApp Portal: Change your Profile Photo using The Portal WebAPI

Disclaimer: The Web API is still in preview, don’t use in production just yet! More info here.

The New Portal WebAPI opened so many doors to customize the portal outside the normal entity forms, web forms , entity lists etc.

You remember that Profile Photo container in the profile pages on the portal? Almost every client I worked with thought it is a working feature but it is not. We end up hiding this container using CSS to remove that confusion.

With the introduction of WebAPI, now we can do it with a supported way.

On the contact entity, there is a field called entityimage, this field is a byte array that holds the picture of the contact. We have two cases to worry about, on the load of the page, we need to read the image from the logged-in contact record. Also, we need to add an upload button to allow portal users to change their photos. We will end up with something like this:

Before starting, we need to setup the portal for the web api and to expose the contact entity properly.

  1. Make sure you have an entity permission setup that allows reading and updating contact. This should be the default for the logged in user since they can modify their profile data.
  2. To enable the contact entity for WebAPI, you need to create the following site settings:
    • site setting name: webapi/contact/enabled value:true
    • site setting name: webapi/contact/fields value:* (Notice I put * to have all fields but if you want to only access the entityimage field, just list the comma separated logical names of the fields you want exposed). If you have hard time knowing the field name, use Power App Portal Web API Helper plugin on XRM toolbox to enable/disable fields and entities for portal.
  3. Make sure that your portal is 9.2.6.41 or later.

There is a piece of code that you need for all of your web api calls. In the normal situation, I prefer to have this piece of code in a web template or in a web file that gets loaded on the website level. For our scenario I will include this piece of code as part of the solution so don’t worry about copying it now as it will be part of the final code. Basically, this piece of code takes your API Request, adds an authentication header and send and AJAX request to Dataverse, nothing fancy.

(function(webapi, $){
		function safeAjax(ajaxOptions) {
			var deferredAjax = $.Deferred();
	
			shell.getTokenDeferred().done(function (token) {
				// add headers for AJAX
				if (!ajaxOptions.headers) {
					$.extend(ajaxOptions, {
						headers: {
							"__RequestVerificationToken": token
						}
					}); 
				} else {
					ajaxOptions.headers["__RequestVerificationToken"] = token;
				}
				$.ajax(ajaxOptions)
					.done(function(data, textStatus, jqXHR) {
						validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
					}).fail(deferredAjax.reject); //AJAX
			}).fail(function () {
				deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
			});
	
			return deferredAjax.promise();	
		}
		webapi.safeAjax = safeAjax;
	})(window.webapi = window.webapi || {}, jQuery)

Moving on. As we said before, we need to tackle the page load case and the upload button case.

For the page load, we need to query the entityimage using FetchXML, the following script retrieves the field and assign its value to entityImageBytes liquid variable.

 
 <!--Get the user profile photo (entityimage)-->
 {% fetchxml contactImageQuery %}
     <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
        <entity name="contact" >
            <attribute name="entityimage" />
            <attribute name="contactid" />
            <filter type="and">
            <condition attribute="contactid" operator="eq" uitype="contact" value="{{ user.id }}" />
            </filter>
        </entity>
    </fetch>
{% endfetchxml %}

{% assign entityImageBytes = contactImageQuery.results.entities[0].entityimage %}

Next, we need to convert these bytes to a format that the Html “img” understands, in this case, we need to convert it to a base64 string. In the below two java script function, we do exactly that. Thanks to Franco Musso’s blog, where I borrowed part of the logic to do the format conversion that saved me some googling time 🙂

The loadProfileImage function, finds the image tag that resides on the profile page. I had to do some other styling for the div around the image but it is up to you. Then, that byte array we got through FetchXML is converted to a long comma separated string that gets fed to bytesStringToBase64 function that encodes that byte sequence into a base64 format. Once we get that base64 string, we assign it as the “src” attribute of the “img” tag. At that moment, you should have the profile picture populated with an image if it exists.

function loadProfileImage() {
    // Select the profile photo and its container, style them as needed
    var profileImgWell = $($(".well")).css("padding", "1px");
    var profileImg = $($(".well a img"))[0];
    $(profileImg).css("width", "100%");
    $(profileImg).css("height", "100%");

    // get the bytes returned by the fetxhxml query and join them to form a single string
    var bytesData = "{{entityImageBytes| | join: ','}}";

    // convert the byte array to base64 string and assign it to the src attribute of the profile image
    var base64String = bytesData ? bytesStringToBase64(bytesData) : null;
    if (base64String) {
        profileImg.src = 'data:image/jpeg;base64,' + base64String;
    }

}

function bytesStringToBase64(bytesString) {
    if (!bytesString) return null;
    var uarr = new Uint8Array(bytesString.split(',').map(function(x) {
        return parseInt(x);
    }));
    return btoa(String.fromCharCode.apply(null, uarr));
}

Next, we need to add the ability of uploading a photo by the logged-in user. To do that, we will add some HTML to add the upload button and the file selector. On the file input, you notice that we call a function (convertImageFileToBase64String) that converts the file into a base64 string, why is that? because when we update the image file in Dataverse, the JSON payload of the UPDATE request needs the image to be in base64 format.

<div style="display:inline-block;">
    <label>Update your profile photo </label>
    <input type="file" name="file" id="file" onchange="convertImageFileToBase64String(this)"/>
    <div id="uploadPhotoDiv"  style="visibility:hidden">
    <button type="button" onclick="onUploadClicked()" id="uploadPhoto">Upload</button>
    </div>
<div>

The convertImageFileToBase64String function basically reads the file and convert it to a base64 string using the FileReader.readAsDataURL method, the result is stored in image_file_base64 variable, we need that later when we do the UPDATE request. You can be more professional than me and avoid the public variable and make the function return a promise instead of this sketchy method 🙂

var image_file_base64;
function convertImageFileToBase64String(element) {
    var file = element.files[0];
    var reader = new FileReader();
    reader.onloadend = function() {
        image_file_base64 = reader.result;
        image_file_base64 = image_file_base64.substring(image_file_base64.indexOf(',') + 1);
    }
    reader.readAsDataURL(file);
}

We only have on thing left, the upload button. This button should take the base64 image that we just converted when we uploaded the image and send it to Dataverse using an UPDATE request. the image_file_base64 is already in the format we want, the only thing we need to do is to call our AJAX Wrapper we mentioned in the beginning and provide it with AJAX request options as shown below. Notice that on success of the call, I optimistically update the image without the need to refresh the page.

function onUploadClicked(){
        $("#uploadPhoto").text("Uploading...");
        webapi.safeAjax({
            type: "PATCH",
            url: "/_api/contacts({{user.id}})",
            contentType: "application/json",
            data: JSON.stringify({
                "entityimage": image_file_base64
            }),
            success: function(res) {
                $("img").first().attr('src', 'data:image/png;base64,' + image_file_base64);
                $("#uploadPhotoDiv").css("visibility","hidden");
                $("#uploadPhoto").text("Upload");
               }
        });
}

Now to the full code and how to use in a two steps in your portal.

 
 <!--Get the user profile photo (entityimage)-->
 {% fetchxml contactImageQuery %}
     <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
        <entity name="contact" >
            <attribute name="entityimage" />
            <attribute name="contactid" />
            <filter type="and">
            <condition attribute="contactid" operator="eq" uitype="contact" value="{{ user.id }}" />
            </filter>
        </entity>
    </fetch>
{% endfetchxml %}

{% assign entityImageBytes = contactImageQuery.results.entities[0].entityimage %}
<div style="display:inline-block;">
    <label>Update your profile photo </label>
    <input type="file" name="file" id="file" onchange="convertImageFileToBase64String(this)"/>
    <div id="uploadPhotoDiv"  style="visibility:hidden">
    <button type="button" onclick="onUploadClicked()" id="uploadPhoto">Upload</button>
    </div>
<div>
<script>
$(document).ready(function() {
    // Load the profile photo from dataverse on document ready
    loadProfileImage();
});

// ajax wrapper provided by Microsoft.
(function(webapi, $){
        function safeAjax(ajaxOptions) {
            var deferredAjax = $.Deferred();
     
            shell.getTokenDeferred().done(function (token) {
                // add headers for AJAX
                if (!ajaxOptions.headers) {
                    $.extend(ajaxOptions, {
                        headers: {
                            "__RequestVerificationToken": token
                        }
                    }); 
                } else {
                    ajaxOptions.headers["__RequestVerificationToken"] = token;
                }
                $.ajax(ajaxOptions)
                    .done(function(data, textStatus, jqXHR) {
                        validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
                    }).fail(deferredAjax.reject); //AJAX
            }).fail(function () {
                deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
            });
     
            return deferredAjax.promise();  
        }
        webapi.safeAjax = safeAjax;
    })(window.webapi = window.webapi || {}, jQuery)

function onUploadClicked(){
        $("#uploadPhoto").text("Uploading...");
        webapi.safeAjax({
            type: "PATCH",
            url: "/_api/contacts({{user.id}})",
            contentType: "application/json",
            data: JSON.stringify({
                "entityimage": image_file_base64
            }),
            success: function(res) {
                $("img").first().attr('src', 'data:image/png;base64,' + image_file_base64);
                $("#uploadPhotoDiv").css("visibility","hidden");
                $("#uploadPhoto").text("Upload");
                
            }
        });
}
function loadProfileImage() {
    // Select the profile photo and its container, style them as needed
    var profileImgWell = $($(".well")).css("padding", "1px");
    var profileImg = $($(".well a img"))[0];
    $(profileImg).css("width", "100%");
    $(profileImg).css("height", "100%");

    // get the bytes returned by the fetxhxml query and join them to form a single string
    var bytesData = "{{entityImageBytes| | join: ','}}";

    // convert the byte array to base64 string and assign it to the src attribute of the profile image
    var base64String = bytesData ? bytesStringToBase64(bytesData) : null;
    if (base64String) {
        profileImg.src = 'data:image/jpeg;base64,' + base64String;
    }

}

function bytesStringToBase64(bytesString) {
    if (!bytesString) return null;
    var uarr = new Uint8Array(bytesString.split(',').map(function(x) {
        return parseInt(x);
    }));
    return btoa(String.fromCharCode.apply(null, uarr));
}


var image_file_base64;

function convertImageFileToBase64String(element) {

    var file = element.files[0];
    var reader = new FileReader();
    reader.onloadend = function() {
        image_file_base64 = reader.result;
        image_file_base64 = image_file_base64.substring(image_file_base64.indexOf(',') + 1);
        $("#uploadPhotoDiv").css("visibility","visible");
    }
    reader.readAsDataURL(file);
}

</script>

Let’s assume you pasted the whole code in a web template called “Contact Photo Uploader”, open your profile page or child page on the portal as an administrator, click this little edit icon on the page copy

Click on the HTML source button in the dialog that appears

and paste this line {%include “Contact Photo Uploader”%} and save the html snippet.

That’s it, this will include the whole web template we just built on the page and the result should be something like this without my photo of course :). Note that the upload button will only appear once you select the file.