Skip to content

Instantly share code, notes, and snippets.

@marco79cgn
Last active September 28, 2025 13:14
Show Gist options
  • Save marco79cgn/40ce08a1735dede2ab35acf375b6a4df to your computer and use it in GitHub Desktop.
Save marco79cgn/40ce08a1735dede2ab35acf375b6a4df to your computer and use it in GitHub Desktop.
Custom iOS widget that shows both the store and online availability of a given product (for Scriptable.app)
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: teal; icon-glyph: apple-alt;
// default zipOrStore and partNo - will be overwritten by your widget parameters
let zipOrStore = '50670'
let partNo = "MFHP4ZM/A"
// insert your ntfy url
const notifyUrl = "https://ntfy.sh/******"
// force push notification - set to true in order to test that your setup is working correctly
const forcePushNotification = false
let storeName
let city
let params = getParams(args.widgetParameter)
const shopAvailability = await getShopAvailability()
const onlineAvailability = await getOnlineAvailability()
loadCachedAvailability()
let widget = new ListWidget()
widget.setPadding(0, 8, 6, 8)
let appleText = widget.addText("")
appleText.centerAlignText()
appleText.font = Font.boldMonospacedSystemFont(24)
widget.addSpacer(2)
let productText = widget.addText(shopAvailability.product)
productText.font = Font.boldRoundedSystemFont(13)
// productText.textColor = Color.orange()
productText.textOpacity = 1
productText.lineLimit = 4
productText.minimumScaleFactor = 0.8
productText.centerAlignText()
widget.addSpacer(4)
let onlineText = widget.addText("Online")
onlineText.font = Font.semiboldRoundedSystemFont(11)
onlineText.textOpacity = 0.5
onlineText.centerAlignText()
let onlineAvailabilityText = widget.addText(onlineAvailability)
onlineAvailabilityText.font = Font.semiboldRoundedSystemFont(11)
onlineAvailabilityText.textOpacity = 1
onlineAvailabilityText.centerAlignText()
widget.addSpacer(3)
let storeText
if(shopAvailability.storeName !== shopAvailability.city) {
storeText = widget.addText(shopAvailability.storeName + ' ' + shopAvailability.city)
} else {
storeText = widget.addText(shopAvailability.city)
}
storeText.font = Font.semiboldRoundedSystemFont(11)
storeText.textOpacity = 0.5
storeText.lineLimit = 2
storeText.minimumScaleFactor = 0.8
storeText.centerAlignText()
let availabilityText = widget.addText(shopAvailability.message)
availabilityText.font = Font.semiboldRoundedSystemFont(11)
if (shopAvailability.message.toLowerCase().indexOf('nicht') >=0) {
availabilityText.textColor = new Color("#DF0000")
await saveStatus("unavailable")
} else {
availabilityText.textColor = Color.green()
// check if unavailable before
const previousStatus = await loadCachedAvailability()
if (previousStatus == "unavailable") {
await sendNotification()
}
await saveStatus("available")
}
if(forcePushNotification) {
await sendNotification()
}
availabilityText.textOpacity = 1
availabilityText.minimumScaleFactor = 0.8
availabilityText.centerAlignText()
widget.url = "https://store.apple.com/de/xc/product/" + params.partNo
if(config.runsInApp) {
widget.presentSmall()
}
Script.setWidget(widget)
Script.complete()
// fetches the local shop availability
async function getShopAvailability() {
let availabilityMessage
let productName
let url
if(zipOrStore.length == 5) {
console.log("Using zip: " + zipOrStore)
url = "https://www.apple.com/de/shop/retail/pickup-message?pl=true&parts.0=" + params.partNo + "&location=" + params.zipOrStore
} else {
console.log("Using given unique store " + zipOrStore)
url = "https://www.apple.com/de/shop/retail/pickup-message?pl=true&parts.0=" + params.partNo + "&purchaseOption=fullPrice&mts.0=regular&mts.1=sticky&fts=true&store=" + params.zipOrStore
}
let req = new Request(url)
try {
let result = await req.loadJSON()
console.log("result: " + JSON.stringify(result))
let store = result.body.stores[0]
const item = store.partsAvailability[params.partNo]
availabilityMessage = item.pickupSearchQuote
productName = item.messageTypes.regular.storePickupProductTitle
storeName = store.storeName
city = store.city
} catch(exception){
console.log("Exception Occured.")
availabilityMessage = "N/A"
productName = "Keine Internetverbindung"
storeName = "Oder ungültige"
city = "PartNo"
}
return { "message" : availabilityMessage, "product" : productName , "storeName" : storeName, "city" : city }
}
// fetches the online store availability
async function getOnlineAvailability() {
let deliveryDate
const url = "https://www.apple.com/de/shop/delivery-message?mt=regular&parts.0=" + params.partNo
let req = new Request(url)
try {
let result = await req.loadJSON()
// console.log(result)
deliveryDate = result.body.content.deliveryMessage[params.partNo].regular.deliveryOptions[0].date
} catch(exception){
console.log("Exception Occured. " + exception)
deliveryDate = "N/A"
}
return deliveryDate
}
function getParams(widgetParams) {
if(widgetParams) {
let split = widgetParams.split(';')
partNo = split[0]
if(split.length > 1) {
zipOrStore = split[1]
}
}
return { "partNo" : partNo, "zipOrStore" : zipOrStore }
}
async function sendNotification() {
let req = new Request(notifyUrl)
req.method = "POST"
if(forcePushNotification) {
req.headers = {
"Title": "*TEST MODE* Apple Store",
"Click": "https://store.apple.com/de/xc/product/" + params.partNo,
"Tags": "apple"
}
} else {
req.headers = {
"Title": "Apple Store",
"Click": "https://store.apple.com/de/xc/product/" + params.partNo,
"Tags": "apple"
}
}
req.body = shopAvailability.product +" ist jetzt verfügbar im Apple Store " + storeName + " in " + city + "! (Tap to order)"
req.loadJSON()
}
async function loadCachedAvailability() {
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let partNoEscaped = params.partNo.replace("/", "-")
let path = fm.joinPath(dir, partNoEscaped + ".txt")
let lastStatus = Data.fromFile(path)
if (lastStatus != null) {
return lastStatus.toRawString()
} else {
return "first-call"
}
}
async function saveStatus(status) {
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let partNoEscaped = params.partNo.replace("/", "-")
let path = fm.joinPath(dir, partNoEscaped + ".txt")
fm.writeString(path, status)
}
@marco79cgn
Copy link
Author

