Skip to content

Instantly share code, notes, and snippets.

@schl3ck
Last active November 23, 2025 20:03
Show Gist options
  • Select an option

  • Save schl3ck/b8e21e362f1646c8680e53073e7c95a9 to your computer and use it in GitHub Desktop.

Select an option

Save schl3ck/b8e21e362f1646c8680e53073e7c95a9 to your computer and use it in GitHub Desktop.
iOS Scriptable script to track the price and in-app purchases of your favourite apps
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: laptop-code;
// share-sheet-inputs: url;
/*******************************************
* *
* /\ *
* / \ _ __ _ __ *
* / /\ \ | '_ \| '_ \ *
* / ____ \| |_) | |_) | *
* /_/ \_\ .__/| .__/ *
* | | | | *
*__ __ _|_| |_| *
*\ \ / / | | | | *
* \ \ /\ / /_ _| |_ ___| |__ ___ _ __ *
* \ \/ \/ / _` | __/ __| '_ \ / _ \ '__|*
* \ /\ / (_| | || (__| | | | __/ | *
* \/ \/ \__,_|\__\___|_| |_|\___|_| *
* *
* 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 "Remove *
* app" at the top, press "Done" and it *
* will ask you to select the apps to *
* remove. *
* - To reset the changes in prices, check *
* in the result view the checkbox "Reset *
* price changes" at the top and press *
* "Done" *
* *
* This script can also run in *
* notifications. It then will display *
* only the changes. *
* *
* You can also start this script with the *
* URL scheme with a parameter "mode" to *
* directly execute its action. This is *
* useful for the actions inside *
* notifications. It can have the values *
* "view" to only view the apps without *
* polling for any changes, "reset" to *
* delete every recorded change and *
* "remove" to jump to the selection where *
* you can remove the apps *
* *
* ! ! ! ! ! ! ! ! ! *
* This script can only handle apps from *
* the AppStore for now *
* ! ! ! ! ! ! ! ! ! *
* *
* Below this comment is a small *
* configuration section. *
* *
* Scroll to the bottom for the changelog! *
* *
* Made by @schl3ck (Reddit, Automators *
* Talk) in November 2018 *
* *
* Version 1.1.2 *
* *
*******************************************/
// if true, updates will be tracked
const trackVersion = true;
// if true, in-app purchases will be tracked
const trackIAPs = true;
// if true, starts the in app results view with an overview of the found changes
const defaultToChangesView = true;
// automatically close the progress view and open the changes view when no error has occurred
const autoOpenChangesWhenFinishedLoading = true;
// ======= end of config =======
// set it to true initially as it gets set to false when at least one app could retrieve its in-app-purchases (even if there are none)
let errorFindingInAppDueToWebsiteChange =
trackIAPs &&
"errorFindingInApp" in args.queryParameters &&
args.queryParameters.errorFindingInApp === "true";
const errorFindingInAppTitle =
"Looks like Apple changed their AppStore web appeareance. Tap here to see if there is an update for this script.";
const errorFindingInAppSubtitle = "If there is no update, please try again after some time.";
const forumPostURLOfScript =
"https://talk.automators.fm/t/appwatcher-track-the-price-of-apps-and-their-in-app-purchases/3381";
let fm = FileManager.iCloud();
let settingsFilename = "AppWatcher.json";
let file = fm.joinPath(fm.documentsDirectory(), settingsFilename);
await fm.downloadFileFromiCloud(file);
let apps = fm.readString(file);
let noFile = false;
if (!apps) {
if (config.runsInWidget) {
Script.setWidget(
MessageWidget("No settings file was found. Please run the script in the app first."),
);
Script.complete();
return;
} else {
let alert = new Alert();
alert.title = `The file "${settingsFilename}" was not found in the Scriptable iCloud folder. You can select a file or 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 = "[]";
break;
}
noFile = true;
}
}
apps = JSON.parse(apps);
apps.forEach((app) => {
setUndef(app);
if (!app.inApp) return;
app.inApp.forEach(setUndef);
});
if (noFile) save();
function MessageWidget(message) {
let w = new ListWidget();
w.backgroundColor = Color.dynamic(Color.white(), Color.black());
w.addSpacer();
let text = w.addText(message);
text.centerAlignText();
text.textColor = Color.red();
text.minimumScaleFactor = 0.5;
w.addSpacer();
let stack = w.addStack();
stack.layoutHorizontally();
stack.addSpacer();
text = stack.addText("Last update:");
text.rightAlignText();
text.textColor = Color.gray();
text.font = Font.footnote();
text.lineLimit = 1;
text.minimumScaleFactor = 0.7;
stack.addSpacer(1);
let time = stack.addDate(new Date());
time.rightAlignText();
time.applyTimeStyle();
time.textColor = Color.gray();
time.font = Font.footnote();
time.minimumScaleFactor = 0.7;
return w;
}
class noSave {
constructor(data) {
this.data = data;
log(data);
}
toJSON() {
return undefined;
}
toString() {
return this.data.toString();
}
}
function setUndef(i) {
if (!i.price) return;
if (i.price[0] == null) {
i.price[0] = undefined;
i.formattedPrice[0] = undefined;
}
}
function getInAppPurchases(html, url, id) {
let regex = /<script type="application\/json" id="serialized-server-data">(.+?)(?=<\/script>)/s;
let match = html.match(regex);
if (!match) {
log("Did not find serialized-server-data, " + url);
// probably the website has changed
errorFindingInAppDueToWebsiteChange = errorFindingInAppDueToWebsiteChange && true;
return;
}
let json = JSON.parse(match[1]);
if (!json[0].data.titleOfferDisplayProperties.hasInAppPurchases) {
// the regex found something. It looks like the website hasn't changed yet
errorFindingInAppDueToWebsiteChange = false;
return [];
}
const regexLooksLikePrice = /\d+(?:[ ,.]\d+)*/;
const candidates = json[0].data.shelfMapping.information.items.filter((item) => {
if (
"items_V3" in item &&
Array.isArray(item.items_V3) &&
item.items_V3.length > 0 &&
item.items_V3[0].$kind === "textPair" &&
regexLooksLikePrice.test(item.items_V3[0].trailingText)
) {
return true;
}
if (
"items" in item &&
Array.isArray(item.items) &&
item.items.length > 0 &&
"textPairs" in item.items[0] &&
Array.isArray(item.items[0].textPairs) &&
item.items[0].textPairs.length > 0 &&
regexLooksLikePrice.test(item.items[0].textPairs[0][1])
) {
return true;
}
return false;
});
if (candidates.length === 0) {
// probably the website has changed
errorFindingInAppDueToWebsiteChange = errorFindingInAppDueToWebsiteChange && true;
return;
}
if (candidates.length > 1) {
console.log(
"found multiple candidates for in app purchases (choosing first): " +
JSON.stringify(candidates, null, 2),
);
}
const items = candidates[0].items;
const items_V3 = candidates[0].items_V3;
/**
* Parses the price from a string.
*
* It should be able to parse all these number formats:
* - 780
* - 6,000
* - 1,678.00
* - 1 000,00
* - 1.199.000
* @param {string} text
*/
function getPrice(text) {
if (!text) {
return 0;
}
const match = text.match(regexLooksLikePrice);
if (!match) {
throw new Error(`Found no price in text "${text}"`);
}
// we can safely remove any spaces as thousand separator
let number = match[0].replaceAll(" ", "");
// remove , and . as thousand separator
for (const separator of [",", "\\."]) {
if (new RegExp(`\\d+${separator}\\d{3}`).test(number)) {
number = number.replaceAll(separator[separator.length - 1], "");
}
}
// any remaining , are floating point separator
number = number.replaceAll(",", ".");
return parseFloat(number);
}
// log("items_V3: " + JSON.stringify(items_V3, null ,2))
// log("items: " + JSON.stringify(items, null, 2))
if (items_V3) {
const titleKey = "leadingText";
const priceKey = "trailingText";
// we found something. It looks like the website hasn't changed yet
errorFindingInAppDueToWebsiteChange = false;
return items_V3
.filter((i) => i.$kind === "textPair")
.map((i) => {
return {
id: i[titleKey],
name: i[titleKey],
price: getPrice(i[priceKey]),
formattedPrice: i[priceKey],
};
});
}
if (items) {
const titleIndex = 0;
const priceIndex = 1;
// we found something. It looks like the website hasn't changed yet
errorFindingInAppDueToWebsiteChange = false;
return items[0].textPairs.map((i) => {
return {
id: i[titleIndex],
name: i[titleIndex],
price: getPrice(i[priceIndex]),
formattedPrice: i[priceIndex],
};
});
}
log("Did not find in-app purchases information, " + url);
errorFindingInAppDueToWebsiteChange = errorFindingInAppDueToWebsiteChange && true;
return;
}
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() {
// log(JSON.stringify(apps, null, 4));
// return;
if (fm.fileExists(file)) fm.remove(file);
fm.writeString(
file,
JSON.stringify(
apps,
(k, v) => {
if (k === "removed") return undefined;
return v;
},
0,
),
);
log("Saved!");
}
let addApp = args.urls[0];
if (addApp) {
if (!/^https?:\/\/(?:itunes|apps)\.apple\.com\/(?:[^/]+\/)?app\//.test(addApp)) {
let a = new Alert();
a.title = "Not an AppStore app";
a.message = "I'm sorry, but this script only supports apps from the AppStore for now 😕";
a.addCancelAction("OK");
await a.presentAlert();
return;
}
let id = parseInt(addApp.match(/id(\d+)/)[1]);
let country = addApp.match(/https?:\/\/(?:itunes|apps)\.apple\.com\/([^/]+)/)[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, country, trackViewUrl: addApp });
save();
alert.title = "Show all apps?";
alert.addAction("Yes");
alert.addCancelAction("No");
if (-1 === (await alert.presentAlert())) {
Script.complete();
return;
}
}
if (!apps.length) {
const msg =
"There are no apps in your list. Please add an app by sharing its AppStore URL to this script.";
if (config.runsInWidget) {
Script.setWidget(MessageWidget(msg));
Script.complete();
} else {
let a = new Alert();
a.title = "No apps";
a.message = msg;
a.addCancelAction("OK");
await a.presentAlert();
Script.complete();
}
return;
}
let launchMode = args.queryParameters.mode;
log("launchMode: " + launchMode);
let wv;
if (config.runsInApp) {
// launchMode = "edit"
}
if (!launchMode) {
log("retrieving prices");
let ui = config.runsInApp ? new UITable() : null;
let loaded = {
total: apps.length,
successful: 0,
failed: 0,
errors: [],
/** @type {string | null} */
open: null,
};
function refreshTable() {
if (!ui) return;
ui.removeAllRows();
let row, cell;
row = new UITableRow();
ui.addRow(row);
row.isHeader = true;
row.addText("AppWatcher Loading").centerAligned();
row = new UITableRow();
ui.addRow(row);
row.height = 15;
row = new UITableRow();
ui.addRow(row);
row.addText("" + loaded.successful, "Successful").centerAligned();
row.addText("" + loaded.failed, "Failed").centerAligned();
row.addText("" + loaded.total, "Total").centerAligned();
if (loaded.open) {
row = new UITableRow();
ui.addRow(row);
row.backgroundColor = Color.green();
row.addText("Open").centerAligned();
row.onSelect = () => Safari.open(loaded.open);
}
for (const error of loaded.errors) {
row = new UITableRow();
ui.addRow(row);
row.addText("" + error);
row.dismissOnSelect = false;
row.onSelect = async () => {
const a = new Alert();
a.title = "Error";
a.message = "" + error;
a.addCancelAction("OK");
a.addAction("Copy");
if ((await a.presentAlert()) === 0) {
Pasteboard.copyString("" + error);
}
};
}
ui.reload();
}
let countries = {};
apps.forEach((a) => {
countries[a.country] ||= [];
countries[a.country].push(a.id);
});
log("getting app details");
let req = Promise.all(
Object.entries(countries).map(([c, ids]) =>
new Request(`https://itunes.apple.com/lookup?country=${c}&id=${ids.join(",")}`)
.loadJSON()
.then((a) => a.results),
),
);
let json = (await req).reduce((acc, val) => acc.concat(val), []);
// log(JSON.stringify(json, null, 4))
// return;
log("getting in-app purchases");
if (trackIAPs) {
ui?.present();
if (ui) {
refreshTable();
await new Promise((resolve) => Timer.schedule(100, false, resolve));
}
await Promise.all(
json.map((i) => {
if (!i.trackViewUrl) log("item: " + JSON.stringify(i, null, 4));
let req = new Request(i.trackViewUrl);
return req
.loadString()
.then((html) => {
return getInAppPurchases(html, i.trackViewUrl, i.trackId);
})
.then(
(ia) => {
log("in app for app: " + i.trackViewUrl + "\n" + JSON.stringify(ia, null, 2));
loaded.successful++;
refreshTable();
i.inApp = ia;
},
(err) => {
logError("error by request " + i.trackName + ":\n" + err);
loaded.failed++;
loaded.errors.push(
JSON.stringify(
{
url: i.trackViewUrl,
message: err.message,
line: err.line,
column: err.column,
stack: err.stack,
type: err.type,
name: err.name,
},
null,
2,
),
);
refreshTable();
},
);
}),
);
}
json = json.map((app, i) => {
return {
inApp: app.inApp,
price: app.price,
formattedPrice: app.formattedPrice,
name: app.trackName,
icon: app.artworkUrl60,
trackViewUrl: app.trackViewUrl,
id: app.trackId,
version: app.version,
};
});
// log(JSON.stringify(json, null, 4))
apps.forEach((old, i) => {
let app = json.find((a) => a.id == old.id);
old.price ||= [undefined, undefined];
old.formattedPrice ||= [undefined, undefined];
old.inApp ||= [];
old.version ||= [undefined, undefined];
if (!app) {
log("no app found for: " + old.trackViewUrl);
old.removed = true;
// log(typeof old.price[1]);
if (old.price[1] == null) {
old.price[1] = -1;
old.formattedPrice[1] = "";
}
old.name =
old.name ||
new noSave(
(old.trackViewUrl &&
decodeURI(
old.trackViewUrl.match(
/https?:\/\/(?:itunes|apps)\.apple\.com\/(?:[^/]+\/){2}([^/]+)\/id/,
)[1],
)) ||
"removed",
);
} else {
old.name = app.name;
old.icon = app.icon;
old.trackViewUrl = app.trackViewUrl;
if (old.price[1] !== app.price) {
old.price.shift();
old.price.push(app.price);
old.formattedPrice.shift();
old.formattedPrice.push(app.formattedPrice);
}
if (old.version[1] !== app.version) {
old.version.shift();
old.version.push(app.version);
}
if (trackIAPs) {
if (!app.inApp) {
log("no in-app found for: " + old.trackViewUrl);
old.removed = true;
return;
}
// log("old.inApp:\n" + JSON.stringify(old.inApp, null, 4));
old.inApp = app.inApp.map((ia) => {
// let oldia = old.inApp.find((a) => a.id === ia.id);
// log("ia:\n" + JSON.stringify(ia, null, 4));
let oldia = /*oldia ||*/ old.inApp.find(
(a) => a.name === ia.name && a.price[1] === ia.price,
);
oldia = oldia || old.inApp.find((a) => a.name === ia.name);
// log("oldia:\n" + JSON.stringify(oldia, null, 4));
if (oldia && oldia.price[1] !== ia.price) {
oldia.price.shift();
oldia.price.push(ia.price);
oldia.formattedPrice.shift();
oldia.formattedPrice.push(ia.formattedPrice);
} else if (!oldia) {
oldia = {
price: [undefined, ia.price],
formattedPrice: [undefined, ia.formattedPrice],
name: ia.name,
id: ia.id,
};
}
oldia.id = ia.id;
if (oldia.formattedPrice[1] == null || oldia.formattedPrice[1] == oldia.price[1]) {
oldia.formattedPrice[1] = ia.formattedPrice || oldia.price[1];
}
return oldia;
});
old.inApp.sort((a, b) => a.price[1] - b.price[1]);
}
}
});
// await QuickLook.present(JSON.stringify(apps, null, 4));
apps.sort((a, b) => {
let c = a.price[1] - b.price[1];
return a.removed ? (b.removed ? c : -1) : b.removed ? 1 : c;
});
save();
if (ui && trackIAPs) {
await new Promise((res) => Timer.schedule(100, false, res));
const url =
URLScheme.forRunningScript() +
"?mode=view&failed=" +
apps
.filter((i) => i.removed)
.map((i) => i.id)
.join(",") +
"&errorFindingInApp=" +
errorFindingInAppDueToWebsiteChange;
// const a = new Alert()
// a.title = "url"
// a.message = url;
// await a.presentAlert()
log(url);
if (!autoOpenChangesWhenFinishedLoading || loaded.errors.length) {
loaded.open = url;
refreshTable();
} else {
Safari.open(url);
}
return;
}
} // /!launchMode
let changes = apps
.map((app) => {
let a = {};
Object.entries(app).forEach(([k, v]) => {
a[k] = v;
});
app = a;
app.inApp = trackIAPs ? app.inApp.filter((ia) => typeof ia.price[0] === "number") : [];
return app;
})
.filter((app) => {
return (
app.inApp.length || typeof app.price[0] === "number" || app.removed || app.version[0] != null
);
});
// present results
if (config.runsInNotification) {
let ui = new UITable(),
row,
cell;
if (errorFindingInAppDueToWebsiteChange) {
row = new UITableRow();
row.addText(errorFindingInAppTitle, errorFindingInAppSubtitle).centerAligned();
row.dismissOnSelect = false;
row.onSelect = () => {
Safari.open(forumPostURLOfScript);
};
row.height = 110;
row.backgroundColor = Color.orange();
ui.addRow(row);
}
if (changes.length) {
changes.forEach((app) => {
row = new UITableRow();
row.height = 60;
row.dismissOnSelect = false;
row.onSelect = () => {
Safari.open(app.trackViewUrl);
};
cell = row.addImageAtURL(app.icon);
// cell.centerAligned();
cell.widthWeight = 20;
cell = row.addText(app.name.toString());
cell.widthWeight = 60;
cell = row.addText(
app.formattedPrice[1],
app.formattedPrice[0] && `was ${app.formattedPrice[0]}`,
);
cell.rightAligned();
cell.widthWeight = 30;
if (app.formattedPrice[0]) {
row.backgroundColor =
app.price[1] === 0
? Color.green()
: app.price[1] < app.price[0]
? Color.yellow()
: Color.red();
}
ui.addRow(row);
if (app.removed) {
row = new UITableRow();
row
.addText("There was a problem retrieving data for this app. Maybe the app was removed 🙁")
.centerAligned();
ui.addRow(row);
}
if (trackVersion && app.version[0] != null) {
row = new UITableRow();
row.addText("New version! " + app.version[0] + " -> " + app.version[1]).centerAligned();
ui.addRow(row);
}
if (trackIAPs) {
// inApps
app.inApp.forEach((ia) => {
row = new UITableRow();
row.height = 35;
row.backgroundColor =
ia.price[1] === 0
? Color.green()
: ia.price[1] < ia.price[0]
? Color.yellow()
: Color.red();
// cell spacer
cell = row.addText("");
cell.widthWeight = 4;
cell = row.addText(ia.name);
cell.widthWeight = 60;
cell = row.addText(ia.formattedPrice[1], `was ${ia.formattedPrice[0]}`);
cell.rightAligned();
cell.widthWeight = 30;
ui.addRow(row);
});
}
// row spacer
row = new UITableRow();
row.height = 10;
ui.addRow(row);
});
// remove last spacer
ui.removeRow(row);
} else {
row = new UITableRow();
row.addText("No changes found").centerAligned();
ui.addRow(row);
}
ui.present();
Script.complete();
return;
}
if (config.runsInWidget) {
const w = MessageWidget("This script does not support running in widgets");
Script.setWidget(w);
Script.complete();
return;
}
if (!launchMode || launchMode === "view") {
log("viewing");
const removed = args.queryParameters.failed?.split(",") || [];
// {
// let a = new Alert()
// a.title = "failed :"
// a.message = `${JSON.stringify(removed)}`
// await a.presentAlert()
// }
changes.push(
...apps.filter((app) => {
if (removed.includes(app.id)) {
app.removed = true;
return true;
}
return false;
}),
);
const appToHTML = (app) => {
/* eslint-disable */
return `<tr class="${getColor(app.price)}">
<td>
<a href="${app.trackViewUrl}">
<img src="${app.icon}" width="60" height="60">
</a>
</td>
<td>${app.name}</td>
<td>${
typeof app.price[0] !== "undefined" ? `<del>${app.formattedPrice[0]}</del>&nbsp;` : ""
}${app.formattedPrice[1]}</td>
</tr>
${
app.removed
? `<tr><td colspan="3" class="text-center">
There was a problem retrieving data for this app.<br>
Maybe it was removed 🙁
</td></tr>`
: ""
}
${
trackVersion && app.version[0] != null
? `<tr><td colspan="3" class="text-center">
New version! ${app.version[0]} -&gt; ${app.version[1]}
</td></tr>`
: ""
}
${
trackIAPs
? `<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>&nbsp;` : ""
}${ia.formattedPrice[1]}</td>
</tr>`;
})
.join("")}
</table>
</td></tr>`
: ""
}`;
/* eslint-enable */
};
/* eslint-disable */
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;
}
.table {
max-width: 100%;
}
.text-center {
text-align: center;
white-space: wrap !important;
}
.controls {
margin: 2px;
}
.btn {
margin: 2px;
text-align: left;
}
.btn span {
position: relative;
top: -4px;
}
.btn span:before {
content: "\\2610";
position: relative;
left: -5px;
top: 1px;
font: 20pt "Menlo-Regular";
}
input:checked + .btn span:before {
content: "\\2611";
}
.message {
padding: 10 0 0;
text-align: center;
font-size: 2em;
}
.no-in-app-found {
background-color: orange;
}
.no-in-app-found .title {
font-size: 0.6em;
}
.no-in-app-found .subtitle {
font-size: 0.45em;
}
a.full-size {
width: 100%;
display: inline-block;
color: black;
}
a.full-size:hover,
a.full-size:active,
a.full-size.active,
a.full-size:focus,
a.full-size.focus {
text-decoration: none;
color: black;
}
</style>
</head>
<body>
<div class="d-flex flex-wrap justify-content-around controls">
<input type="checkbox" id="removeApps" hidden>
<label class="btn btn-danger flex-grow-1" for="removeApps"><span>Remove apps</span></label>
<input type="checkbox" id="resetPrices" hidden>
<label class="btn btn-secondary flex-grow-1" for="resetPrices"><span>Reset price changes</span></label>
<input type="checkbox" id="toggleView" hidden ${
defaultToChangesView ? `checked="checked"` : ""
}>
<label class="btn btn-primary flex-grow-1" for="toggleView"><span>Show only changes</span></label>
</div>
${
errorFindingInAppDueToWebsiteChange
? `<div class="message no-in-app-found">
<a class="full-size" href="${forumPostURLOfScript}">
<span class="title">${errorFindingInAppTitle}</span>
<br />
<span class="subtitle">${errorFindingInAppSubtitle}</span>
</a>
</div>`
: ""
}
${
changes.length
? `<table class="table table-striped" id="changesView">
<thead>
<tr>
<th>Icon</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
${changes.map(appToHTML).join("\n")}
</tbody>
</table>`
: `<div id="changesView" class="message">No changes found</div>`
}
<table class="table table-striped" id="fullView">
<thead>
<tr>
<th>Icon</th>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
${apps.map(appToHTML).join("")}
</tbody>
</table>
<script>
let btn = document.getElementById("toggleView");
let changesView = document.getElementById("changesView");
let fullView = document.getElementById("fullView");
function toggleView() {
if (btn.checked) {
changesView.style.display = "";
fullView.style.display = "none";
} else {
changesView.style.display = "none";
fullView.style.display = "";
}
}
btn.addEventListener("change", toggleView);
toggleView();
</script>
</body>
</html>`;
/* eslint-enable */
// Pasteboard.copyString(html);
// await QuickLook.present(html);
wv = new WebView();
wv.shouldAllowRequest = (req) => {
// log("ShouldAllowRequest url: " + req.url);
if (/^(itms-appss|https?):\/\//.test(req.url)) {
Safari.open(req.url);
return false;
}
return true;
};
wv.loadHTML(html);
await wv.present();
launchMode = undefined;
}
let rm = launchMode
? launchMode === "remove"
: await wv.evaluateJavaScript('document.getElementById("removeApps").checked;');
let resetPrices = launchMode
? launchMode === "reset"
: await wv.evaluateJavaScript('document.getElementById("resetPrices").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 = () => {
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 = 65;
cell.leftAligned();
row.addCell(cell);
// price
cell = UITableCell.text(app.formattedPrice[1]);
cell.widthWeight = 17;
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;
});
}
// =========================================
// reset section
if (resetPrices) {
apps.forEach((app) => {
app.price[0] = app.formattedPrice[0] = undefined;
app.version[0] = undefined;
app.inApp.forEach((ia) => {
ia.price[0] = ia.formattedPrice[0] = undefined;
});
});
log("Prices were reset");
}
save();
Script.complete();
/*
Changelog
v1.1.2 - 2025-11-22
Fix for different price formats in different countries
Improve in-app purchases matching to no longer find false positive changes
v1.1.1 - 2025-11-16
Fix for new website data which is different in different languages
v1.1.0 - 2025-11-10
Add progress view while fetching the apps
Fix for new website layout
v1.0.9 - 2021-09-06
Fix some problems with urls
Create/copy file if no file was found
Fix some spelling
v1.0.8 - 2021-04-20
Add option to disable tracking of in-app purchases
Save state more often
v1.0.7 - 2020-10-04
Fixed error when making too many in-app API requests
v1.0.6 - 2020-03-11
Added a check when adding a URL to only allow the addition of apps
v1.0.5 - 2019-09-19
Adapted to the changes of the AppStore website made by Apple. It should find the in-app-purchases again
Added a message if there was a problem fetching the in-app-purchases of all apps (probably due to a new website change)
v1.0.4 - 2019-06-25
Added tracking of app updates
Added overview of found changes for in app result view
Fixed error while adding a new app, as Apple has changed their API slightly
Fixed bug enabling duplicate apps in the list. If you have duplicates, please remove them manually via the interface
v1.0.3 - 2019-03-05
Fixed error "can't find variable chooseItems"
v1.0.2 - 2019-01-29
Fixed not needed inclusion of module "~chooseItems.js"
v1.0.1 - 2019-01-29
Added ability to reset old prices
Fixed error when an app was removed from the AppStore
v1.0 - 2018-12-02
Initial Release
*/
@schima
Copy link

schima commented Nov 16, 2025 via email

@schl3ck
Copy link
Author

schl3ck commented Nov 16, 2025

@schima Unfortunately your attachment got deleted. Can you please add it again?

@schima
Copy link

schima commented Nov 16, 2025

Hello @schl3ck

No worries. I hope this attachment comes thru this time.

IMG_4717

@schima
Copy link

schima commented Nov 19, 2025 via email

@schl3ck
Copy link
Author

schl3ck commented Nov 19, 2025

@schima Yes, I got it, thank you. I'm currently looking into it but I can't give you a date when I've a fix ready.

Could you please send the link to the app?

@schima
Copy link

schima commented Nov 19, 2025

@schl3ck

I appreciate your efforts. Thank YOU. I wish you have a great day today ☺️
I sent the new link to the app and ran it. It gives the following screen

image

@schl3ck
Copy link
Author

schl3ck commented Nov 19, 2025

@schima Sorry, I meant if you can sent me the link to the 1Blocker app, but I think I've got everything I need in the screenshot 👍

@schl3ck
Copy link
Author

schl3ck commented Nov 22, 2025

@schima I've updated the script. I hope your issue is now resolved!

@schima
Copy link

schima commented Nov 22, 2025

@schl3ck yes. I downloaded the file and used it. It looks to be working again 🤗🙏🏼

thank YOU so much. All good for now.
i wish you the best weekend and take care.

If you ever in Vancouver BC Canada area, let me know. I owe you a coffee and lunch 😁

@schl3ck
Copy link
Author

schl3ck commented Nov 23, 2025

@schima Thank you for your offer. Unfortunately I won't be there in the near future because I've not planned any trip to Canada. I live in Austria therefore I can't just hop in the car and go 😅

@schima
Copy link

schima commented Nov 23, 2025

No worries. Thanks for fix 🙏🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment