09/15/2023
Guide for automatic publishing of Android appbundle/apk to Amazon
If you're actively using alternative stores for delivering your Android applications to customers, you might already have heard(or used) Amazon app store. If so, perhaps, you have been wondering how to speed up time to production for your application/game. One of such strategies can be an automated publishing of your application to store using Amazon App Submission Api. Our team have succeeded with this approach, and we would like to share our flow in this blog post.
Pre-requisites
- Amazon API Key
- NodeJS
Flow of uploading new application version to Amazon Appstore
- Create an Amazon Edit which will be the container of all your planned changes
- Upload the APK/Bundle to the container, obtained in the previous step
- (Optionally) Update the metadata related to your update, such as images and app description
- Write the release notes
- Commit edit for changes to take effect
Complete script
const https = require('https');
const querystring = require('querystring');
const fs = require('fs');
const hostname = 'api.amazon.com';
module.exports.amazonPublish = ({ amazonClientSecret, amazonClientId, appId }) => {
return this.getAccessToken({ amazonClientId, amazonClientSecret })
.then(this.createAmazonEdit({ appId }))
.then(this.uploadAmazonApk({ appId }))
.then(this.getCurrentListing({ appId }))
.then(this.modifyListing({ appId }))
.then(this.commitEdit({ appId }));
}
module.exports.getAccessToken = ({ amazonClientId, amazonClientSecret }) => {
return new Promise((resolve, reject) => {
const req = https.request({
hostname,
path: '/auth/o2/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
}, res => {
res.on('data', (data) => {
resolve(JSON.parse(data.toString()));
});
res.on('error', (err) => {
reject(err);
});
});
req.write(querystring.stringify({
"grant_type": "client_credentials",
"client_id": amazonClientId,
"client_secret": amazonClientSecret,
"scope": "appstore::apps:readwrite"
}));
req.end();
});
};
const apkPath = '/path/to/your/application.apk';
module.exports.uploadAmazonApk = ({ appId }) => ({ editId, access_token }) => {
const apkContents = fs.readFileSync(apkPath, { encoding: 'binary' });
return new Promise((resolve, reject) => {
const req = https.request({
hostname: 'developer.amazon.com',
method: 'POST',
path: `/api/appstore/v1/applications/${appId}/edits/${editId}/apks/upload`,
headers: {
'Authorization': `Bearer ${access_token}`,
'Content-Type': 'application/vnd.android.package-archive',
},
}, res => {
res.on('data', (data) => {
const resp = JSON.parse(data.toString());
if (resp.errors) {
console.log(resp);
return reject(resp.errors);
}
resolve({ ...resp, access_token, editId });
});
res.on('error', (err) => {
reject(err);
});
})
req.write(apkContents);
req.end();
});
}
module.exports.commitEdit = ({ appId }) => ({ editId, access_token, etag }) => {
return new Promise((resolve, reject) => {
const req = https.request({
hostname: 'developer.amazon.com',
method: 'POST',
path: `/api/appstore/v1/applications/${appId}/edits/${editId}/commit`,
headers: {
'Authorization': `Bearer ${access_token}`,
'If-Match': etag,
},
}, res => {
res.on('data', (data) => {
const resp = JSON.parse(data.toString());
if (resp.errors) {
console.log(resp);
return reject(resp.errors);
}
console.log('Successfully committed to Amazon');
resolve();
});
res.on('error', (err) => {
reject(err);
});
});
req.end();
});
}
module.exports.getCurrentListing = ({ appId }) => ({ editId, access_token }) => {
return new Promise((resolve, reject) => {
const language = 'en-US';
const req = https.request({
hostname: 'developer.amazon.com',
method: 'GET',
path: `/api/appstore/v1/applications/${appId}/edits/${editId}/listings/${language}`,
headers: {
'Authorization': `Bearer ${access_token}`,
},
}, res => {
console.log(res.headers);
res.on('data', (data) => {
const resp = JSON.parse(data.toString());
if (resp.errors) {
console.log(resp);
return reject(resp.errors);
}
resolve({ listing: resp, etag: res.headers['etag'], access_token, editId });
});
res.on('error', (err) => {
reject(err);
});
});
req.end();
});
}
module.exports.modifyListing = ({ appId }) => ({ editId, access_token, listing, etag }) => {
return new Promise((resolve, reject) => {
const language = 'en-US';
const req = https.request({
hostname: 'developer.amazon.com',
method: 'PUT',
path: `/api/appstore/v1/applications/${appId}/edits/${editId}/listings/${language}`,
headers: {
'Authorization': `Bearer ${access_token}`,
'If-Match': etag,
},
}, res => {
res.on('data', (data) => {
const resp = JSON.parse(data.toString());
if (resp.errors) {
console.log(resp);
return reject(resp.errors);
}
resolve({ ...resp, access_token, etag, editId });
});
res.on('error', (err) => {
reject(err);
});
});
console.log({ listing });
req.write(querystring.stringify({
...listing,
recentChanges: "Minor bugfixes",
}));
req.end();
});
}
module.exports.createAmazonEdit = ({ appId }) => ({ access_token }) => {
return new Promise((resolve, reject) => {
const req = https.request({
hostname: 'developer.amazon.com',
method: 'POST',
path: `/api/appstore/v1/applications/${appId}/edits`,
headers: {
'Authorization': `Bearer ${access_token}`,
},
}, res => {
res.on('data', (data) => {
const resp = JSON.parse(data.toString());
if (resp.errors) {
console.log(resp);
return reject(resp.errors);
}
resolve({ ...resp, access_token });
});
res.on('error', (err) => {
reject(err);
});
})
req.end();
});
}
Photo by Charlota Blunarova on Unsplash