// 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() }