Export Apple Purchase History

Export Purchase History from Apple to a csv file

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Export Apple Purchase History
// @namespace    https://zhuzi.dev
// @version      v0.2
// @description  Export Purchase History from Apple to a csv file
// @author       Bambooom
// @homepageURL  https://zhuzi.dev
// @supportURL   https://zhuzi.dev
// @license      MIT
// @match        https://reportaproblem.apple.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=apple.com
// @grant        none
// ==/UserScript==

(function () {
  ('use strict');

  let count = 0;
  let timer = setInterval(() => {
    if (document.querySelector('.purchase.loaded.collapsed') || count > 50) {
      clearInterval(timer);
      init()
    }
    count++
  }, 200);

  let count2 = 0; // no loading-indicator count
  function waitLoading() {
    setTimeout(() => {
      if (document.querySelector('.purchases > .loading-indicator')) {
        console.log('still need loading: ');
        waitLoading();
      } else {
        autoScroll();
      }
    }, 100);
  }

  function autoScroll() {
    window.scrollTo(0, document.body.scrollHeight);

    setTimeout(() => {
      if (document.querySelector('.purchases > .loading-indicator')) {
        count2 = 0;
        console.log('wait for loading');
        waitLoading();
      } else {
        count2++;

        if (count2 < 5) {
          console.log('going to scroll again');
          autoScroll();
        } else {
          alert('All history loaded.');
        }
      }
    }, 100);
  }

  function getRows() {
    const blocks = Array.from(
      document.querySelectorAll('.purchase.loaded.collapsed')
    );
    const rows = [];

    for (const block of blocks) {
      const date = block
        .querySelector('.purchase-header .invoice-date')
        .textContent.trim(); // '22 Jun 2024'
      const webOrderId = block
        .querySelector('.purchase-header .second-element')
        .textContent.trim(); // 'R24Q46T28S16ZB'
      // const price = block
      //   .querySelector('.purchase-header .third-element span span')
      //   .textContent.trim(); //  total: '¥0.00'

      const list = Array.from(
        block.querySelectorAll('.pli-list.applicable-items .pli')
      );
      list.forEach((li) => {
        const orderId = li.querySelector('label').attributes.for.value; // '76084983461860'
        const name = li
          .querySelector('.pli-data-fields .pli-title')
          .textContent.trim();
        const publisher = li.querySelector('.pli-publisher')
          ? li.querySelector('.pli-publisher').textContent.trim()
          : '';
        const priceText = li.querySelector('.pli-price').textContent.trim();
        const price = priceText === 'Free' ? '0' : priceText;
        const icon = li.querySelector('.pli-artwork img').src;
        rows.push({
          date,
          webOrderId,
          orderId,
          name,
          publisher,
          price,
          icon,
          price,
        });
      });
    }

    return rows;
  }

  function exportData() {
    const rows = getRows();
    JsonToCSV.exportToCSV(
      rows,
      [
        { title: 'Name', key: 'name' },
        { title: 'Date', key: 'date' },
        { title: 'Web Order Id', key: 'webOrderId' },
        { title: 'Order Id', key: 'orderId' },
        { title: 'Publisher', key: 'publisher' },
        { title: 'Price', key: 'price' },
        { title: 'Icon', key: 'icon' },
      ],
      'purchase-history-' +
        new Date().toISOString().split('T')[0].replaceAll('-', '')
    );
  }

  function init() {
    const div1 = document.createElement('div');
    const btn1 = document.createElement('button');
    const btn2 = document.createElement('button');
    btn1.textContent = 'Load all';
    btn2.textContent = 'Export to csv';
    btn1.classList.add('button', 'button-block', 'load-all-btn');
    btn2.classList.add('button', 'button-block', 'load-all-btn');
    btn2.style.marginLeft = '10px';
    div1.appendChild(btn1);
    div1.appendChild(btn2);
    document.querySelector('.search-bar').after(div1);
    const div2 = div1.cloneNode(true);
    document.querySelector('.purchases').after(div2);

    btn1.onclick = autoScroll;
    btn2.onclick = exportData;
  }

  // https://github.com/liqingzheng/export-to-CSV/blob/master/export-to-CSV/export-to-CSV.js
  var JsonToCSV = {
    /*
     * exportToCSV 导出CSV
     * @ param  {Array} data 导出数据 必填项
     * @ param  {Array} columns 导出表头 必填项
     * @ param  {String} fileName 导出文件名称
     */
    exportToCSV(data = [], columns = [], fileName = 'userExportToCSV') {
      if (!data.length && !columns.length) {
        console.error(
          '\u5bfc\u51fa\u6570\u636e\u548c\u8868\u5934\u4e0d\u4e3a\u7a7a'
        );
        return this;
      }
      const bw = this.browser();
      if (bw.ie < 9) return this;
      let CSV = '',
        arr = [],
        colKey,
        curvalue;
      columns.forEach(function (item) {
        arr.push(item.title || item.key);
      });
      CSV = CSV + arr.join(',') + '\r\n';
      data.forEach(function (item) {
        arr = [];
        curvalue = '';
        columns.forEach(function (col) {
          colKey = col.key;
          if (
            typeof item[colKey] == 'string' ||
            typeof item[colKey] === 'number'
          ) {
            curvalue =
              typeof col.formatter === 'function'
                ? col.formatter(item, col, colKey)
                : item[colKey];
            curvalue = typeof curvalue === 'function' ? '' : curvalue + '';
            curvalue = curvalue.replace(/\,/gi, '');
          }
          arr.push(curvalue);
        });
        CSV = CSV + arr.join(',') + '\r\n';
      });
      this.SaveAs(fileName, CSV);
    },
    SaveAs(fileName = '', csvData = '') {
      const bw = this.browser();
      if (!bw.edge || !bw.ie) {
        let alink = document.createElement('a');
        alink.id = 'linkDwnldLink';
        alink.href = this.getDownloadUrl(csvData);
        document.body.appendChild(alink);
        let linkDom = document.getElementById('linkDwnldLink');
        linkDom.setAttribute('download', fileName);
        linkDom.click();
        document.body.removeChild(linkDom);
      } else if (bw.ie >= 10 || bw.edge == 'edge') {
        let _utf = '\uFEFF';
        let _csvData = new Blob([_utf + csvData], {
          type: 'text/csv',
        });
        navigator.msSaveBlob(_csvData, fileName);
      } else {
        let oWin = window.top.open('about:blank', '_blank');
        oWin.document.write('sep=,\r\n' + csvData);
        oWin.document.close();
        oWin.document.execCommand('SaveAs', true, fileName);
        oWin.close();
      }
    },
    getDownloadUrl(csvStr = '') {
      let _utf = '\uFEFF'; // 为了使Excel以utf-8的编码模式,同时也是解决中文乱码的问题
      if (window.Blob && window.URL && window.URL.createObjectURL) {
        let csvData = new Blob([_utf + csvStr], {
          type: 'text/csv',
        });
        return URL.createObjectURL(csvData);
      }
    },
    browser() {
      const Sys = {};
      const ua = navigator.userAgent.toLowerCase();
      let s;
      (s =
        ua.indexOf('edge') !== -1
          ? (Sys.edge = 'edge')
          : ua.match(/rv:([\d.]+)\) like gecko/))
        ? (Sys.ie = s[1])
        : (s = ua.match(/msie ([\d.]+)/))
        ? (Sys.ie = s[1])
        : (s = ua.match(/firefox\/([\d.]+)/))
        ? (Sys.firefox = s[1])
        : (s = ua.match(/chrome\/([\d.]+)/))
        ? (Sys.chrome = s[1])
        : (s = ua.match(/opera.([\d.]+)/))
        ? (Sys.opera = s[1])
        : (s = ua.match(/version\/([\d.]+).*safari/))
        ? (Sys.safari = s[1])
        : 0;
      return Sys;
    },
  };
})();