Converting an app for Windows Store without Desktop Converter

Since the Desktop Converter has been deprecated I will try to give a super brief overview how we proceeded to convert apps for Microsoft Store manually.

This is based on part 1 of the series. We are using the folder _assets-winstore which contains the subfolders Assets and Strings that were created in part 1, as well as the resources.pri file and the AppManifest.xml. The app manifest was originally created with the Desktop Converter but we extended and modified it manually from there. It looks like this:

<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap2="http://schemas.microsoft.com/appx/manifest/uap/windows10/2" xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:rescap3="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/3" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2" xmlns:desktop3="http://schemas.microsoft.com/appx/manifest/desktop/windows10/3" xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" xmlns:wincap3="http://schemas.microsoft.com/appx/manifest/foundation/windows10/windowscapabilities/3" IgnorableNamespaces="uap4 wincap3 rescap3 desktop2 desktop3 com">
  <Identity Name="[UWP_PACKAGE_NAME]" ProcessorArchitecture="x86" Publisher="CN=545F4CB0-739D-4B07-A9DA-C97C94EFE557" Version="[UWP_VERSION].0" />
  <Properties>
    <DisplayName>ms-resource:Resources/PackageDisplayName</DisplayName>
    <PublisherDisplayName>ms-resource:Resources/PublisherDisplayName</PublisherDisplayName>
    <Logo>Assets\StoreLogo.png</Logo>
  </Properties>
  <Resources>
    [UWP_LANGUAGES]
    <Resource uap:Scale="100" />
    <Resource uap:Scale="125" />
    <Resource uap:Scale="150" />
    <Resource uap:Scale="200" />
    <Resource uap:Scale="400" />
  </Resources>
  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" MaxVersionTested="[WINSDK_VERSION]" />
  </Dependencies>
  <Capabilities>
    <rescap:Capability Name="runFullTrust" />
    <Capability Name="internetClient" />
  </Capabilities>
  <Applications>
    <Application Id="[UWP_APP_ID]" Executable="[PROJECT_NAME].exe" EntryPoint="Windows.FullTrustApplication">
      <uap:VisualElements DisplayName="ms-resource:Resources/ApplicationDisplayName" Description="ms-resource:Resources/ApplicationDescription" BackgroundColor="transparent" Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\Square44x44Logo.png">
        <uap:DefaultTile ShortName="ms-resource:Resources/TileShortName" Wide310x150Logo="Assets\Wide310x150Logo.png" Square310x310Logo="Assets\Square310x310Logo.png" Square71x71Logo="Assets\Square71x71Logo.png">
          <uap:ShowNameOnTiles>
            <uap:ShowOn Tile="square150x150Logo" />
            <uap:ShowOn Tile="wide310x150Logo" />
            <uap:ShowOn Tile="square310x310Logo" />
          </uap:ShowNameOnTiles>
        </uap:DefaultTile>
      </uap:VisualElements>
      <Extensions>
        <!-- This part should be optional as we were using URI handlers to open our app from a URL -->
        <uap3:Extension Category="windows.appUriHandler">
          <uap3:AppUriHandler>
            <uap3:Host Name="[DOMAIN]" />
          </uap3:AppUriHandler>
        </uap3:Extension>
        <uap:Extension Category="windows.protocol">
          <uap:Protocol Name="[PROJECT_NAME">
            <uap:Logo>Assets\Square310x310Logo.png</uap:Logo>
            <uap:DisplayName>ms-resource:Resources/ApplicationDisplayName</uap:DisplayName>
          </uap:Protocol>
        </uap:Extension>
      </Extensions>
    </Application>
  </Applications>
</Package>

From there we pack the compiled swf project into an appx file like so. This is Ant, but you should be able to convert this back to a regular cmd.

<property name="targetWinstore" value="folder/to/compiled/swf" />
<property name="targetAppx" value="output/appname.appx" />

<target name="bundle-appx">
		
	<!-- copy assets into project -->
	<copy todir="${targetWinstore}" overwrite="true">
		<fileset dir="_assets-winstore"/>
	</copy>
		
	<echo message="Packaging appx for ${gamedir} ..." />
		
	<exec executable="cmd" failonerror="true">			
		<arg value="/c"/>
		<arg value="${WINSDK_BIN}/makeappx.exe"/>
		<arg value="pack"/>
		<arg value="/o"/>
		<arg value="/d"/>
		<arg value="${targetWinstore}"/>
		<arg value="/p"/>
		<arg value="${targetAppx}"/>
	</exec>
		
	<echo message="Signing appx for ${gamedir} ..." />
		
	<exec executable="cmd" failonerror="true">			
		<arg value="/c"/>
		<arg value="${WINSDK_BIN}/signtool.exe"/>
		<arg value="sign"/>
		<arg value="/fd"/>
		<arg value="SHA256"/>
		<arg value="/a"/>
		<arg value="/f"/>
		<arg value="../_cert/desktop-winstore.pfx"/>
		<arg value="/p"/>
		<arg value="XXX"/>
		<arg value="${targetAppx}"/>
	</exec>
		
</target>

That’s it basically. I hope it will be enough to follow along. Let me know if there are issues or info is missing.

Google Play 64 Bits requirement and Adobe Native Extensions

As you may know, Google Play requires all applications to support 64 bits architecture from August 1, 2019. With Harman taking over the Air SDK from Adobe, Air SDK 33 with 64 bits support is available in a closed beta.

Meanwhile, please be aware that all your Adobe Native Extensions (ANEs) will need an update by the ANE maintainer to support the 64 bits build. If the ANEs do not receive an update, you will not be able to use them from August 1 on Google Play.

If your app is depending on any native extensions from providers like Milkman that no longer update their extensions, you will need to replace the ANEs to make sure your app will continue to work in the future. You can start porting right now to be ready to convert to 64 bits when the SDK goes public.

