Skip to main content

Notifications

Dynamics 365 Community / Blogs / AX for Retail / Communicating with Retail S...

Communicating with Retail Server from Xamarin application

This is to continue set of articles describing how to communicate with Retail Server (RS) from different types of applications. Previous 2 articles

How to access Retail Server in managed code.

How to access Retail Server in TypeScript(JavaScript) application.

showed how to access RS in regular .NET app and in JS one. This article will demonstrate the communication initiated by Xamarin app. I will use Android app just because it is simpler to replicate comparing to iOS because in the later case we would need a Mac for a Build Host purposes.

Basics of communications with RS were described in previous articles so I will not repeat it here. We will leverage the same familiar Retail Server Proxy library which was described earlier and which is also used by eCommerce. It is possible to use that library in Xamarin apps because the library is Portable.

The benefits of using Retail Server Proxy are obvious:

  • You don't need to learn new API, instead you can leverage your knowledge and experience you got while working with eCommerce
  • When/if you change/extend RS API, you will be able to easily rebuild your RS/proxy and it will reflect the changes you did on a server side
  • This is, logically, the same proxy which is leveraged by Cloud POS and Modern POS. Yes, the implementation are technically different - one is .NET library and 2nd one TypeScript/JS but both of them use the same pattern and basically the same types/methods, so, if you are investing in any of those you are in fact investing in both.
  • You don't need to spend time generating your own ODATA client proxy, just use the one shipped out of the box

As was done in other articles let's create simple app which will query RS for a Channel information and then load Channel's Navigational Hierarchy. Note that the purpose of the code below is to share minimal required amount of information so you are able start consuming the proxy from your Xamarin app, to keep the article simple and short the code doesn't demonstrate best design approaches for creating those types of the applications that is out of scope.

Before proceeding please make sure

  • your Visual Studio 2015 is updated (as of today that is Update 3 and hotfixes)
  • you have latest version (with all updates) of Xamarin For Visual Studio 2015.

1. Create the project in VS2015: File->New->Project->Blank Xaml App (Xamarin.Forms Shared). As a result a solution with multiple projects will be created. If you want Android only you can then remove other projects corresponding to other platforms (iOS, Windows, ...). In my case I kept just 2 projects - the one with the Shared code and the one with .Droid extension.

2. Modify the .Droid project by adding references to these dlls (for the location refer to the article mentioned at the beginning of this blog):

Microsoft.Dynamics.Commerce.RetailProxy.dll

Microsoft.OData.Client.dll

3. Open the file (in the Shared project) MainPage.xaml, remove the Label from there and substitute it with this content:

  <StackLayout>
    <Label x:Name="lblError" TextColor="Red" />
    <ListView x:Name="lvCategories">
      <ListView.ItemTemplate>
        <DataTemplate>
          <TextCell Text="{Binding Name}" />
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </StackLayout>

Here we are adding a label, to display any possible errors while communicating with RS, as well as List View - the container for a set of navigational categories.

4. Modify MainPage.xaml.cs:

a) Add an additional using directive

using Microsoft.Dynamics.Commerce.RetailProxy;


 this is to deal with types defined in Retail Proxy DLL.

b) Adding the following function:

 private async Task GetChannelInformation()
        {
            RetailServerContext context = RetailServerContext.Create(RetailServerUrl, OperatingUnitNumber);
            ManagerFactory factory = ManagerFactory.Create(context);
            IOrgUnitManager manager = factory.GetManager<IOrgUnitManager>();

            // Reading channel's configuration. This is needed to find out the channel's ID.
            ChannelConfiguration channelConfig = await manager.GetOrgUnitConfiguration();

            ICategoryManager categoryManager = factory.GetManager<ICategoryManager>();
            PagedResult<Category> categories = await categoryManager.GetCategories(channelConfig.RecordId, new QueryResultSettings { Paging = new PagingInfo { Skip = 0, Top = 100 } });

            lvCategories.ItemsSource = categories;
        }


 

The function creates an instance of Retail Server Proxy, then creates 2 managers (IOrgUnitManager and ICategoryManager) used to extract channel information as well as navigational hierarchy, then it calls methods on those managers and finally assigns the ListView's property ItemsSource to a list of categories downloaded from the server.

c) Modify the constructor by invoking the function above while handling possible errors, so, the constructor should look like:

        public MainPage()
        {
            InitializeComponent();

            Task task = GetChannelInformation();
            task.ContinueWith(t =>
            {
                lblError.Text = task.Exception.InnerExceptions.First().Message;

            }, TaskContinuationOptions.OnlyOnFaulted);
        }

d) Add 2 constants corresponding to the Retail Server's URL and OUN (Operating Unit Number) corresponding to your channel.

 static readonly Uri RetailServerUrl = new Uri("<YourRsEndPointIsHere>");
 const string OperatingUnitNumber = "068";


Don't forget to update the value inside the Uri's  constructor.

Once you run the application you should see the list of the categories:

ProxyXamarin.png

So far we have done anonymous calls only, let's see how to make RS calls in a context of authenticated user. The theory, as well as an example for regular .NET can be found in Basics of building native client capable of C2 authentication . To achieve that in Xamarin app the same principles apply: we will redirect a user to an Identity Provider (Google in this case) to type credentials, once the credentials are validated our app will be provided with an id_token which will eventually be used to annotate every request to RS. Our app will create/read a Customer associated with Google account. Let's modify our app's sources:

1. In the file MainPage.xaml replace the content of ContentPage with:

  <StackLayout>
    <Label x:Name="lblCustomer" />
    <WebView x:Name="browser" HeightRequest="600" />
  </StackLayout>

