once I made a Shortcut to show the in-app purchases of some apps. As soon as I’ve found Scriptable, I started to transfer the Shortcut to it because it was fetching the information a lot faster than Shortcuts. I’ve also extended it to show the last price, if it has changed.
It is pretty easy to use it:
Share an app from the AppStore or with 3D Touch on the icon of the app on the homescreen with this script and it will be added to your personal list of apps that is stored in iCloud
Run the script to show the prices of each app
To remove an app from the list, run the script, check the checkbox at the top of the result view, click “Done” and a list of your apps with it’s icon, name and current price will be shown. Tap on an item to select it and press “Done” at the top left to remove the selected apps
It can also run in notifications, where it only displays the changes in prices! Just schedule some notifications in the script settings and you’re good to go!
This script can only handle apps from AppStore for now!
To download it, either copy it’s contents into the old script replacing everything, or use my Import Script for Github Gists script
If there is any bug, please report it and I like reading comments that you are using it or you learned something from it
Edit
I’ve changed the script to use your local AppStore and not the one from Austria.
Another edit
I’ve updated the script to fix an error if an app was removed from the AppStore. Price changes can now also be reset.
Yet another edit
I’ve fixed requiring a not needed module ~ chooseItems.js
So. Stupid question probably, but how do I import this?
I can’t find any import-function in scriptable. I could probably just extract the script-part from the JSON, but I feel like I’m missing something obvious.
For what it’s worth, it looks like it was the original JS file, but with lots of escape sequences added in \n for new line, \t for tab, etc. I had a quick go at some find and replace and then some more manual re-crafting to see if I could get it running. I succeeded to an extent, but I wonder if some of the regex got mangled in the process, or if I’ve missed something (e.g. in the HTML) somewhere? I can run the reworked script (below), but whenever it attempts to create a new file it just sits there waiting. Right now, I can’t be sure if that’s a problem with the original script or my iterated on version to try and get it to run. Unfortunately I don’t have the time left today to sit down and figure it out. This was just a quick attempt at a quck fix as it was.
Expand to reveal script
/********************************************
* *
* \/\\ *
* \/ \\ _ __ _ __ *
* \/ \/\\ \\ | '_ \\| '_ \\ *
* \/ ____ \\| |_) | |_) | *
* \/_\/ \\_\\ .__\/| .__\/ *
* | | | | *
* __ __ _|_| |_| *
* \\ \\ \/ \/ | | | | *
* \\ \\ \/\\ \/ \/_ _| |_ ___| |__ ___ _ __ *
* \\ \\\/ \\\/ \/ _` | __\/ __| '_ \\ \/ _ \\ '__|*
* \\ \/\\ \/ (_| | || (__| | | | __\/ | *
* \\\/ \\\/ \\__,_|\\__\\___|_| |_|\\___|_| *
* *
\* Track the price of apps and their *
* in-app purchases *
* *
* - To view the list of apps just run *
* this script. *
* - To add an app, just share the app *
* from the AppStore to Scriptable and *
* choose this script. *
* - To remove an app from the list, *
* check in the result view the checkbox *
* at the top, press "Done" and it will *
* ask you to select the apps to remove. *
* *
* Made by @schl3ck (Reddit, Automators *
* Talk) in November 2018 *
* *
********************************************/
let fm = FileManager.iCloud();
let settingsFilename = "AppWatcher.json";
let file = fm.joinPath(fm.documentsDirectory(), settingsFilename);
let apps = fm.readString(file);
if (!apps) {
let alert = new Alert();
alert.title = `The file "${settingsFilename}" was not found in the Scriptable iCloud folder or is not downloaded from iCloud. If you know that it exists, you can select it, otherwise you have to create a new one`;
alert.addAction("Select from iCloud");
alert.addAction("Create new file");
alert.addCancelAction("Cancel");
let i = await alert.presentSheet();
switch (i) {
case -1:
Script.complete();
return;
case 0:
file = (await DocumentPicker.open(["public.json"]))[0];
apps = fm.readString(file);
break;
case 1:
apps = "[]";
}
}
apps = JSON.parse(apps);
apps.forEach(app => {
setUndef(app);
if (!app.inApp) return;
app.inApp.forEach(setUndef);
});
function setUndef(i) {
if (!i.price) return;
if (i.price[0] == null) {
i.price[0] = undefined;
i.formattedPrice[0] = undefined;
}
}
function getInAppPurchases(html) {
let regex = /<script type=\"fastboot\\\shoebox\" id=\"shoebox-ember-data-store\">(\{\"data\":.*?\})<\/script>/s;
let json = JSON.parse(html.match(regex)[1]);
let result = [];
for (let i of json.included) {
if (!i.type.includes("in-app"))
continue;
let price = parseFloat(i.attributes.price.replace(",", ".")) || 0;
let id = i.id;
let name = i.attributes.name;
let formattedPrice = i.attributes.price;
result.push({name, price, formattedPrice});
}
result.sort((a, b) => a.price - b.price);
return result.filter((a, i) => i === 0 || a.name !== result[i-1].name);
}
function getColor([a, b]) {
// log(`getColor(${a}, ${b})`)
if (typeof a === "undefined")
return "";
if (a < b) return "table-danger";
if (b === 0) return "table-success";
if (a > b) return "table-warning";
return "";
}
function save() {
if (fm.fileExists(file))
fm.remove(file);
fm.writeString(file, JSON.stringify(apps, null, 0));
log("Saved!");
}
let addApp = args.urls[0];
if (addApp) {
let id = parseInt(addApp.match(/id(\d+)/)[1]);
let alert = new Alert();
if (apps.find(a => a.id === id)) {
alert.title = "This app is already in the list";
alert.addCancelAction("OK");
await alert.presentAlert();
Script.complete();
return;
}
apps.push({id});
alert.title = "Show all apps?";
alert.addAction("Yes");
alert.addCancelAction("No");
if (-1 === await alert.presentAlert()) {
save();
Script.complete();
return;
}
}
if (!apps.length) {
let a = new Alert();
a.title = "No apps";
a.message = "There are no apps in your list. Please add an app by sharing its AppStore URL to this script.";
a.addCancelAction("OK");
await a.presentAlert();
Script.complete();
return;
}
let req = new Request("https:/bitunes.apple.com/lookup?country=de&id=" + apps.map(app => app.id).join(","));
// req = new Request(url);
let json = (await req.loadJSON()).results;
// QuickLook.present(json);
// return;
let inApp = await Promise.all(json.map(i => {
let req = new Request(i.trackViewUrl);
return req.loadString().then(html => getInAppPurchases(html));
}));
apps = json.map((app, i) => {
return {
inApp: inApp[i],
price: app.price,
formattedPrice: app.formattedPrice,
name: app.trackName,
icon: app.artworkUrl60
};
}).map((app, i) => {
apps[i].name = app.name;
apps[i].icon = app.icon;
apps[i].price = apps[i].price || [undefined, undefined];
apps[i].formattedPrice = apps[i].formattedPrice || [undefined, undefined];
if (apps[i].price[1] !== app.price) {
apps[i].price.shift();
apps[i].price.push(app.price);
apps[i].formattedPrice.shift();
apps[i].formattedPrice.push(app.formattedPrice);
}
apps[i].inApp = apps[i].inApp || [];
apps[i].inApp = app.inApp.map((ia) => {
let old = apps[i].inApp.find(a => a.name === ia.name && a.price === ia.price[1]);
old = old || apps[i].inApp.find(a => a.name === ia.name);
if (old && old.price[1] !== ia.price) {
old.price.shift();
old.price.push(ia.price);
old.formattedPrice.shift();
old.formattedPrice.push(ia.formattedPrice);
} else if (!old) {
old = {
price: [undefined, ia.price],
formattedPrice: [undefined, ia.formattedPrice],
name: ia.name,
id: ia.id
};
}
return old;
});
apps[i].inApp.sort((a, b) => a.price[1] - b.price[1]);
return apps[i];
});
apps.sort((a, b) => a.price[1] - b.price[1]);
// await QuickLook.present(JSON.stringify(apps, null, 4));
let html = `<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, width=device-width">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<style>
.table > tbody > tr:nth-child(odd) > td:last-child,
.table .table td:last-child {
text-align: right;
white-space: nowrap;
}
.table > tbody td {
vertical-align: middle;
}
.table .table {
width: 100%
margin: 0 5px;
padding: 0;
}
.btn {
width: 100%;
border-radius: 0;
font-size: 15pt;
margin: 0;
}
.btn span {
position: relative;
top: -4px;
}
.btn:before {
content: "\\2610";
position: relative;
left: -50px;
top: -4px;
font: 25pt "Menlo-Regular";
}
input:checked + .btn:before {
content: "\\2611";
}
</style>
</head>
<body>
<input type="checkbox" id="removeApps" hidden>
<label class="btn btn-sm btn-warning" for="removeApps"><span>Remove apps</span></label>
<table class="table table-striped">
<thead>
<tr>
<th>Icon</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
${apps.map(app => {
return `<tr class="${getColor(app.price)}">
<td><img src="${app.icon}" width="60" height="60"></td>
<td>${app.name}</td>
<td>${typeof app.price[0] !== "undefined" ? `<del>${app.formattedPrice[0]}</del> ` : ""}${app.formattedPrice[1]}</td>
</tr>
<tr><td colspan="3">
<table class="table table-sm">
${app.inApp.map(ia => {
return `<tr class="${getColor(ia.price)}">
<td>${ia.name}</td>
<td>${typeof ia.price[0] !== "undefined" ? `<del>${ia.formattedPrice[0]}</del> ` : ""}${ia.formattedPrice[1]}</td>
</tr>`;
}).join("")}
</table>
</td></tr>`;
}).join("")}
</tbody>
</table>
</body>
</html>`;
// await QuickLook.present(html);
let wv = new WebView();
wv.loadHTML(html);
await wv.present();
let rm = await wv.evaluateJavaScript(`document.getElementById("removeApps").checked;`);
function createRows(apps, ui) {
ui.removeAllRows();
let row = new UITableRow();
row.dismissOnSelect = false;
row.addText("Please choose which apps you want to REMOVE from the list");
row.height = 60;
ui.addRow(row);
apps.forEach((app) => {
row = new UITableRow();
row.dismissOnSelect = false;
row.onSelect = (number) => {
app.checked = !app.checked;
createRows(apps, ui);
}
// checkmark
let cell = UITableCell.text(app.checked ? "\\u2714" : "");
cell.centerAligned();
cell.widthWeight = 8;
row.addCell(cell);
// icon
cell = UITableCell.imageAtURL(app.icon);
cell.widthWeight = 10;
cell.centerAligned();
row.addCell(cell);
// name
cell = UITableCell.text(app.name);
cell.widthWeight = 67;
cell.leftAligned();
row.addCell(cell);
// price
cell = UITableCell.text(app.formattedPrice[1]);
cell.widthWeight = 15;
cell.rightAligned();
row.addCell(cell);
ui.addRow(row);
});
ui.reload();
}
if (rm) {
let ui = new UITable();
createRows(apps, ui);
await ui.present();
apps = apps
.filter(app => !app.checked)
.map(app => {
delete app.checked;
return app;
});
}
save();
Script.complete();
But that’s just the value for the script-key, right? Looks to me like this is JSON that is exported from scriptable somehow, with the icon metadata and everything.
If you take a Scriptable script and choose to Save to Files from the Share Sheet, guess what the file format looks like
The problem is, for this file format, as far as I know, there is no built-in import method to allow you to get the file back into Scriptable.
If you actually look at one of the JS files directly outside of Scriptable, you’ll see that Scriptable only data at the top - just as the comments suggest. The Scriptable UI just filters them out.
I’m sorry guys, it is the exported .scriptable file, but iCloud only shows the contents and not the file… I’ve uploaded it to my OneDrive and updated the link at the top. Download the file and open it with Scriptable. I hope that it works now
And on a more general note the script also seems to work. Don’t really have much time to play around with it right now, but it looks like you did a great job!
You can just use open in, there’s a “Copy to Scriptable” action that works
I’ve updated the script with a fix for apps that were removed from the AppStore.
And the changes in price can now also be reset.
For the next update, I’m working on integrating it in notifications. It works, but now I want to extract that notification edit thing to make it as plugin because it looked quite nice
I’m sorry if you have gotten an error with the latest version. That was my fault. But please write the next time that you got an error and also copy it into your post
I’ve fixed the error “can’t find variable chooseItems”. Post was updated!
I just wanted to let you all know that the script can also run in notifications! Just schedule one in the script settings and it will only show you the changes in the notification!
I’ve updated the script to adapt it to the latest changes Apple has made to its AppStore websites (thereby breaking this script).
And if when Apple does it again, it should show a banner with a link to this topic.