Currency Converter

try to take over the world!

  1. // ==UserScript==
  2. // @name Currency Converter
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.3.0
  5. // @description try to take over the world!
  6. // @icon http://store.steampowered.com/favicon.ico
  7. // @author Bisumaruko
  8. // @include http*://yuplay.ru/*
  9. // @include http*://*.gamersgate.com/*
  10. // @include http*://www.greenmangaming.com/*
  11. // @include http*://gama-gama.ru/*
  12. // @include http*://*.gamesplanet.com/*
  13. // @include http*://www.cdkeys.com/*
  14. // @include http*://directg.net/*
  15. // @include http*://www.humblebundle.com/*
  16. // @include http*://www.indiegala.com/*
  17. // @include http*://www.bundlestars.com/*
  18. // @include http*://www.opiumpulses.com/*
  19. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.slim.min.js
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.6/sweetalert2.min.js
  21. // @resource SweetAlert2CSS https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.6/sweetalert2.min.css
  22. // @grant GM_xmlhttpRequest
  23. // @grant GM_setValue
  24. // @grant GM_getValue
  25. // @grant GM_addStyle
  26. // @grant GM_getResourceText
  27. // @run-at document-start
  28. // @connect www.ecb.europa.eu
  29. // ==/UserScript==
  30.  
  31. /* global swal, games */
  32.  
  33. // inject swal css
  34. GM_addStyle(
  35. GM_getResourceText('SweetAlert2CSS'),
  36. );
  37.  
  38. // setup swal
  39. swal.setDefaults({
  40. timer: 3000,
  41. useRejections: false,
  42. });
  43.  
  44. // load config
  45. const config = JSON.parse(
  46. GM_getValue('Bisko_CC', '{}'),
  47. );
  48. const interval = 3 * 60 * 60 * 1000; // update exchange rate every 3 hours
  49. const currencies = {
  50. ORI: {
  51. nameEN: '',
  52. nameZH: '恢復',
  53. symbol: '',
  54. },
  55. AUD: {
  56. nameEN: 'Australian Dollar',
  57. nameZH: '澳幣',
  58. symbol: 'AU$',
  59. },
  60. CAD: {
  61. nameEN: 'Canadian Dollar',
  62. nameZH: '加幣',
  63. symbol: 'CA$',
  64. },
  65. CNY: {
  66. nameEN: 'Chinese Yuan',
  67. nameZH: '人民幣',
  68. symbol: 'CN¥',
  69. },
  70. EUR: {
  71. nameEN: 'Euro',
  72. nameZH: '歐元',
  73. symbol: '€',
  74. },
  75. GBP: {
  76. nameEN: 'Great Britain Pound',
  77. nameZH: '英鎊',
  78. symbol: '£',
  79. },
  80. HKD: {
  81. nameEN: 'Hong Kong Dollar',
  82. nameZH: '港幣',
  83. symbol: 'HK$',
  84. },
  85. JPY: {
  86. nameEN: 'Japanese Yen',
  87. nameZH: '日圓',
  88. symbol: 'JP¥',
  89. },
  90. KRW: {
  91. nameEN: 'South Korean Won',
  92. nameZH: '韓圓',
  93. symbol: '₩',
  94. },
  95. MYR: {
  96. nameEN: 'Malaysian Ringgit',
  97. nameZH: '令吉',
  98. symbol: 'RM',
  99. },
  100. NTD: {
  101. nameEN: 'New Taiwan Dollar',
  102. nameZH: '台幣',
  103. symbol: 'NT$',
  104. },
  105. NZD: {
  106. nameEN: 'New Zealand Dollar',
  107. nameZH: '紐幣',
  108. symbol: 'NZ$',
  109. },
  110. RUB: {
  111. nameEN: 'Russian Ruble',
  112. nameZH: '盧布',
  113. symbol: 'руб',
  114. },
  115. USD: {
  116. nameEN: 'United States Dollar',
  117. nameZH: '美元',
  118. symbol: 'US$',
  119. },
  120. };
  121.  
  122. let originalCurrency = 'USD';
  123.  
  124. // jQuery extension
  125. $.fn.price = function price() {
  126. return this.eq(0).text().replace(/[^.0-9]/g, '');
  127. };
  128.  
  129. $.fn.bindPriceData = function bindPriceData() {
  130. return this.each((index, element) => {
  131. const $ele = $(element);
  132.  
  133. $ele
  134. .addClass('CCPrice')
  135. .attr('data-CCPrice', JSON.stringify({
  136. currency: originalCurrency,
  137. price: $ele.text().replace(/[^.0-9]/g, ''),
  138. }));
  139. });
  140. };
  141.  
  142. // constructing functions
  143. const has = Object.prototype.hasOwnProperty;
  144. const preferredCurrency = () => (has.call(config.exchangeRate.rates, config.preferredCurrency) ? config.preferredCurrency : 'CNY'); // default to CNY
  145. const updateCurrency = (currency = null) => {
  146. let targetCurrency = currency || preferredCurrency();
  147.  
  148. $('.CCPrice').each((index, element) => {
  149. const $ele = $(element);
  150. const data = JSON.parse(
  151. $ele.attr('data-CCPrice'),
  152. );
  153.  
  154. let convertedPrice = 0;
  155.  
  156. if (targetCurrency === 'ORI') {
  157. targetCurrency = data.currency;
  158. convertedPrice = data.price;
  159. } else {
  160. const rates = config.exchangeRate.rates;
  161.  
  162. convertedPrice = data.price * (rates[targetCurrency] / rates[data.currency]);
  163. }
  164.  
  165. $ele.text(
  166. convertedPrice
  167. .toLocaleString('en', {
  168. style: 'currency',
  169. currency: targetCurrency,
  170. maximumFractionDigits: 2,
  171. })
  172. .replace('MYR', currencies.MYR.symbol)
  173. .replace('NTD', currencies.NTD.symbol),
  174. );
  175. });
  176. };
  177. const constructMenu = () => {
  178. const $li = $('<li class="Bisko_CC_Menu"><a>Currencies</a></li>');
  179. const $ul = $('<ul></ul>').appendTo($li);
  180. const preferred = preferredCurrency();
  181.  
  182. GM_addStyle(`
  183. .Bisko_CC_Menu ul {
  184. display: none;
  185. position: absolute;
  186. padding: 0;
  187. background-color: #272727;
  188. z-index: 9999;
  189. }
  190. .Bisko_CC_Menu:hover ul { display: block; }
  191. .Bisko_CC_Menu li { padding: 2px 10px; list-style-type: none; cursor: pointer; }
  192. .Bisko_CC_Menu li:hover, .preferred { background-color: SandyBrown; }
  193. `);
  194.  
  195. Object.keys(currencies).forEach((currency) => {
  196. const itemName = `${currency} ${currencies[currency].nameZH}`;
  197.  
  198. $ul.append(
  199. $(`<li class="${currency}">${itemName}</li>`)
  200. .addClass(() => (preferred === currency ? 'preferred' : ''))
  201. .click(() => {
  202. config.preferredCurrency = currency;
  203.  
  204. GM_setValue('Bisko_CC', JSON.stringify(config));
  205. updateCurrency(currency);
  206.  
  207. $('.Bisko_CC_Menu .preferred').removeClass('preferred');
  208. $(`.Bisko_CC_Menu .${currency}`).addClass('preferred');
  209. }),
  210. );
  211. });
  212.  
  213. return $li;
  214. };
  215. const handler = () => {
  216. switch (location.host) {
  217. case 'yuplay.ru':
  218. originalCurrency = 'RUB';
  219.  
  220. GM_addStyle(`
  221. .games-pricedown span.CCPrice { font-size: 18px; }
  222. .good-title span.CCPrice { margin-right: 3px; font-size: 22px; }
  223. `);
  224.  
  225. $('.header-right').append(
  226. constructMenu(),
  227. );
  228. // homepage games-pricedown
  229. $('.games-pricedown .sale > s, .games-pricedown .price').bindPriceData();
  230. // homepage game box & product page
  231. $('.games-box .price, .good-title .price')
  232. .contents()
  233. .each((index, node) => {
  234. const $node = $(node);
  235. const text = $node.text().trim();
  236.  
  237. if (node.tagName === 'S') $node.bindPriceData(); // retail price
  238. if (node.tagName === 'SPAN') $node.remove(); // currency
  239. else if (node.nodeType === 3 && text.length > 0) { // sales price
  240. $node.replaceWith(
  241. $(`<span>${text}<span>`).bindPriceData(),
  242. );
  243. }
  244. });
  245. break;
  246. case 'gama-gama.ru':
  247. originalCurrency = 'RUB';
  248.  
  249. GM_addStyle(`
  250. .Bisko_CC_Menu > a, .Bisko_CC_Menu li { color: white; }
  251. .Bisko_CC_Menu ul { margin: 0; }
  252. `);
  253.  
  254. $('#top_back').append(
  255. constructMenu()
  256. .wrapInner('<div class="Bisko_CC_Menu top_menu"></div>')
  257. .children()
  258. .unwrap(),
  259. );
  260. // homepage
  261. $('.price_1, .old_price, .promo_price').bindPriceData();
  262. // product page
  263. $('.card-info-oldprice > span, .card-info-price > span').bindPriceData();
  264. $('.card-info-oldprice, .card-info-price')
  265. .contents()
  266. .filter((i, node) => node.nodeType !== 1)
  267. .remove();
  268. break;
  269. case 'ru.gamersgate.com':
  270. case 'cn.gamersgate.com':
  271. if (location.host.startsWith('ru')) originalCurrency = 'RUB';
  272. if (location.host.startsWith('cn')) originalCurrency = 'CNY';
  273.  
  274. GM_addStyle(`
  275. .Bisko_CC_Menu { width: 49px; text-align: center; }
  276. .Bisko_CC_Menu svg {
  277. width: 36px;
  278. height: 36px;
  279. background-color: #093745;
  280. border: 1px solid #2c7c92;
  281. }
  282. .Bisko_CC_Menu, .Bisko_CC_Menu li { background-image: none !important; color: white; }
  283. .Bisko_CC_Menu li {
  284. height: initial !important;
  285. float: none !important;
  286. padding: 5px 10px !important;
  287. }
  288. .Bisko_CC_Menu li:hover, .preferred { background-color: SandyBrown !important; }
  289. `);
  290.  
  291. $('.btn_menuseparator').replaceWith(
  292. constructMenu(),
  293. );
  294. $('.Bisko_CC_Menu a').replaceWith(`
  295. <svg viewBox="0 0 24 24">
  296. <path fill="#a9ebea" d="M11.8,10.9C9.53,10.31 8.8,9.7 8.8,8.75C8.8,7.66 9.81,6.9 11.5,6.9C13.28,6.9 13.94,7.75 14,9H16.21C16.14,7.28 15.09,5.7 13,5.19V3H10V5.16C8.06,5.58 6.5,6.84 6.5,8.77C6.5,11.08 8.41,12.23 11.2,12.9C13.7,13.5 14.2,14.38 14.2,15.31C14.2,16 13.71,17.1 11.5,17.1C9.44,17.1 8.63,16.18 8.5,15H6.32C6.44,17.19 8.08,18.42 10,18.83V21H13V18.85C14.95,18.5 16.5,17.35 16.5,15.3C16.5,12.46 14.07,11.5 11.8,10.9Z" />
  297. </svg>
  298. `);
  299. // homepage & product page
  300. $(`
  301. .prtag > span,
  302. .grid-old-price,
  303. .price_price > span,
  304. div > span > .bold.white,
  305. li > .f_right:nth-child(2) > span
  306. `).bindPriceData();
  307. break;
  308. case 'www.greenmangaming.com':
  309. GM_addStyle(`
  310. .Bisko_CC_Menu > a {
  311. margin: 0 !important;
  312. padding: 6px 15px !important;
  313. font-size: 14px;
  314. }
  315. .Bisko_CC_Menu > a:hover, .Bisko_CC_Menu li:hover, .preferred { background-color: #494a4f !important; }
  316. `);
  317.  
  318. $('.megamenu').append(
  319. constructMenu(),
  320. );
  321.  
  322. try {
  323. // product page
  324. originalCurrency = games.Currency;
  325. $('price > span').bindPriceData();
  326. } catch (e) {
  327. // home page & search page
  328. const currencyObserver = new MutationObserver((mutations) => {
  329. mutations.forEach((mutation) => {
  330. mutation.addedNodes.forEach((addedNode) => {
  331. if (addedNode.tagName === 'SCRIPT' && addedNode.src.includes('bam.nr-data.net')) {
  332. const currency = decodeURI(addedNode.src).split('currency_code":"').pop().slice(0, 3);
  333.  
  334. if (currency.length > 0) {
  335. originalCurrency = currency;
  336.  
  337. const $prices = $('.prices > span, .prices > p, .listing-price > span');
  338. // offset decimal place in Europe (. ,)
  339. if (['EUR', 'RUB'].includes(currency)) $prices.text((i, text) => text.replace(',', '.'));
  340.  
  341. $prices.bindPriceData();
  342. updateCurrency();
  343. }
  344.  
  345. currencyObserver.disconnect();
  346. }
  347. });
  348. });
  349. });
  350.  
  351. currencyObserver.observe(document.head, { childList: true });
  352.  
  353. // search listing
  354. const $listing = $('.table-search-listings');
  355.  
  356. if ($listing.length > 0) {
  357. new MutationObserver((mutations) => {
  358. mutations.forEach((mutation) => {
  359. mutation.addedNodes.forEach((addedNode) => {
  360. $(addedNode).find('.listing-price > span').bindPriceData();
  361. updateCurrency();
  362. });
  363. });
  364. }).observe($listing[0], { childList: true });
  365. }
  366. }
  367. break;
  368. case 'uk.gamesplanet.com':
  369. case 'de.gamesplanet.com':
  370. case 'fr.gamesplanet.com': {
  371. originalCurrency = !location.host.startsWith('uk') ? 'EUR' : 'GBP';
  372.  
  373. const GPHandler = () => {
  374. const $prices = $('.price_base > strike, .price_current');
  375.  
  376. $('.container > ul').append(
  377. constructMenu(),
  378. );
  379. // offset decimal place in Europe (. ,)
  380. if (!location.host.startsWith('uk')) {
  381. $prices.text((i, text) => text.replace(',', '.'));
  382. }
  383.  
  384. $prices.bindPriceData();
  385. updateCurrency();
  386. };
  387.  
  388. GPHandler(true);
  389.  
  390. new MutationObserver((mutations) => {
  391. mutations.forEach((mutation) => {
  392. mutation.removedNodes.forEach((removedNode) => {
  393. if (removedNode.id === 'nprogress') GPHandler();
  394. });
  395. });
  396. }).observe(document, {
  397. childList: true,
  398. subtree: true,
  399. });
  400. break;
  401. }
  402. case 'www.cdkeys.com': {
  403. originalCurrency = $('.currency-switcher:first-child .value').text();
  404. const currenciesDefault = ['AUD', 'CAD', 'EUR', 'GBP', 'JPY', 'NZD', 'USD'];
  405. const currenciesAppend = ['CNY', 'HKD', 'KRW', 'MYR', 'NTD', 'RUB'];
  406. // bind event to default currency switcher to save preferrred currency
  407. $('.currency-switcher:first-child ul span').each((index, element) => {
  408. const $ele = $(element);
  409. const currency = $ele.text().slice(0, 3);
  410.  
  411. if (currenciesDefault.includes(currency)) {
  412. $ele.parent().click(() => {
  413. config.preferredCurrency = currency;
  414.  
  415. GM_setValue('Bisko_CC', JSON.stringify(config));
  416. });
  417. }
  418. });
  419. // append currencies not in the default currency switcher
  420. currenciesAppend.forEach((currency) => {
  421. $('.currency-switcher:first-child ul').append(
  422. $(`<li><a>${currency} - ${currencies[currency].nameEN}</a></li>`).click(() => {
  423. config.preferredCurrency = currency;
  424.  
  425. GM_setValue('Bisko_CC', JSON.stringify(config));
  426. updateCurrency(currency);
  427. }),
  428. );
  429. });
  430.  
  431. $('.price').bindPriceData();
  432. break;
  433. }
  434. case 'directg.net':
  435. originalCurrency = 'KRW';
  436.  
  437. GM_addStyle(`
  438. .Bisko_CC_Menu ul {
  439. width: 180px;
  440. border-top: 3px solid #67c1f5;
  441. background-color: #1b2838 !important;
  442. opacity: 0.95;
  443. color: rgba(255,255,255,0.5) !important;
  444. }
  445. .Bisko_CC_Menu ul li { padding: 10px 30px; font-weight: bold; }
  446. .Bisko_CC_Menu ul li:hover, .preferred { background-color: initial !important; color: white; }
  447. `);
  448.  
  449. $('.nav').append(
  450. constructMenu(),
  451. );
  452. $('span.PricebasePrice, span.PricesalesPrice')
  453. .bindPriceData()
  454. .next('span[itemprop="priceCurrency"]')
  455. .remove();
  456. break;/*
  457. case 'www.origin.com': {
  458. const region = location.pathname.split('/')[1];
  459. const currencyRegion = {
  460. twn: 'NTD',
  461. jpn: 'JPY',
  462. rus: 'RUB',
  463. usa: 'USD',
  464. nzl: 'NZD',
  465. irl: 'EUR',
  466. kor: 'KRW',
  467. };
  468. }*/
  469. case 'www.humblebundle.com':
  470. originalCurrency = 'USD';
  471.  
  472. GM_addStyle(`
  473. .Bisko_CC_Menu { float: left; }
  474. .Bisko_CC_Menu > a { display: block; padding: 15px 20px; }
  475. .Bisko_CC_Menu ul { background-color: #494f5c !important; color: rgba(255, 255, 255, 0.6); }
  476. .Bisko_CC_Menu ul li { padding: 5px 10px; }
  477. .Bisko_CC_Menu ul li:hover, .preferred { background-color: initial !important; color: white; }
  478. `);
  479.  
  480. $('.nav:first-child').append(
  481. constructMenu(),
  482. );
  483.  
  484. new MutationObserver((mutations) => {
  485. mutations.forEach((mutation) => {
  486. mutation.removedNodes.forEach((removedNode) => {
  487. if (removedNode.data === 'The Humble Store: Loading') {
  488. $('.price, .store-price, .full-price, .current-price').bindPriceData();
  489. updateCurrency();
  490. }
  491. });
  492. });
  493. }).observe(document.head, {
  494. childList: true,
  495. subtree: true,
  496. });
  497. break;
  498. case 'www.indiegala.com':
  499. originalCurrency = 'USD';
  500.  
  501. GM_addStyle(`
  502. .Bisko_CC_Menu ul {
  503. transform: translateY(-100%);
  504. background-color: #474747 !important;
  505. border: 1px solid #272727;
  506. }
  507. .Bisko_CC_Menu ul li { padding: 8px 15px; color: #dad6ca; }
  508. .Bisko_CC_Menu ul li:hover, .preferred { background-color: #2E2E2E !important; color: white; }
  509. `);
  510.  
  511. $('#libdContainer > ul:not(:first-child)').append(
  512. constructMenu(),
  513. );
  514. $('.Bisko_CC_Menu > a').addClass('libd-group-item libd-bounce');
  515. // homepage & product page
  516. $('.inner-info, .inner, .price-container')
  517. .contents()
  518. .each((index, node) => {
  519. const $node = $(node);
  520. const text = $node.text().trim();
  521.  
  522. $node.parent().parent().css('overflow', 'hidden');
  523. if (node.nodeType === 1 && node.tagName !== 'BR') $node.bindPriceData();
  524. else if (node.nodeType === 3 && text.length) {
  525. $node.replaceWith(
  526. $(`<a>${text}</a>`).bindPriceData(),
  527. );
  528. }
  529. });
  530. // search result
  531. $('.price-cont').bindPriceData();
  532. break;
  533. case 'www.bundlestars.com':
  534. originalCurrency = 'USD';
  535.  
  536. GM_addStyle(`
  537. .Bisko_CC_Menu ul { background-color: #212121 !important; }
  538. .Bisko_CC_Menu ul li { padding: 8px 15px; }
  539. .Bisko_CC_Menu ul li:hover, .preferred { background-color: #1A1A1A !important; }
  540. `);
  541.  
  542. $('.nav').eq(0).append(
  543. constructMenu(),
  544. );
  545. new MutationObserver((mutations) => {
  546. mutations.forEach((mutation) => {
  547. mutation.removedNodes.forEach((removedNode) => {
  548. if (removedNode.id === 'loading-bar-spinner') {
  549. $('.bs-price, .bs-currency-discount > span, .bs-currency-price').bindPriceData();
  550. $('.bs-card-meta > .bs-pricing, .bs-card-meta > .bs-currency-discount').css({
  551. position: 'absolute',
  552. right: '19px',
  553. });
  554. updateCurrency();
  555. }
  556. });
  557. });
  558. }).observe(document.body, { childList: true });
  559. break;
  560. case 'www.opiumpulses.com':
  561. originalCurrency = 'USD';
  562.  
  563. GM_addStyle(`
  564. .Bisko_CC_Menu {
  565. float: right;
  566. margin-top: 5px;
  567. padding: 1px 5px;
  568. vertical-align: middle;
  569. background-image: linear-gradient(#4E4E4E, #101112 40%, #191b1d);
  570. font-size: 12px;
  571. line-height: 1.5;
  572. border-radius: 3px;
  573. border-bottom-right-radius: 0;
  574. border-top-right-radius: 0;
  575. }
  576. .Bisko_CC_Menu > a { color: #f89406; }
  577. `);
  578.  
  579. $('.top-bar > .container > form').after(
  580. constructMenu()
  581. .wrapInner('<div class="Bisko_CC_Menu top_menu"></div>')
  582. .children()
  583. .unwrap(),
  584. );
  585. // homepage
  586. $('.album__container p > span:first-of-type').bindPriceData();
  587. // store page
  588. $('.product-box s, .product-box .text-danger').bindPriceData();
  589. new MutationObserver((mutations) => {
  590. mutations.forEach((mutation) => {
  591. mutation.addedNodes.forEach((addedNode) => {
  592. if (addedNode.id === 'productlistview') {
  593. $(addedNode).find('.product-box s, .product-box .text-danger').bindPriceData();
  594. updateCurrency();
  595. }
  596. });
  597. });
  598. }).observe(document.body, {
  599. childList: true,
  600. subtree: true,
  601. });
  602. break;
  603. default:
  604. }
  605. // only update prices when appended currencies are selected
  606. if (originalCurrency !== preferredCurrency()) updateCurrency();
  607. };
  608. const getExchangeRate = () => {
  609. GM_xmlhttpRequest({
  610. method: 'GET',
  611. url: 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml',
  612. onload: (res) => {
  613. if (res.status === 200) {
  614. try {
  615. config.exchangeRate = {
  616. lastUpdate: Date.now(),
  617. rates: {},
  618. };
  619.  
  620. res.response.split("\n").forEach((line) => {
  621. if (line.includes('currency=')) {
  622. const currency = line.split('currency=\'').pop().slice(0, 3);
  623. const rate = line.trim().split('rate=\'').pop().slice(0, -3);
  624.  
  625. config.exchangeRate.rates[currency] = parseFloat(rate);
  626. }
  627. });
  628. config.exchangeRate.rates.EUR = 1;
  629.  
  630. // get NTD
  631. GM_xmlhttpRequest({
  632. method: 'GET',
  633. url: 'https://www.google.com/search?q=1+EUR+%3D+NTD',
  634. onload: (searchRes) => {
  635. const rate = parseFloat(searchRes.response.split('<div class="vk_ans vk_bk">').pop().slice(0, 7).trim());
  636. const NTDRate = isNaN(rate) ? config.exchangeRate.rates.HKD * 3.75 : rate;
  637.  
  638. config.exchangeRate.rates.NTD = NTDRate;
  639. GM_setValue('Bisko_CC', JSON.stringify(config));
  640. },
  641. onerror: () => {
  642. config.exchangeRate.rates.NTD = config.exchangeRate.rates.HKD * 3.75;
  643. },
  644. });
  645.  
  646. handler();
  647. } catch (e) {
  648. swal(
  649. 'Parsing Failed',
  650. 'An error occured when parsing exchange rate data, please reload to try again',
  651. 'error',
  652. );
  653. }
  654. } else {
  655. swal(
  656. 'Loading Failed',
  657. 'Unable to fetch exchange rate data, please reload to try again',
  658. 'error',
  659. );
  660. }
  661. },
  662. });
  663. };
  664.  
  665. $(() => {
  666. if (Object.keys(config).length === 0) getExchangeRate(); // first installed
  667. else if (Date.now() - interval > config.lastUpdate) getExchangeRate(); // update exchange rate
  668. else handler();
  669. });