Fortunately, Distriqt has a large arsenal of ANEs and is in the process of updating all of them. They have recently posted a progress / status page where you can check which ANEs are already available.

We are currently in the process of replacing certain extensions from old providers with extensions from Distriqt to be ready for the future. If you are using inhouse or custom ANEs, be sure to get in touch with the maintainers to inform them about the upcoming requirement. Conversion of ANEs can start right now with the current beta SDK. The Air SDK 33 beta is available to anyone that will contact Harman via email to adobe.support@harman.com and requires you submit to the Air 33 beta license agreement.

More info on the Google Play 64 bits requirement is available here. More info on Harman taking over Air from Adobe is available here.

Publishing an Adobe Air app to the Windows Store: In App Purchases

Part II: In-App Payments on Microsoft Store

In this part of the blog I am going through all the steps required to add in-app billing to your Air app on the Microsoft Store. Please start with the first part of the blog if you haven’t read it, yet.

The key component for this part is an Adobe Native Extension (ANE) that supports the Microsoft Store API for in-app purchases. For our games we are using an ANE that the Distriqt team has developed for us. This ANE is available here: https://airnativeextensions.com/extension/com.distriqt.WindowsStore

Foreword

Before we start: I have implemented roughly a dozen different payment services into our games. The API for the Microsoft Store was by far the worst. You can consider yourself lucky if you only care about client-side payments, but if you require server-side verification, things will get dirty. You have been warned.

ANE Project Setup

In your application.xml, add the extensionIDs for the Store payment ANE.

<extensions>
    <extensionID>com.distriqt.WindowsStore</extensionID>
    <extensionID>com.distriqt.Core</extensionID>
</extensions>

Maybe you remember our adt command from Part I:

adt -package -storetype pkcs12 -keystore “cert\desktop.p12” -storepass [PASSWORD] -target bundle air\install.air application.xml -C bin . -extdir “..\_extensions”

In our case, this means we are going to put the native extension files (*.ane) into a directory called _extensions that is one level higher than the application.xml. At the same time, create a directory called _extensionsUnpacked that you place next to the _extensions folder. In this folder, create directories for each ANE that you name like the ane file itself, but with an “Unpacked.” just before the “ane” extension (see example below).

ANE files are just zip folders. Copy each ane into a temporary location, change the filename to zip and unzip each into the respective “Unpacked” directory you created. You should end with a structure like this one:

The Unpacked directory is required when debugging Air software locally on Windows, since adt likes to work with the unzipped directories instead of the ANE files.

While we are at it, the Distriqt ANE comes with a set of Windows *.dll files that are required for the final .exe to run. Create a directory _windowsStoreDll next to the _extensions and _extensionsUnpacked folders and copy all 5 *.dll files there.

I am using FlashDevelop and added a line in the bat scripts to copy these files into the build folder after the exe has been created. Use something like this:

robocopy ../_windowsStoreDll air/install.air

If you are not using FlashDevelop, you just need to make sure that you copy the *.dll files next to your *.exe after the adt command from above was executed.

Store Preparation

Browse to the Windows Dev Center and log into the dashboard to add at least one in-app item for your app that you can use to test purchases. The Windows Dashboard calls them add-ons, but they have support for persistent and consumable items, just like you would expect.

Once you created your item in the dashboard, click that item and find a Store ID below the name. You will need to use this Store ID in order to tell the API which item you want to buy.

If I remember correctly, you will need to submit your app and your add-on to Microsoft in order to make test calls to the API, but feel free to correct me if I’m wrong. At least if you are interested in server-side verification.

Now that you have an item ready, lets have a brief look at the implementation:

public override function initNativePayment() : void
{
	_debug = new TextField();
	_debug.visible = false;
	_debug.width = 400;
	_debug.height = 400;
	_debug.background = true;
			
	_stage.addChild(_debug);
			
	if (WindowsStore.isSupported && !_paymentsInitialized)
	{
		_paymentsInitialized = true;
		WindowsStore.service.setup();
				
		WindowsStore.service.addEventListener(PurchaseEvent.MAKE_PURCHASE_COMPLETE, makePurchaseCompleteHandler);
		WindowsStore.service.addEventListener(PurchaseEvent.MAKE_PURCHASE_ERROR, makePurchaseErrorHandler);
		WindowsStore.service.addEventListener(PurchaseEvent.GET_PURCHASES_COMPLETE, inventoryCompleteHandler);
		WindowsStore.service.addEventListener(PurchaseEvent.GET_PURCHASES_ERROR, inventoryErrorHandler);
		WindowsStore.service.addEventListener(ProductEvent.GET_PRODUCTS_COMPLETE, getProductsCompleteHandler);
		WindowsStore.service.addEventListener(ProductEvent.GET_PRODUCTS_ERROR, getProductsErrorHandler);
		WindowsStore.service.addEventListener(StoreIDEvent.GET_STOREID_COMPLETE, storeIdCompleteHandler);
		WindowsStore.service.addEventListener(StoreIDEvent.GET_STOREID_ERROR, storeIdErrorHandler);
				
		//error logging				
		WindowsStore.service.addEventListener( "log", logHandler);
		WindowsStore.service.addEventListener( ErrorEvent.ERROR, errorHandler);
	}
}

You can see we are going to use a TextField called _debug that can display some debugging information after the app is live. In order to get this past the Microsoft quality control team, make sure the TextField is invisible and only opens when entering a secret code or pressing a special key. This will be a great help to debug the Store payments later if something goes wrong.

In order to access the information of items in your app and display them to the user, start by loading the products that you are interested in:

var items : Array = ["Store ID from before"];
WindowsStore.service.getProducts( items );

