iOS-Widgets/Apple-Order-Status/Apple-Store-Order-Status.js

579 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: shopping-cart;
// Version 1.2.1
/// Used by enums
const enumValue = (name) => Object.freeze({toString: () => name})
/// Used by DisplayMode
const lightBackgroundColor = Color.white()
const darkBackgroundColor = new Color('#222', 1.0)
const autoBackgroundColor = Color.dynamic(lightBackgroundColor, darkBackgroundColor)
const lightTextColor = Color.black()
const darkTextColor = Color.white()
const autoTextColor = Color.dynamic(lightTextColor, darkTextColor)
const lightBackgroundProgressColor = new Color('#D2D2D7', 1.0)
const darkBackgroundProgressColor = new Color('#707070', 1.0)
const autoBackgroundProgressColor = Color.dynamic(lightBackgroundProgressColor, darkBackgroundProgressColor)
const lightFillProgressColor = new Color('#008009', 1.0)
const darkFillProgressColor = new Color('#00A00D', 1.0)
const autoFillProgressColor = Color.dynamic(lightFillProgressColor, darkFillProgressColor)
/**
* Enum for display mode.
* @readonly
* @enum {{name: string, backgroundColor: Color, textColor: Color}}
*/
const DisplayMode = Object.freeze({
LIGHT: {
name: "light",
backgroundColor: lightBackgroundColor,
textColor: lightTextColor,
backgroundProgressColor: lightBackgroundProgressColor,
fillProgressColor: lightFillProgressColor,
toString: () => name
},
DARK: {
name: "dark",
backgroundColor: darkBackgroundColor,
textColor: darkTextColor,
backgroundProgressColor: darkBackgroundProgressColor,
fillProgressColor: darkFillProgressColor,
toString: () => name
},
AUTO: {
name: "auto",
backgroundColor: autoBackgroundColor,
textColor: autoTextColor,
backgroundProgressColor: autoBackgroundProgressColor,
fillProgressColor: autoFillProgressColor,
toString: () => name
}
})
/**
* Enum for widget family.
* @readonly
* @enum {Symbol}
*/
const WidgetFamily = Object.freeze({
SMALL: enumValue("small"),
MEDIUM: enumValue("medium"),
LARGE: enumValue("large")
})
//////////////////// - EDIT ME - ///////////////////////////
/// Display mode
///
/// - DisplayMode.LIGHT: Light mode
/// - DisplayMode.DARK: Dark mode
/// - DisplayMode.AUTO: Follow system settings
const displayMode = DisplayMode.LIGHT
/// Debug mode: on / off
const debug = false
/// Debug input, following widget format:
/// - "<order-number>;<email>"
/// - "<order-number>;<email>;<item-number>"
///
/// ie. const debugInput = "W111111111;tim@apple.com;5"
const debugInput = null
/// Debug widget size (LARGE, MEDIUM or SMALL)
const debugWidgetFamily = WidgetFamily.MEDIUM
////////////////////////////////////////////////////////////
const cacheMinutes = 60 * 2
const today = new Date()
let width;
let widgetFamily;
const h = 5
const backgroundColor = displayMode.backgroundColor
const textColor = displayMode.textColor
const backgroundProgressColor = displayMode.backgroundProgressColor
const fillProgressColor = displayMode.fillProgressColor
if (debug && debugWidgetFamily !== null) {
widgetFamily = debugWidgetFamily
} else {
switch (config.widgetFamily) {
case 'small':
widgetFamily = WidgetFamily.SMALL
width = 200
break
case 'medium':
widgetFamily = WidgetFamily.MEDIUM
width = 400
break
case 'large':
widgetFamily = WidgetFamily.LARGE
width = 400
break
}
}
switch (widgetFamily) {
case WidgetFamily.SMALL:
width = 200
break
case WidgetFamily.MEDIUM:
width = 400
break
case WidgetFamily.LARGE:
width = 400
break
}
////////////////////////////////////////////////////////////
let widgetInputRAW = args.widgetParameter;
let widgetInput;
if (widgetInputRAW !== null || (debug && debugInput !== null)) {
if (widgetInputRAW !== null) {
widgetInput = widgetInputRAW.toString().trim().split(';').map(v => v.trim())
} else {
widgetInput = debugInput.trim().split(';').map(v => v.trim())
}
if (!/^[A-Za-z][0-9]+/.test(widgetInput[0])) {
throw new Error('Invalid ordernumber format: "' + widgetInput[0] + '"')
}
if (widgetInput[2] && !/^[\d]+$/.test(widgetInput[2])) {
throw new Error('Third parameter has to be a number')
}
} else {
throw new Error('No Ordernumber and E-Mail address set')
}
////////////////////////////////////////////////////////////
const files = FileManager.local()
const path = files.joinPath(files.cacheDirectory(), "widget-apple-store-order-" + widgetInput[0])
const cacheExists = files.fileExists(path)
const cacheDate = cacheExists ? files.modificationDate(path) : 0
////////////////////////////////////////////////////////////
const localeText = {
default: ['Day', 'Days', {
'PLACED': 'Order Placed',
'PROCESSING': 'Processing',
'PREPARED_FOR_SHIPMENT': 'Preparing for Ship',
'SHIPPED': 'Shipped',
'DELIVERED': 'Delivered'
}],
en: ['Day', 'Days', {
'PLACED': 'Order Placed',
'PROCESSING': 'Processing',
'PREPARED_FOR_SHIPMENT': 'Preparing for Ship',
'SHIPPED': 'Shipped',
'DELIVERED': 'Delivered'
}],
de: ['Tag', 'Tage', {
'PLACED': 'Bestellung aufgegeben',
'PROCESSING': 'Vorgang läuft',
'PREPARED_FOR_SHIPMENT': 'Versand wird vorbereitet',
'SHIPPED': 'Bestellung versandt',
'DELIVERED': 'Geliefert'
}],
fr: ['Jour', 'Jours', {
'PLACED': 'Commande enregistrée',
'PROCESSING': 'Traitement',
'PREPARED_FOR_SHIPMENT': 'En cours de préparation pour expédition',
'SHIPPED': 'Expédiée',
'DELIVERED': 'Livrée'
}],
es: ['día', 'días', {
'PLACED': 'Pedido recibido',
'PROCESSING': 'Procesando',
'PREPARED_FOR_SHIPMENT': 'Preparando envío',
'SHIPPED': 'Enviado',
'DELIVERED': 'Entregado'
}],
it: ['giorno', 'giorni', {
'PLACED': 'Ordine inoltrato',
'PROCESSING': 'ElaborazioneIn',
'PREPARED_FOR_SHIPMENT': 'Spedizione in preparazione',
'SHIPPED': 'Spedito',
'DELIVERED': 'ConsegnatoIncompleto'
}]
}
////////////////////////////////////////////////////////////
const parseLongDate = (stringDate) => {
const months = {
'January': 0,
'February': 1,
'March': 2,
'April': 3,
'May': 4,
'June': 5,
'July': 6,
'August': 7,
'September': 8,
'October': 9,
'November': 10,
'December': 11
}
const m = stringDate.match(/([\w]+)[\s]([\d]{1,2}),[\s]([0-9]{4})/)
return new Date(m[3], months[m[1]], m[2])
}
const parseShortDate = (stringDate, orderDate) => {
const months = {
'Jan': 0,
'Feb': 1,
'Mar': 2,
'Apr': 3,
'May': 4,
'Jun': 5,
'Jul': 6,
'Aug': 7,
'Sep': 8,
'Oct': 9,
'Okt': 9,
'Nov': 10,
'Dec': 11,
'Dez': 11
}
let m
m = stringDate.match(/([\d]{1,2}) ([\w]{3})/)
if (!m) {
m = stringDate.match(/([\w]+),? ([\d]{1,2})/)
if (m) {
const t = m[1].slice(0, 3)
m[1] = m[2]
m[2] = t
} else {
throw new Error('Failed to extract the delivery date from string: ' + stringDate)
}
}
let deliveryDate = new Date((new Date().getFullYear()), months[m[2]], m[1])
if (deliveryDate < orderDate) {
deliveryDate.setFullYear(deliveryDate.getFullYear() + 1)
}
return deliveryDate
}
////////////////////////////////////////////////////////////
function creatProgress(total, havegone) {
const context = new DrawContext()
context.size = new Size(width, h)
context.opaque = false
context.respectScreenScale = true
context.setFillColor(backgroundProgressColor)
const path = new Path()
path.addRoundedRect(new Rect(0, 0, width, h), 3, 2)
context.addPath(path)
context.fillPath()
context.setFillColor(fillProgressColor)
const path1 = new Path()
const path1width = (width * havegone / total > width) ? width : width * havegone / total
path1.addRoundedRect(new Rect(0, 0, path1width, h), 3, 2)
context.addPath(path1)
context.fillPath()
return context.getImage()
}
////////////////////////////////////////////////////////////
const getTimeRemaining = function (endtime) {
const total = Date.parse(endtime) - Date.parse(new Date());
const seconds = Math.floor((total / 1000) % 60);
const minutes = Math.floor((total / 1000 / 60) % 60);
const hours = Math.floor((total / (1000 * 60 * 60)) % 24);
const days = Math.floor(total / (1000 * 60 * 60 * 24));
return {
total,
days,
hours,
minutes,
seconds
};
}
////////////////////////////////////////////////////////////
const getOrderdetails = async (ordernumber, email) => {
const reqSession = new Request('https://secure.store.apple.com/shop/order/list')
resSession = await reqSession.loadString()
const CookieValues = reqSession.response.cookies.map((v) => {
return v.name + "=" + v.value
})
const xAosStkMatch = resSession.match(/"x-aos-stk":"([\w-_]+)"/)
if (!xAosStkMatch) {
throw new Error('Needed x-aos-stk token not found')
}
const postUrl = (reqSession.response.url.replace('/orders', '/orderx')) + '&_a=guestUserOrderLookUp&_m=signIn.orderLookUp'
const postReq = new Request(postUrl)
postReq.headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': reqSession.response.url,
'x-aos-model-page': 'olssSignInPage',
'x-aos-stk': xAosStkMatch[1],
'X-Requested-With': 'XMLHttpRequest',
'Cookie': CookieValues.join('; ')
}
postReq.method = "POST";
postReq.body = `signIn.orderLookUp.orderNumber=${ordernumber}&signIn.orderLookUp.emailAddress=${email}`
const resPostReq = await postReq.loadString()
if (postReq.response.statusCode !== 200) {
throw new Error(`Got HTTP ${postReq.response.statusCode} from API.`)
}
let postResData
try {
postResData = JSON.parse(resPostReq)
} catch (e) {
throw new Error('Can\'t parse API response.')
}
if (postResData['head']['status'] !== 302) {
throw new Error('Fetching the data failed. Got unexpected response. Please try it later.')
}
const req = new Request(postResData['head']['data']['url'])
const res = await req.loadString()
const rawJSON = res.match(/<script id="init_data" type="application\/json">[\s]+(.*)[\s]+<\/script>/)
if (!rawJSON) {
return null
}
const data = JSON.parse(rawJSON[1])
if (!data['orderDetail']) {
console.log(data)
throw new Error('no orderDetail attribute')
}
data.widgetURL = postResData['head']['data']['url']
return data
}
////////////////////////////////////////////////////////////
let orderDetails
if (cacheExists && (today.getTime() - cacheDate.getTime()) < (cacheMinutes * 60 * 1000)) {
console.log("Get from Cache")
orderDetails = JSON.parse(files.readString(path))
} else {
console.log("Get from Website")
try {
orderDetails = await getOrderdetails(widgetInput[0], widgetInput[1])
if (orderDetails !== null) {
console.log("Write to Cache")
files.writeString(path, JSON.stringify(orderDetails))
}
} catch (e) {
console.error('Fetching data from website failed:')
console.error(e)
if (cacheExists) {
console.warn('Fallback to Cache')
orderDetails = JSON.parse(files.readString(path))
} else {
throw new Error('Fetching the data failed. No data to show.')
}
}
}
if (debug) {
console.log(JSON.stringify(orderDetails, null, 2))
}
let widget = new ListWidget();
if (!orderDetails) {
widget.addText('No order found')
} else {
// filter on orderItem to remove giveBackOrderItem
orderDetails['orderDetail']['orderItems']['c'] = orderDetails['orderDetail']['orderItems']['c'].filter((e) => {
return /orderItem-[\d]+/.test(e)
})
if (widgetInput[2] && !orderDetails['orderDetail']['orderItems']['c'][widgetInput[2] - 1]) {
throw new Error(`No Item on position ${widgetInput[2]}`)
}
const languageCode = Device.preferredLanguages()[0].match(/^[\a-z]{2}/)
const itemPosition = orderDetails['orderDetail']['orderItems']['c'][(widgetInput[2] - 1) || 0]
const itemDetails = orderDetails['orderDetail']['orderItems'][itemPosition]['orderItemDetails']
const itemStatusTracker = orderDetails['orderDetail']['orderItems'][itemPosition]['orderItemStatusTracker']
const orderDate = parseLongDate(orderDetails['orderDetail']['orderHeader']['d']['orderPlacedDate'])
let deliveryDate = null
try {
deliveryDate = parseShortDate(itemDetails['d']['deliveryDate'], orderDate)
} catch (e) {
console.error(e)
}
const itemName = itemDetails['d']['productName']
const itemImageUrl = itemDetails['d']['imageData']['src'].replace(/wid=[\d]+/, 'wid=200').replace(/hei=[\d]+/, 'hei=200')
const itemImage = await(new Request(itemImageUrl)).loadImage()
const remainingDays = getTimeRemaining(deliveryDate).days + 1;
widget.setPadding(10, 10, 10, 10)
widget.backgroundColor = backgroundColor
widget.url = orderDetails.widgetURL
const headlineText = widget.addText(' Order Status')
headlineText.font = Font.regularSystemFont(14)
headlineText.textColor = textColor
headlineText.centerAlignText()
widget.addSpacer(5)
const productStack = widget.addStack()
productStack.layoutHorizontally()
const imageStack = productStack.addStack()
imageStack.backgroundColor = Color.white()
imageStack.size = new Size(37, 37)
imageStack.setPadding(1, 1, 1, 1)
imageStack.cornerRadius = 2
itemImageElement = imageStack.addImage(itemImage)
itemImageElement.imageSize = new Size(35, 35)
productStack.addSpacer(20)
rightProductStack = productStack.addStack()
rightProductStack.layoutVertically()
rightProductStack.addSpacer(5)
const itemNameText = rightProductStack.addText(itemName)
itemNameText.font = Font.regularSystemFont(10)
itemNameText.textColor = textColor
itemNameText.minimumScaleFactor = 0.5
if (widgetFamily === WidgetFamily.SMALL) {
itemNameText.lineLimit = 4
} else {
itemNameText.lineLimit = 2
}
widget.addSpacer()
if (deliveryDate !== null && itemStatusTracker['d']['currentStatus'] !== 'DELIVERED') {
const t = (localeText[languageCode]) ? localeText[languageCode] : localeText.default
let postFix = (remainingDays === 1) ? t[0] : t[1]
const remainingDayText = widget.addText(remainingDays + ' ' + postFix)
remainingDayText.font = Font.regularSystemFont(26)
remainingDayText.textColor = textColor
remainingDayText.centerAlignText()
remainingDayText.minimumScaleFactor = 0.5
widget.addSpacer()
const total = (deliveryDate - orderDate) / (1000 * 60 * 60 * 24)
const daysGone = total - remainingDays
const progressStack = widget.addStack()
progressStack.layoutVertically()
progressStack.spacing = 3
if (itemStatusTracker['d']['currentStatus']) {
let statusText
try {
const localeStatusText = (localeText[languageCode] && localeText[languageCode][2]) ? localeText[languageCode][2] : localeText.default[2]
statusText = progressStack.addText(localeStatusText[itemStatusTracker['d']['currentStatus']])
} catch (e) {
console.error(e)
statusText = progressStack.addText(itemStatusTracker['d']['currentStatus'])
}
statusText.textColor = textColor
statusText.font = Font.regularSystemFont(8)
}
progressStack.addImage(creatProgress(total, daysGone))
const footerStack = progressStack.addStack()
footerStack.layoutHorizontally()
const orderDateText = footerStack.addText(orderDate.toLocaleDateString())
orderDateText.textColor = textColor
orderDateText.font = Font.regularSystemFont(8)
orderDateText.lineLimit = 1
footerStack.addSpacer()
const deliveryDateText = footerStack.addText(deliveryDate.toLocaleDateString())
deliveryDateText.textColor = textColor
deliveryDateText.font = Font.regularSystemFont(8)
deliveryDateText.lineLimit = 1
} else {
widget.addSpacer()
fallbackStack = widget.addStack()
fallbackStack.layoutHorizontally()
fallbackStack.addSpacer()
let icon
let text
if (itemStatusTracker['d']['currentStatus'] === 'DELIVERED') {
icon = SFSymbol.named('house')
const localeStatusText = (localeText[languageCode] && localeText[languageCode][2]) ? localeText[languageCode][2] : localeText.default[2]
text = localeStatusText['DELIVERED']
} else if (itemDetails['d']['deliveryDate'] === 'Out for Delivery') {
icon = SFSymbol.named('shippingbox')
text = itemDetails['d']['deliveryDate'] // ToDO: Add translation
} else {
text = itemDetails['d']['deliveryDate']
}
if (icon) {
const iconStack = fallbackStack.addStack()
iconStack.layoutVertically()
iconStack.addSpacer()
const iconElement = iconStack.addImage(icon.image)
iconElement.imageSize = new Size(15, 15)
iconStack.addSpacer()
fallbackStack.addSpacer(5)
}
const fallbackTextStack = fallbackStack.addStack()
fallbackTextStack.layoutVertically()
fallbackTextStack.centerAlignContent()
fallbackTextStack.addSpacer()
const fallbackText = fallbackTextStack.addText(text)
fallbackText.font = Font.regularSystemFont(14)
fallbackText.textColor = textColor
fallbackText.minimumScaleFactor = 0.5
fallbackText.lineLimit = 1
fallbackTextStack.addSpacer()
fallbackStack.addSpacer()
widget.addSpacer()
}
}
if (!config.runsInWidget) {
// Present in debug mode
switch (widgetFamily) {
case WidgetFamily.SMALL:
await widget.presentSmall()
break
case WidgetFamily.MEDIUM:
await widget.presentMedium()
break
case WidgetFamily.LARGE:
await widget.presentLarge()
break
}
} else {
// Tell the system to show the widget.
Script.setWidget(widget)
Script.complete()
}