Hi Nick,
du hast dir deine Frage bereits selbst beantwortet. ;)
Wenn du die Parameter in den Widget Einstellungen vornimmst, musst du das Skript selbst überhaupt nicht anfassen bzw. editieren. Zudem musst du es nur ein einziges Mal in Scriptable hinterlegen und kannst dennoch mehrere Widgets für unterschiedliche Produkte oder Postleitzahlen auf deinem Homescreen anlegen.

Die Kodierung im Skript selbst ist nur ein Fallback, falls man keine Parameter im Widget selbst gesetzt hat. Letztere haben aber Priorität, sofern vorhanden.

@NickCrack
Copy link

Hi Nick, du hast dir deine Frage bereits selbst beantwortet. ;) Wenn du die Parameter in den Widget Einstellungen vornimmst, musst du das Skript selbst überhaupt nicht anfassen bzw. editieren. Zudem musst du es nur ein einziges Mal in Scriptable hinterlegen und kannst dennoch mehrere Widgets für unterschiedliche Produkte oder Postleitzahlen auf deinem Homescreen anlegen.

Die Kodierung im Skript selbst ist nur ein Fallback, falls man keine Parameter im Widget selbst gesetzt hat. Letztere haben aber Priorität, sofern vorhanden.

Vielen Dank und macht absolut Sinn 👍☺️

Da bin ich doch mal gespannt ob ich so mein iPhone bekomme.

Die Frage bezüglich Aktualisierung hattest du weiter oben ja bereits beantwortet

@marco79cgn
Copy link
Author

iPhone 17 Pro | Max:

Cosmic Orange:
256 GB: MG8H4ZD/A | MFYN4ZD/A
512 GB: MG8M4ZD/A | MFYT4ZD/A
1 TB: MG8Q4ZD/A | MFYW4ZD/A
2 TB: - | MG004ZD/A

Tiefblau:
256 GB: MG8J4ZD/A | MFYP4ZD/A
512 GB: MG8N4ZD/A | MFYU4ZD/A
1 TB: MG8R4ZD/A | MFYX4ZD/A
2 TB: - | MG014ZD/A

Silber:
256 GB: MG8G4ZD/A | MFYM4ZD/A
512 GB: MG8K4ZD/A | MFYQ4ZD/A
1 TB: MG8P4ZD/A | MFYV4ZD/A
2 TB: - | MFYY4ZD/A

iPhone Air:

Himmelblau:
256 GB: MG2P4ZD/A
512 GB: MG2V4ZD/A
1 TB: MG304ZD/A

Schwarz:
256 GB: MG2L4ZD/A
512 GB: MG2Q4ZD/A
1 TB: MG2W4ZD/A

Wolkenweiß:
256 GB: MG2M4ZD/A
512 GB: MG2T4ZD/A
1 TB: MG2X4ZD/A

Lichtgold:
256 GB: MG2N4ZD/A
512 GB: MG2U4ZD/A
1 TB: MG2Y4ZD/A

@marco79cgn
Copy link
Author

Update: 12.09.2025

Die Postleitzahl (zip) kann jetzt optional auch eine Store-ID sein. In diesem Fall wird nur explizit für den einen, angegebenen Store geprüft. Im Falle einer Postleitzahl werden auch die Stores in der Umgebung berücksichtigt.

Store IDs:

R559 → Köln Schildergasse
R520 → Köln Rheincenter
R331 → Düsseldorf
R403 → Centro Oberhausen
R434 → Sulzbach MTZ
R352 → Frankfurt (Große Bockenheimer Straße)
R455 → Hannover
R519 → Sindelfingen
R396 → Hamburg Jungfernstieg
R366 → Hamburg Alstertal
R431 → Augsburg City Galerie
R521 → München OEZ
R045 → München Rosenstraße
R430 → Dresden Altmarkt-Galerie
R358 → Berlin Kurfürstendamm
R443 → Berlin Rosenthaler Straße

@wir43
Copy link

wir43 commented Sep 27, 2025

Hallo Marco, super Script, danke dafür. Leider bekomme ich immer nur die Meldung „keine Internetverbindung“ im Widget, wenn ich statt der PLZ die Store ID nutze, ist das ein bekannter Fehler?
Mit der PLZ funktioniert es einwandfrei .

IMG_6952

@marco79cgn
Copy link
Author

marco79cgn commented Sep 28, 2025

Hallo Marco, super Script, danke dafür. Leider bekomme ich immer nur die Meldung „keine Internetverbindung“ im Widget, wenn ich statt der PLZ die Store ID nutze, ist das ein bekannter Fehler? Mit der PLZ funktioniert es einwandfrei .

@wir43 Danke für's Feedback! Da hat Apple tatsächlich mal was an deren API geändert. Ist korrigiert. Bitte das Skript neu kopieren von oben.

@wir43
Copy link

wir43 commented Sep 28, 2025

Funktioniert mit der Store ID wieder, danke für die schnelle Korrektur.

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