This will trigger a ProductEvent.GET_PRODUCTS_COMPLETE that will contain Store information about the items you requested, like name, localized price, etc.

To start the purchase for an item, call this code:

var request:PurchaseRequest = new PurchaseRequest();
request.setProductId("Store ID from before");
WindowsStore.service.makePurchase( request );

This should respond with a PurchaseEvent.MAKE_PURCHASE_COMPLETE or PurchaseEvent.MAKE_PURCHASE_ERROR depending if the user completed the purchase.

In the event handler makePurchaseCompleteHandler you can now consider the purchase successful and either give the item to the user right away or start a server side verification.

If you work on a client-only app, the work-through ends here for you. Congrats on implementing Microsoft Store payments!

Please note: The API calls to Microsoft Store will only function correctly after your Air app has been packaged to an .appx Store Application, not when debugging locally or running the .exe file. Keep this in mind before you start testing your code. Follow the instructions in Part I to package your app.

Server-side verification: Where the trouble starts

Okay we have made it through the client side purchase. To verify the purchase on your server and issue the item on your database or just give a green light to your client to prevent cheating, you will need a (free) Microsoft Azure account.

For some reason I have a hard time finding the proper link to the Azure Dasboard from all the Azure Marketing material. Once you have registered, find the Azure Portal here: portal.azure.com

Azure Active Directory

In order to allow your servers to access information from the Microsoft Store for your users, you will need to setup an Azure Active Directory for your apps. In the Azure Portal, click Azure Active Directory on the left side.

From there, navigate to App Registrations and create a new web app for your server. Follow this guide for step 1 and step 2: https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service

The calls to the Microsoft servers will require a valid token for the application that you have created. Navigate to the application in Azure Active Directory that you have created and find Certificates & Secrets on the left side. Go there and add a new client secret key that will be valid forever. Copy that key and the application ID to create the server side code that generates your token like so:

$url = "https://login.microsoftonline.com/$tenantId/oauth2/token";

$data = array(
	'grant_type' => 'client_credentials',
	'client_id' => $clientId,
	'client_secret' => $clientKey,
	'resource' => 'https://onestore.microsoft.com'
);

$fields = http_build_query($data);
			
$post = curl_init();
			 
curl_setopt($post, CURLOPT_URL, $url);
curl_setopt($post, CURLOPT_POST, count($data));
curl_setopt($post, CURLOPT_POSTFIELDS, $fields);
curl_setopt($post, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($post, CURLOPT_TIMEOUT, 5);
			 
$raw = curl_exec($post);
			 
curl_close($post);
		
$response = json_decode($raw, true);
		
$token = $response['access_token'];
$expires = $response['expires_in'];

Now if you are wondering what the $tenantId is and where to find it, that is a valid question. If you use any language other than english in the dashboard, you are lost in translation and you can just start guessing around. Fortunately I wasted my time on this so you don’t have to. It’s the ID just below the application (client) ID on the overview of the application you have registered in the Azure Active Directory.

Connect Store App with Azure App

Before Azure can query information for your app on behalf of your server, you will need to grant the Azure application we created for your server access to your Microsoft Store application. Once more navigate to the Windows Dev Center and to the dashboard in your browser. Go to Products, click your app, go to Services and select Product collections and purchases. Here, paste the application ID from your Azure App registrations, the same one we used before on the server side code to create the token. Then save the page. This will grant the access for your servers through Azure.

Query and fulfil purchases for users

Okay, almost there. Now whenever your users complete a purchase in the client, you will want to tell your server to verify and fulfill this purchase. Unfortunately, Microsoft decided to not tell your client anything about this purchase that could be used to verify it on the server, not even a transactionID or receipt.

Instead, you will have to query the Windows Store API on your client to receive the “collections ID” of the user. It’s sort of a user ID. But it’s not really a user ID. It only lasts for 90 days and then it will be invalid, so don’t store it anywhere for long term use. Also, this does not work if the user is not logged into the Windows Store with their Microsoft account and you have no way to trigger a login yourself.

Oh by the way: You can only retrieve the collections ID of the user on your client with a valid Azure Active Directory token that we have created on your server in the previous step. And these are valid only short term, so don’t hardcode them. What?!

Summarized, these are the required steps to finish the transaction:

  1. After the purchase has completed on the client, ask your server for a valid collections token. (Not before, user might not be logged into their Microsoft account)
  2. With this token, retrieve user collections ID on the client.
  3. Send this collections ID back to your server. (Do not store it, it will invalidate)
  4. On your server, ask Microsoft servers for any unfulfilled purchases of the user.
  5. Issue the items to the user, for example add virtual currency in your database.
  6. Report purchases as fulfilled to Microsoft.

The code to retrieve the collections ID for the user in AS3 looks like this:

WindowsStore.service.getStoreId(token, yourUserID, "collections");

This will trigger the events StoreIDEvent.GET_STOREID_COMPLETE or StoreIDEvent.GET_STOREID_ERROR. On success, you will be able to access event.storeID in the StoreIDEvent and send it to your server.

To execute correctly, this function requires your app to be published to the Microsoft Store! You can change the access level for the app so that only a selected group of people has access to the app and that it is not visible to other users, but it must be published, otherwise the collections ID will not be returned.

Server Side Example

Here is a small example how to use the collections ID to retrieve and fulfil the purchases for that user. First, we create a general function to call Microsoft servers with an authorization token (the one we created earlier):

private static function makeAuthorizedCall($url, $token, $data)
{
	$fields = json_encode($data);
		
	$headers = array(
		'Authorization: Bearer ' . $token,
		'Host: collections.mp.microsoft.com',
		'Content-Type: application/json',
		'Content-Length: ' . strlen($fields)
	);
		
	$ch = curl_init();
		
	curl_setopt( $ch, CURLOPT_URL, $url );
	curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers);
	curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
		
	curl_setopt( $ch, CURLOPT_POST,           1 );
	curl_setopt( $ch, CURLOPT_POSTFIELDS,     $fields);
		
	$result = curl_exec($ch);
	$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		
	// Close connection
	curl_close($ch);
		
	return $result;
}