2. Add the following constants and fields to the class MainPage:

        const string RequestFormatString = "{0}?client_id={1}&redirect_uri={2}&state={3}&scope={4}&response_type={5}&nonce={6}";
        const string AuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
        const string ClientId = "58340890588-7fk40bvjjn5n34f1sd9e2ckhnp41gdtj.apps.googleusercontent.com";
        const string RedirectUri = "https://usnconeboxax1ecom.cloud.onebox.dynamics.com/Pages/OauthV2Redirect/OauthV2Redirect.aspx";

        const string ErrorCommerceIdentityNotFound = "Microsoft_Dynamics_Commerce_Runtime_CommerceIdentityNotFound";

        private string state;
        private string nonce;

Those are needed to prepare auth request and handle customer creation while contacting RS.

3. Comment out the constructor you have and paste this one:

        public MainPage()
        {
            InitializeComponent();
            state = Guid.NewGuid().ToString();
            nonce = Guid.NewGuid().ToString();
            browser.Navigated += Browser_Navigated;
            string authRequest = string.Format(RequestFormatString, AuthEndpoint, ClientId, RedirectUri, state, "openid", "id_token", nonce);
            browser.Source = authRequest;
        }


The purpose of the code above mainly to setup WebView.

Comment out the method GetChannelConfiguration.

4. Add the following methods:

        private void Browser_Navigated(object sender, WebNavigatedEventArgs e)
        {
            string idToken = GetQueryValue(e.Url, "&id_token");
            if (idToken != null)
            {
                string receivedState = GetQueryValue(e.Url, "#state");
                if (receivedState != state)
                {
                    throw new UserAuthenticationException();
                }

                browser.IsVisible = false;

                ManagerFactory factory = CreateManagerFactory(idToken);
                Customer customer = Task.Run<Customer>(async () => await GetOrCreateCustomer(factory)).Result;
                lblCustomer.Text = "Account Number: " + customer.AccountNumber;
            }
        }

        ManagerFactory CreateManagerFactory(string idToken)
        {
            RetailServerContext context = RetailServerContext.Create(RetailServerUrl, OperatingUnitNumber, idToken);
            ManagerFactory managerFactory = ManagerFactory.Create(context);
            return managerFactory;
        }

        async Task<Customer> GetOrCreateCustomer(ManagerFactory factory)
        {
            const int CustomerTypePerson = 1;

            // Creating instance of Customer's Manager to send requests specific to Customer entity.
            ICustomerManager customerManager = factory.GetManager<ICustomerManager>();

            Customer customer = null;
            // Trying to read existing customer mapped to an external user's ID provided by the Identity Provider via Id Token.
            try
            {
                customer = await customerManager.Read(string.Empty);
            }
            catch (UserAuthorizationException exception)
            {
                if (exception.ErrorResourceId != ErrorCommerceIdentityNotFound)
                {
                    // Something went wrong, let the exception go up.
                    throw;
                }
            }

            // If no customer was found create a new one.
            if (customer == null)
            {
                long salt = DateTime.Now.Ticks;
                customer = new Customer { AccountNumber = string.Empty, Email = "mail" + salt + "@contoso.com", FirstName = "Demo", CustomerTypeValue = CustomerTypePerson };
                customer = await customerManager.Create(customer);
            }

            return customer;
        }

        static string GetQueryValue(string source, string key)
        {
            string result = null;
            string prefix = key + "=";
            string expression = prefix + "(.*?)&";
            System.Text.RegularExpressions.Regex regEx = new System.Text.RegularExpressions.Regex(expression);
            string parsedValue = regEx.Match(source).Value;
            if (parsedValue != string.Empty)
            {
                parsedValue = parsedValue.Substring(prefix.Length);
                parsedValue = parsedValue.Replace("&", string.Empty);
                result = parsedValue;
            }
            return result;
        }


 The purpose of Browser_Navigated is to provide a callback invoked when navigation occurs in a browser. It is called multiple times as user walks through sign-in process, for each call we validate presence of id_token and validity of the state parameter. In addition, you can also consider adding extra security by validating the nonce to make sure its value (found in the token's claims) matches the one used to prepare original request, I am not aware of classes provided by Xamarin out of the box which could help accessing JWT content but you could probably consider using something like https://github.com/senzacionale/xamarin-jose-jwt if you do decide to add this extra security (RS validates token's signature as well as few mandatory claims).

So, once the token is available that callback creates an instance of ManagerFactory, then Reads (if exists) or Creates (if doesn't exist) a corresponding Customer and finally displays its Account Number in the form.

Note that the constants above contain the Client Id corresponding to the Google application you don't control, therefore don't rely on that a lot, it is probably OK to use it for demos (if that app still exists by the time you are reading this) but for more or less serious tasks you should create your own Web App in Google (https://console.developers.google.com/) and then update the constants (ClientId and RedirectUri) with the values you own. Just in case, one more time: RS supports OpenID Connect, therefore, you don't have to use Google, you can use any Identity Provider which supports OpenID Connect. 

This should help you bootstrap creating your Xamarin applications consuming Retail Server. Real application would of course make much bigger number of calls, if you are building C2 (Customer facing) application then consider reviewing eCommerce Online Store which can be found in the SDK on your LCS box, it uses exactly the same proxy, therefore you should have problems identifying methods to use in your own application.

Comments

*This post is locked for comments

  • SergeyP Profile Picture SergeyP 2,928
    Posted at

    @Akhilesh Chobey are you creating the project in Visual Studio 2017 by using

    Visual C#->Android

    ?

  • Community Member Profile Picture Community Member Microsoft Employee
    Posted at

    How can I do it in an android project with only .axml layout file? I am trying to build the Retail experience app but stuck at authentication.