Now we can use this function to get all products of a user that have not yet been fulfilled. $uid in this case is the same userID we have used on the client side earlier when asking for the collections ID:

public static function getProducts($uid, $collectionsId, $token)
{
	$url = "https://collections.mp.microsoft.com/v6.0/collections/query";
		
	$userIdentity = array(
		'identityType' => 'b2b',
		'identityValue' => $collectionsId,
		'localTicketReference' => $uid
	);
		
	$data = array(
		'beneficiaries' => array($userIdentity),
		'productTypes' => array('UnmanagedConsumable'),
		'validityType' => 'Valid'

	);
		
	$response = self::makeAuthorizedCall($url, $token, $data);
		
	$result = json_decode($response, true);
		
	return $result;
}

You can parse the response and give the user access to all the products he bought. Here is a small example that iterates over the result set:

$items = WindowsStore::getProducts($uid, $collectionsId, $token);

foreach($items['items'] as $item)
{			
	$winstoreProductId = $item['productId'];
	$transactionId = $item['transactionId'];
			
	error_log("$transactionId, $winstoreProductId");
}

The $winstoreProductId is the very same Store ID we have seen in the Windows Dev Center Dashboard earlier when we created the add-on. You can use it on your server to identify what exactly the user bought.

Once you have issued the item to the user, you need to tell Microsoft that you have fulfilled the contract:

public static function reportFulfilled($uid, $collectionsId, $winstoreProductId, $transactionId, $token)
{		
	$url = "https://collections.mp.microsoft.com/v6.0/collections/consume";
		
	$userIdentity = array(
		'identityType' => 'b2b',
		'identityValue' => $collectionsId,
		'localTicketReference' => $uid
	);
		
$data = array(
		'beneficiary' => $userIdentity,
		'productId' => $winstoreProductId,
		'transactionId' => $transactionId
				
	);
		
	$response = self::makeAuthorizedCall($url, $token, $data);
		
	$result = json_decode($response, true);
		
	return $result;
}

And that’s it, you are done!

A final note: The $token we created will be invalidated after a short while (I believe it’s one hour). The best practice recommends to renew this token with a specific call, but it is also possible to just create a new token with the credentials like we did in the beginning. Just don’t take the shortcut to generate the token on your client, the credentials must remain secret. Don’t include them in your client!

Let me know if you face any issues with the process or in case I missed out important steps. Also let me know in case you published an app based on this blog, I would love to see it!

Good luck out there buddy.

Handling the Notch for iPhone XS, iPhone XS Max, iPhone XR in Adobe Air

The iPhone XS, iPhone XS Max and iPhone XR have arrived and all come with a Notch that we already know from the iPhone X. As a developer it is your responsibility to adjust your Air app accordingly to make sure you have a safe area at the top and bottom of the device. Because if you don’t, the Notch will cover your UI at those areas.

Our app on iPhone XS before we released a quick fix

Request the full device resolution in your app

First, make sure that your app will be able to render at the native resolutions of the new iPhones. If you don’t request the native rendering resolutions of the device, the new iPhones will fall back to use the resolution of older devices that you support and add black bars on the top and bottom of your app.  Technically that makes no problems, but the black bars prevent your users from getting the most of your app. Also, Apple will not allow you to release updates of your apps until you use the complete screen for your app and support the Notch correctly at full resolution.

To request the native resolution of the latest iPhone generation, you will have to include the correct Splash Screens in Air. You can find the complete list of supported splash screens in Adobe Air here. A complete discussion of splash screens is outside of the scope of this article, but in short, the Splash Screens for iPhone X and iPhone XS should have these names:

Portrait: Default-812h@3x~iphone.png
Landscape: Default-Landscape-812h@3x~iphone.png

At the time of this writing, no splash screens for iPhone XS Max and iPhone XR are supported by Adobe Air, meaning we will have to wait until Adobe releases an update.

If you have already released an update of your app recently then chances are that you already support the iPhoneX resolution with the correct splash screen. That means without any further change, your app will also run on the iPhone XS in native resolution right now, because iPhone X and iPhone XS share the same device resolution.

In our case that was a problem because in our code we activated the Notch logic based on the deviceModel. But we did not handle the Notch for the iPhone XS correctly, because when the code was written, the iPhone XS did not yet exist. Since the app already requested the native resolution of the iPhone XS but did not activate our Notch code adjustment, the UI was covered by the Notch and breaking the user experience.

Model names of the new iPhones

To correctly adjust our code to support Notch rendering logic, we are checking for the deviceModels and activate the code accordingly.

These are the device model identifiers of iPhones that come with a Notch:

iPhone X iPhone10,3
iPhone X GSM iPhone10,6
iPhone XS iPhone11,2
iPhone XS iPhone11,2
iPhone XS Max iPhone11,4
iPhone XS Max China iPhone11,6
iPhone XR iPhone11,8

Check for these device models and update your rendering accordingly. The deviceModel information is not available in Air by default, but you can use a native extension to retrieve it, for example this one.

Code Example

Here is a code example of how to set the correct paddings in your application for a Notch device. That means no buttons or other important content should be inside the safe area.

private function updateViewportPadding(width:Float, height:Float, deviceModel:String):Void 
{
	var notches:Array<String> = ["iPhone10,3", "iPhone10,6", "iPhone11,2", "iPhone11,4", "iPhone11,6", "iPhone11,8"];
		
	//If iPhone with notch: add padding
	if(notches.indexOf(deviceModel) > -1)
	{
		var NOTCH_SIZE :Float = 88;
		var BOTTOM_SIZE:Float = 68;


		//Landscape - left: 88, top: 0, right: 88, bottom: 68
		if (width > height)	viewport.setPadding(NOTCH_SIZE, 0, NOTCH_SIZE, BOTTOM_SIZE);
		//Portrait - left: 0, top: 88, right: 0, bottom: 68
		else   			viewport.setPadding(0, NOTCH_SIZE, 0, BOTTOM_SIZE);
	}
}

Please note that the above is Haxe code, but it should be easy to adjust it to Action Script 3. The viewport in our case is a class that holds information that tells the app where to render the content on the stage. You will have to add a similar logic to your code by yourself.

On the current generation for all iPhones with a Notch, the top safe area is 88 pixels and the bottom safe area (where the iOS swipe bar is located) is 68 pixels. In our app, we use a top padding of 88px and a bottom padding of 68px in portrait mode, and a left padding of 88px, a right padding of 88px and a bottom padding of 68px in landscape mode.

Now you need to call this function on startup and whenever your device rotates. That way you calculate the proper padding for the current orientation. Obviously the behavior is different, depending whether the device is running in portrait or landscape since the Notch rotates together with the device.

Of course the tricky part is how your application will keep the content inside of the padding. But this is your job as a developer and there is no one fits all solution. You will have to make sure that every screen and every scene of your app will respect this padding and not render below the Notch or the safe area at the bottom of the device.

Ready for the Future

If you happen to have a server that your application connects to, I would recommend that you send the deviceModel on startup to your server and send the Notch data back to your client. That way, your app will be future proof and you can add new devices on the server to handle the Notch without updating your clients whenever new devices with a Notch are released.

 

Happy coding!

 

Publishing an Adobe Air app to the Windows Store

Part I: From Air to Windows Store

We have recently released our first game for Windows 10 on the Windows store, including in-app purchases. Since there are only few resources available how to convert an app to a Windows Universal Package (UWP), especially from an Air perspective, I want to share a step-by-step guide. This first part will cover the conversion of a generic Air app to the Windows store. The second part covers in-app purchases and is available here.

If you want to have a look first, here is a link to our game on the store:
Direct Link (only works on Windows 10)
Windows Store website

Prerequisites

To get your Air app live in the store, we will use the Microsoft Desktop App Converter that Microsoft has released to allow an easy conversion from Win32 applications to Windows Store appx packages. While it is a very convenient tool, a lot of steps are involved. Here is a quick overview of the requirements. Don’t worry, we will go through each of them in a moment:

Now take a deep breath, it’s time to get your hands dirty.

Preparing your app

When you think about releasing your app or game for the Windows store, you should already have a desktop version of your project ready. It is worth noting that you will have to publish the app as an .exe file, an Air package will not work. So make sure that your adt command uses the target bundle. This is what our adt call looks like:

adt -package -storetype pkcs12 -keystore “cert\desktop.p12” -storepass [PASSWORD] -target bundle air\install.air application.xml -C bin . -extdir “..\_extensions”

Note that the adt call references the Air configuration file “application.xml” that you should already have. Furthermore, we include -extdir “..\_extensions”. The extensions directory is only required for Adobe Native Extensions (ANEs) that you might not need at this point, so you could remove that part. However I will keep this for reference since we will need it in the future when adding Store payments.

With this call, adt should convert your app into a a standalone Windows project that should run when opening the .exe file.

Optional: Wrapping your app into an installer

If you already have an Air app for Windows that you offer to your customers, chances are that you also have an installer file that will setup your app or game on your clients’ computer. You can use this installer with the Desktop App Converter. Alternatively, we can directly target the Air output without using an installer file. More on this later.

We have chosen to work with Inno Setup but there are a number of free alternatives available. I have not tried other options, so the decision is really up to you.

The installer should include all files and subdirectories that are inside the “air/install.air” directory. Reserve some time for this step and test your installer carefully before you continue, ideally on systems that have never seen your project before. This will make it easier to spot errors before you scratch your head over why the Windows Store build does not work correctly. Make sure that all dependencies are correctly installed and also removed when uninstalled. Creating installers is not a no-brainer, so don’t expect this to be done in 5 minutes.

Prepare the Store entry

Now is a good time to create your app entry in the Windows Dev Center. This can also be done at a later time, but it will give you access to some info that is referenced in the conversion process if you want to release the build on the store. If you skip this step now, you can use debug info later in the process until you are ready.

When you are signed in, click Dashboard on top to access the Partner Center. If I remember correctly, this step involves some verification process by Microsoft, but it is so long ago that I don’t remember all the details. Feel free to comment on this so I can include it here.

Once you are ready and a verified partner, you should be able to create a new app. With the app entry created, find the “App management” tab and click “App identity”. This list includes some values that we can use for the conversion process.

The Conversion – Prepraration

The next step is to actually convert your app into a Universal Windows Platform (UWP) .appx package. Please note that despite the name, it will only work on Windows 10 for PC.

Before we can start, we need to install and setup the converter. Download the Desktop App Converter through the Microsoft store from here.

Next, download the correct image file from here that is needed to initialize the converter before you can use it. The image file version number should match your Windows build number that you find via Windows Settings -> System -> About -> OS build.

Now open the Windows search bar and find the “Windows App Converter”. Right click the entry and start the converter as an administrator. A console will open, enter the following commands:

1) Set-ExecutionPolicy bypass

2) DesktopAppConverter.exe -Setup -BaseImage .\BaseImage-1XXXX.wim -Verbose

This might involve a system restart. If you require additional help, the official Microsoft documentation that this part of the blog is based on can be found here.

Your converter installation should now be ready for use.

The Conversion – Execution

Conversion time, finally! Before we start, I would like to give some folder structure recommendations as follows.

Create a new folder on your hard drive where you plan to keep your conversion files in. In this folder, create a readme.txt file where you can paste all the handy commands that we will use, since you will need them every time you create a new version. Also in that directory, create a folder called “builds” that will contain the converted app or apps. Using this subfolder will allow you to exclude it from git to save bandwidth.

Depending on whether you have an installer for your Air application or not, one of the two option applies. In both cases, make sure you are running the Desktop App Converter as an admin, then paste the commands into the console window.

1) Convert without installer

DesktopAppConverter.exe -Installer "[PATH_TO_FOLDER]" -AppExecutable [FILENAME.EXE] -Destination [TARGET_PATH\builds] -PackageName "[Package/Identity/Name from App Identity (see above)]" -PackageDisplayName "[YOUR_APP_NAME]" -AppId "[APP_ID]" -PackageArch "x86" -Publisher "CN=[Package/Identity/Publisher from App Identity (see above)]" -PackagePublisherDisplayName "[Package/Properties/PublisherDisplayName from App Identity (see above)]" -AppDisplayName "[YOUR_APP_NAME]" -Version 1.0.0.0 -MakeAppx -Sign -Verbose

2) Convert with installer

DesktopAppConverter.exe -Installer "[PATH_TO_INSTALLER_FILE]" -InstallerArguments "[ARGUMENTS]" -Destination [TARGET_PATH\builds] -PackageName "[Package/Identity/Name from App Identity (see above)]" -PackageDisplayName "[YOUR_APP_NAME]" -AppId "[APP_ID]" -AppDisplayName [YOUR_APP_NAME]" -PackageArch "x86" -PackagePublisherDisplayName "[Package/Properties/PublisherDisplayName from App Identity (see above)]" -Publisher "CN=[Package/Identity/Publisher from App Identity (see above)]" -Version 1.0.0.0 -MakeAppx -Sign -Verbose

For the three values that reference the App Identity from the Windows Dev Center Dasboard of your app, you can use some default values until you plan to ship the product. For example -PackageName "My Great Game" -PackagePublisherDisplayName "Best Company".

-PackageArch "x86"  is required in our case, but might differ if you build an Air app with 64 bits support.

The directive -InstallerArguments "[ARGUMENTS]" depends on whether your installer needs arguments to run in silent mode. In our case we use -InstallerArguments "/SILENT /LOG=[SOME_LOG_PATH].txt"but maybe you can skip it altogether.

The first time that you run this command the converter will make sure that all required settings are activated. There is a virtualization feature that needs to be turned on and there is a chance that your computer does not even support it. At least in our case one of our machines could not run the converter and we had to use a different system.

Anyhow, if that problem is solved and your system has been restarted potentially a couple of times, you should now find an .appx file in your [TARGET_PATH\builds\PACKAGE_NAME] directory.

Testing the appx package

The converter automatically created a subfolder in the builds directory with the Package Name of your app. In this folder you find the .appx file along with a certificate and other resources. Unfortunately you will have to install the certificate every single time you want to test a new version of the .appx.

Double click the auto-generated.cer file. Then click “Install certificate”. Next choose “Local Machine” and confirm administrator access. Next choose “Place all certificates in the following store” and click “Browse”. Here choose “Trusted Root Certification Authorities” and click “OK” and then “Next”, then “Finish”. It’s no joke, you have to do this every single time you create a new version and want to test it. If you don’t, Windows will not allow you to install your .appx package.

Now you can double click the .appx file and Install your app on the system. In subsequent conversions you need to uninstall the existing version of the .appx first. Do this by searching your app in the Windows 10 search bar, then right click and Uninstall it. Only then the new version can be installed, otherwise it will just offer you the option to start the already installed version.

Optional: Faster iterations with your own certificate

If you happen to own a code signing certificate already, or you feel installing the certificate 50 times a day is worth buying one, you can skip the manual installation of the auto generated certificate by using a proper certificate from a verified authority. Please note that this only works during development and that you can not ship the .appx to Microsoft with a custom code signing certificate.

To do this, remove the -Sign directive from the conversion command and instead sign the .appx file yourself with signtool.exe which is part of the Windows 10 SDK and can be found under Windows Kits\10\bin\[version]\x64:

signtool.exe sign -f [PATH_TO_CERT.p12] -p [PASSWORD] -fd SHA256 -v [PATH_TO_APP].appx

Note that signtool will make sure that .appx publisher field and certificate publisher match. A day long story short, alter your -Publisher "CN=[Package/Identity/Publisher]directive from the converter command to match the info that you find out by using this OpenSSL command:

openssl pkcs12 -nokeys -in [PATH_TO_CERT.p12] -passin pass:[PASSWORD]

If this doesn’t work at all you can find signing debug error logging like so: In the Windows Search bar, run “Eventvwr.msc”. Open the event log: Event Viewer (Local) > Applications and Services Logs > Microsoft > Windows > AppxPackagingOM > Microsoft-Windows-AppxPackaging/Operational

Magic!

Polishing Icons

The .appx that you have now should technically be ready to be released on the Store. However, the icon assets that the converter produces look really bad. Don’t you worry, I got you covered.

If you haven’t, yet, it is time to install Visual Studio from here.

Now a couple of head scratches later, you should find the Visual Studio entry in the Windows 10 Start menu. Don’t open the executable, instead open the Visual Studio subfolder. Inside there should be the “Developer Command Prompt”. Run it to find another console window.

We will now unpack the .appx package and fiddle with its contents. To create the extracted version of the package, run this command in the Visual Studio console:

makeappx unpack /v /p [PATH_TO_UWP].appx /d [TARGET_PATH\builds\APPNAME-Unpacked]

If that was successful, you will find a new folder in the builds directory that includes all files that were contained in the .appx. Go inside and into the Assets folder. It contains a number of images that all look very blurry. Go back and copy the Assets folder as a whole. Then navigate to the root of your conversion folder and create a new folder called “_assets-[APPNAME]” next to the “builds” folder and paste the Assets folder inside. We will later store more files than just the Assets subfolder inside it, so don’t just copy the Assets folder next to builds.

It is now time to replace the contents of all the .png files in the “_assets-[APPNAME]\Assets” folder with your high quality custom icons. Of course make sure that you do not accidentally change the dimensions of any of the images. This process will take a while and that is why we store a copy of the polished icons outside of the builds folder, so we can copy it quickly next time you release an update.

In the past, Microsoft did not allow icons over a certain file size and this size limit was easily triggered. So in case that happens to you, Adobe Photoshop has a nice feature to save PNG files with 8 bits by preserving highest quality of content. But I haven’t seen the error with recent updates of the converter even without compression.

When you are done, copy the “_assets-[APPNAME]\Assets” folder into the unpacked .appx folder and override the blurry icons.

Now you can recreate the .appx package with the updated assets by executing this command in the Visual Studio prompt:

makeappx pack /p [PATH_TO_UWP.appx] /l /d [TARGET_PATH\builds\APPNAME-Unpacked]

Confirm to overwrite the existing .appx file and find the updated package in the builds folder.

Optional: Creating a multi language package

Unfortunately, the Desktop App Converter creates English only packages by default. Here is a small guide to convert your app into a multi-language application.

Navigate to the _assets-[APPNAME] folder and create a new folder called “Strings” next to the existing “Assets” folder that you put there in the previous step. In the Strings folder, create new folders for each language you want to create by using the ISO-Standard in lower case. In our case that means “en-us” and “de-de”.

Inside each of the language folders now create a new file called “resources.resw”. Despite the ending, this will be an XML file with the following content, localized for the specific language:

<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="ApplicationDescription">
<value>Your description</value>
</data>
<data name="ApplicationDisplayName">
<value>Your App Name</value>
</data>
<data name="PackageDisplayName">
<value>Your App Name</value>
</data>
<data name="PublisherDisplayName">
<value>Company Name</value>
</data>
<data name="TileShortName">
<value>Your App Name on the Tile</value>
</data>
</root>

Again, we are storing the Strings folder for reference here. Now copy it into the unpacked folder of your .appx where you also copied the Assets folder.

Next we need to reference the translations in the project. In the unpacked project folder, find the manifest file called “AppxManifest.xml” and open it. Here we need to replace the hard coded strings with variables that reference above language files.

<DisplayName>ms-resource:Resources/PackageDisplayName</DisplayName>

<PublisherDisplayName>ms-resource:Resources/PublisherDisplayName</PublisherDisplayName>

<uap:VisualElements DisplayName="ms-resource:Resources/ApplicationDisplayName" Description="ms-resource:Resources/ApplicationDescription" [... keep the rest of the arguments as they are]>

<uap:DefaultTile ShortName="ms-resource:Resources/TileShortName" [... keep the rest of the arguments as they are]>

In the <Resources>node, add an entry for each language you support just above the scaling entries like so:

<Resource Language="en-us" />
<Resource Language="de-de" />
<Resource uap:Scale=...

Save your changes and copy the file into your assets directory, next to Assets and Strings for future reference. Notice that when you copy the manifest file over the converted one, this will override the version number, so you have to change it manually each time, because the Store will not allow you to upload the same version number twice.

One final step is required to make multi language work, because the new String resources are not yet registered in the project. We now need to create a new .pri file that organizes the project resources based on the resources of the unpacked app.

Open the Visual Studio Developer Command Prompt again and cd into the unpacked .appx directory. Then execute these commands from there:

Notice how all supported languages are separated with an underscore:
1) makepri createconfig /cf ..\[APPNAME].xml /dq en-US_de-DE /pv 10.0 /o

2) makepri new /pr . /cf ..\[APPNAME].xml /of ..\resources.pri /mf AppX /o

Verify results:
makepri dump /if ..\resources.pri /of ..\resources /o

Navigate one level up into the builds folder and you should find a number of files, including the desired resources.pri file. Delete the other files that are no longer needed and copy the resources.pri into the unpacked .appx folder as well as into the assets folder next to the AppManifest.xml for future reference.

Now you can create the .appx package again, including the multi language changes, like before:

makeappx pack /p [PATH_TO_UWP.appx] /l /d [TARGET_PATH\builds\APPNAME-Unpacked]

Shipping to the Windows Store

The .appx you have at your hands now should be good for release. Navigate to the Windows Dev Center Dashboard and select the app you have created earlier. Next, start a new submission, if you haven’t already. A submission is basically a version of your app, including it’s presence on the store.

In this submission, navigate to packages and drag and drop your .appx file into the upload field. Uploading the .appx will check for various errors, for example Publisher name, Package name, asset file sizes and so on. So expect some problems and dedicate some time to fix them the first time you do this.

Before you can release the app to production, Microsoft will ask you to send them a small report about your app. This will include results from the “Windows app cert kit” that is included in the Windows 10 SDK and answering some questions about your product. This step will take up a couple of days, so plan ahead.

Once all of that is done you should be able to finalize your Windows Store presence, including description, screenshots and so on. Hit the “Submit to the Store” when everything is ready. From here it should only take a couple of days until your app is live and available for users on the store.

Updating

Whenever you want to release a new version of your app, you can skip some of the previous steps that only need to be taken once. Here is a short little overview of your update loop:

  • Update your Air build and installer (if any)
  • Convert Air build / installer with the Desktop App Converter to .appx
  • Unpack .appx with Visual Studio Command Prompt
  • Replace contents in unpacked folder with your files (Assets , and if multi-language: Strings, AppxManifest.xml and resources.pri)
  • If multi-language: Edit AppxManifest.xml to set new version number
  • Package .appx with Visual Studio Command Prompt from the unpacked folder
  • Upload .appx to Windows Dev Center and submit to Store

It is worth noting that Microsoft will not allow remote updates from within the .appx application by downloading swf files from a server, even though it is technically possible to do that. Our app was rejected for this reason when it showed an update mechanic. That means the only way to provide a new version to your users is by submitting a new .appx through the store.

Conclusion

I hope this guide is useful for anyone interested in deploying Air for Windows Store. If you think this was complex, wait until we have a look at in-app purchasing in Part II.

In any case let me know if you face any problems during the process, if something is unclear or if I missed to cover important aspects. If you worked through the guide and have released something, please post a link here so I and other Air devs can check it out, I am always curious to see what other people work on.

Happy coding!

Releasing a preview of our HTML5 clients

Earlier this year, we have finished porting our Action Script 3 codebase to Haxe. The codebase has been growing over the last couple of years (it started 2009, to be exact) and so we were very cautious if Haxe could deliver acceptable results, without nasty bugs that are hidden in the basement of that codebase forever unnoticed.

The process

For the port we used AS3HX, a popular conversion tool, with a minor set of custom additions and fixes since we experienced some problems in our code with the default version. The whole process took a couple of months until all AS3 was replaced with Haxe and we were able to export our games from pure Haxe code. The first iteration of that port went live to all ours users at the end of February 2018 for web (Flash) and our mobile and desktop clients (with Air). Except for a couple of minor fixes that we had to add in the following weeks, everything went well and the team started to work on the HTML5 target at the end of March. From our initial tests it was already clear that even though we ported all codes to Haxe + OpenFL, a lot of refactoring would be needed to make HTML5 become a reality. In that sense, porting to Haxe while targeting Flash was easier, because the language changed, but the environment (Flash Player) was the same. This time, we had to replace the environment, and even worse, a couple of features hat we made heavy use of were not yet supported in OpenFL HTML5.

The Open Source nature of Haxe + OpenFL helped us a lot in the process, and we made a couple of fixes for problems or missing features we encountered. We also created pull requests to get our additions into the official builds, for example for MovieClip.buttonMode or blend modes during MovieClip animations. It took roughly six months until we were able to release the preview of the HTML5 builds to our players and we are currently evaluating feedback from our community to identify potential bugs and problems before we switch to HTML5 by default in the next months.

The result

Here is a side by side comparison of the Flash and HTML5 versions at their current state:

Side by Side comparison of our lobby

Most prominently, we have replaced the font since the original font was looking blurry, especially when rendered in smaller sizes. We are also baking profile pictures on our server now, instead of masking them directly on the client, which means from now on all users will have squared avatars. (Previously we were supporting any dimensions of pictures). Some more  details have changed, for example the alpha on “running games” has been removed for improved performance.

Filters are still tricky. The glow on the slots icon is baked into the bitmap in Animate CC.

Other things that we plan to work on in future releases are filter effects (glows and drop shadows), removing blur on some of the textfields (this is likely a bug related to blendmode we intend to fix in OpenFL) and adding back our 3D ingame view. In the above example you can see that the filters on the dynamic text are still missing, since those are heavy on performance and sometimes produce undesired results.

Ingame view looks almost the same, but Bitmap resizing is worse in HTML.

The ingame view looks practically identical, but we found that bitmap resizing in HTML5 produces worse results than in Flash the further you get away from the original resolution. That means careful selection of the asset sizes for your use case. Some people describe better results with multi-step resizing if you need to display a large range of different sizes.

Performance comparison

Performance wise the HTML5 version is slightly slower than the Flash version, on low end devices, which is most notable when browsing through the menu. Especially instantiating assets from swflite seems to be much heavier compared to Flash, which results in small lags when opening popups on low end devices. The team is still working on that, though, and we hope to achieve Flash-like performance over the next sprints.

At the moment we are exporting to Canvas because we noticed that it offers the best average performance. In our experience WebGL will perform faster on devices with a dedicated GPU but worse on devices without, so for low end devices, Canvas seems to be the better option. We plan to add a switch between both modes to offer the best experience based on user choice or automated discovery. Since Away3D is not supported in Canvas, a WebGL choice is also a requirement to enable our 3D game view again.

Of course we also had some learnings over the course of the last months, for example we made heavy use of strict Haxe casts which turned out to be a performance bottleneck. It will probably take a couple more months until we have all of those potential improvements identified and fixed.

However those performance decreases are most notable on low end machines and performance should feel the same for the average user.

And there are good things, too: HTML5 supports native refresh rates, which Flash doesn’t. So on my home screen I have a smooth 144 FPS experience, something that you would never have seen in Flash.

144 FPS is possible in HTML5!

The games also run on mobile browsers out of the box now with good performance on modern devices. We consider to detect this and show our mobile views in this case, to offer a true mobile optimized browser performance. This is where we can benefit from having one single crossplatform codebase, since all the mobile view code is available in HTML5 already and just waiting to be switched on.

Conclusion

If you are interested in the current state of our HTML5 client, you can give it a go here: https://apps.facebook.com/rummy-palace?html5=1 You can use the bottom left button to switch between the Flash and HTML5 version.

Overall Haxe + OpenFL was the right choice and the only real option to keep our large codebase instead of starting over with a new technology. We can now export from one codebase to HTML5 and swf. We will continue to support Flash Player with the swf export as a fallback for the forseeable future, as well as packaging it with Air in our container projects for mobile and desktop clients. A couple of workflows had to be adjusted, but now we can still use our .fla assets like before and deploy a single codebase to all platforms. Way to go!