├── .gitignore ├── .eslintrc.json ├── package.json ├── partNumbers.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "no-console": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iphone-x-availability", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Carlos E Silva", 11 | "license": "ISC", 12 | "dependencies": { 13 | "command-line-args": "^4.0.7", 14 | "command-line-usage": "^4.0.1", 15 | "node-fetch": "^1.7.3" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^4.10.0", 19 | "eslint-config-airbnb-base": "^12.1.0", 20 | "eslint-plugin-import": "^2.8.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /partNumbers.json: -------------------------------------------------------------------------------- 1 | { 2 | "x": { 3 | "TMOBILE": { 4 | "gray": { 5 | "64": "MQAQ2LL/A", 6 | "256": "MQAU2LL/A" 7 | }, 8 | "silver": { 9 | "64": "MQAR2LL/A", 10 | "256": "MQAV2LL/A" 11 | } 12 | }, 13 | "ATT": { 14 | "gray": { 15 | "64": "MQAJ2LL/A", 16 | "256": "MQAM2LL/A" 17 | }, 18 | "silver": { 19 | "64": "MQAK2LL/A", 20 | "256": "MQAN2LL/A" 21 | } 22 | }, 23 | "SPRINT": { 24 | "gray": { 25 | "64": "MQCR2LL/A", 26 | "256": "MQCV2LL/A" 27 | }, 28 | "silver": { 29 | "64": "MQCT2LL/A", 30 | "256": "MQCW2LL/A" 31 | } 32 | }, 33 | "VERIZON": { 34 | "gray": { 35 | "64": "MQCK2LL/A", 36 | "256": "MQCN2LL/A" 37 | }, 38 | "silver": { 39 | "64": "MQCL2LL/A", 40 | "256": "MQCP2LL/A" 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iPhone X Availability Node CLI 2 | 3 | A node cli tool I put together to check the iPhone X in-store availability near me instead of having to refresh the apple website over and over again. 4 | 5 | **Warning**: This was very quickly put together so it is very rough and has a lot of room for improvement. It did end up being successful in letting me know there was some new stock near me a few hours after I built it so at the very least: it works! 6 | 7 | By the way this was only tested on a Mac, I can't guarantee it will work on other platforms. 8 | 9 | Feel free to submit a pull request with improvements or just simply fork it and modify it for your own needs. Soon there won't be a shortage of iPhones anymore so this tool will be irrelevant. 10 | 11 | ### What it does 12 | It continously makes requests to Apple's availability api. When it finds some new stock near you, it displays the stores' name and distance from your zipcode then exits the program. 13 | 14 | ### Requirements 15 | - Node v8+ 16 | 17 | ### How to use 18 | 19 | 1. Clone this repository `git clone https://github.com/carlosesilva/iphone-x-availability-node-cli.git` 20 | 1. Go into the directory `cd iphone-x-availability-node-cli` 21 | 1. Instal dependencies `npm install` 22 | 1. Run the program `node index.js --zip=12345` 23 | 24 | ### Examples 25 | 26 | Check availability for TMOBILE iPhone X 256gb in space gray every 30 seconds within 60 miles of the zip code 12345 (pretty much the default settings so we don't need to specify them here, the zip code is always required though) 27 | ``` 28 | node index.js --zip=012345 29 | ``` 30 | 31 | Check availability for ATT iPhone X 64gb in silver every minute within 30 miles of the zip code 02115 32 | ``` 33 | node index.js --carrier=ATT --color=silver --storage=64 --delay=60 --zip=02115 --distance=30 34 | ``` 35 | 36 | Use the --help flag for more information 37 | ``` 38 | node index.js --help 39 | ``` 40 | 41 | ### Options 42 | | option | accepted values | default | 43 | | -------- | --------------------------------------------- | ------------------------------------- | 44 | | carrier | `'ATT'`, `'SPRINT'`, `'TMOBILE'`, `'VERIZON'` | `'TMOBILE'` | 45 | | model | `'x'` | `'x'` | 46 | | color | `'silver'`, `'gray'` | `'gray'` | 47 | | storage | `64`, `256` | `256` | 48 | | delay | Integer number of seconds between requests | `30` | 49 | | distance | Integer number of miles from zipcode to look | `60` | 50 | | zip* | 5 digit US zip code | Not provided, you must enter your own | 51 | 52 | *\*required* 53 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Require npm packages. 2 | const fetch = require('node-fetch'); 3 | const commandLineArgs = require('command-line-args'); 4 | const getUsage = require('command-line-usage'); 5 | 6 | // Get partNumbers from json file. 7 | const partNumbers = require('./partNumbers.json'); 8 | 9 | // Define command line args accepted. 10 | const optionDefinitions = [ 11 | { 12 | name: 'carrier', 13 | type: String, 14 | defaultValue: 'TMOBILE', 15 | description: 16 | "Define which carrier to search for. Accepted options are: 'ATT', 'SPRING', 'TMOBILE', 'VERIZON'.", 17 | }, 18 | { 19 | name: 'model', 20 | type: String, 21 | defaultValue: 'x', 22 | description: 23 | "Define which model iPhone to search for. 'x' is the only option currently available.", 24 | }, 25 | { 26 | name: 'color', 27 | type: String, 28 | defaultValue: 'gray', 29 | description: 30 | "Define which color iPhone to search for. Accepted options are: 'silver', 'gray'.", 31 | }, 32 | { 33 | name: 'storage', 34 | type: String, 35 | defaultValue: 256, 36 | description: "Define which storage size to search for. Accepted options are: '64', '256'.", 37 | }, 38 | { 39 | name: 'zip', 40 | type: String, 41 | defaultOption: true, 42 | description: 'Define the area to search in by zip code. This option is required.', 43 | }, 44 | { 45 | name: 'distance', 46 | type: Number, 47 | defaultValue: 60, 48 | description: 'Define the distance from the supplied zip code to look for iPhone.', 49 | }, 50 | { 51 | name: 'delay', 52 | type: Number, 53 | defaultValue: 30, 54 | description: 'Define the number of seconds between requests.', 55 | }, 56 | { name: 'help', type: Boolean, description: 'Display this help screen.' }, 57 | ]; 58 | 59 | // Parse command line args. 60 | const options = commandLineArgs(optionDefinitions); 61 | 62 | // Define the help screen to be displayed if --help is present in options 63 | const usageDefinition = [ 64 | { 65 | header: 'iPhone X Availability Node CLI', 66 | content: 67 | "The app continously makes requests to Apple's availability api. When it finds some new stock near you, it displays the stores' name and distance from your zipcode then exits the program.", 68 | }, 69 | { 70 | header: 'Synopsis', 71 | content: [ 72 | { 73 | desc: 'Default arguments.', 74 | example: 75 | '$ node index.js [bold]{--carrier} TMOBILE [bold]{--model} x [bold]{--color} gray [bold]{--storage} 256 [bold]{--delay} 30 [bold]{--distance} 60', 76 | }, 77 | { 78 | desc: 'Simple example', 79 | example: '$ node index.js [bold]{--zip} 10001 [bold]{--color} silver', 80 | }, 81 | { 82 | desc: 'Help screen.', 83 | example: '$ node index.js [bold]{--help}', 84 | }, 85 | ], 86 | }, 87 | { 88 | header: 'Options', 89 | optionList: optionDefinitions, 90 | }, 91 | ]; 92 | 93 | // if --help is present or --zip wasn't defined, 94 | // then display the help screen and exit the program. 95 | if (options.help || options.zip === undefined) { 96 | console.log(getUsage(usageDefinition)); 97 | process.exit(); 98 | } 99 | 100 | // Get part number for the specified device. 101 | const partNumber = 102 | partNumbers[options.model][options.carrier.toUpperCase()][options.color][options.storage]; 103 | 104 | // Construct the endpoint url with the options selected. 105 | const endpoint = `https://www.apple.com/shop/retail/pickup-message?pl=true&cppart=${options.carrier}/US&parts.0=${partNumber}&location=${options.zip}`; 106 | 107 | // Keep track of the last request time. 108 | let lastRequestTimestamp = null; 109 | 110 | /** 111 | * Update program status display 112 | * 113 | * @param {String} str The string that will be outputed. 114 | */ 115 | function updateStatus() { 116 | // If lastRequestTimestamp hasn't been update yet, do nothing. 117 | if (lastRequestTimestamp === null) { 118 | return; 119 | } 120 | 121 | // Get the amount of time elapsed since last request. 122 | const timeDelta = Date.now() - lastRequestTimestamp; 123 | const timeInSeconds = Math.floor(timeDelta / 1000); 124 | process.stdout.write(`Status: Device not available. Last request made ${timeInSeconds} seconds ago\r`); 125 | } 126 | 127 | /** 128 | * Parse the returned data and find stores where the device is available 129 | * 130 | * @param {Object} data The api response. 131 | * @return {Array} The array of stores where the devices is available. 132 | */ 133 | function processResponse(data) { 134 | // Destructure the stores object out of the body. 135 | const { stores } = data.body; 136 | 137 | // Filter out stores that do not have the device available. 138 | const storesAvailable = stores.filter((store) => { 139 | // Check if store is within distance. 140 | if (store.storedistance < options.distance) { 141 | // Select the specified device partNumber. 142 | const part = store.partsAvailability[partNumber]; 143 | // Check that the pickupDisplay property says 'available'. 144 | const availability = part.pickupDisplay === 'available'; 145 | // Return true if the device is available or else false. 146 | return availability; 147 | } 148 | // Store wasn't within distance so return false. 149 | return false; 150 | }); 151 | 152 | // Return an array of stores where the device is available. 153 | return storesAvailable; 154 | } 155 | 156 | /** 157 | * Make a request to the endpoint and get list of stores available 158 | * 159 | * @return {Promise} A promise that should resolve to an array of stores available. 160 | */ 161 | function getStoresAvailable() { 162 | // Update lastRequestTimestamp. 163 | lastRequestTimestamp = Date.now(); 164 | 165 | return fetch(endpoint) 166 | .then(stream => stream.json()) 167 | .catch(error => process.stderr.write('Fetch Error :-S', error)) 168 | .then(data => processResponse(data)); 169 | } 170 | 171 | /** 172 | * Output list of stores where the device is avaliable. 173 | * 174 | * @param {Array} storesAvailable The array of stores where the device is avaliable. 175 | */ 176 | function displayStoresAvailable(storesAvailable) { 177 | // Construct the output string by reducing the storesAvailable array into a string. 178 | const storesAvailableStr = storesAvailable.reduce( 179 | (result, store) => 180 | `${result}\n${store.address.address} which is ${store.storeDistanceWithUnit} away`, 181 | '', 182 | ); 183 | 184 | // Output bell sound. 185 | console.log('\u0007'); 186 | 187 | // Output the message. 188 | console.log(`The device is currently available at ${storesAvailable.length} stores near you:`); 189 | console.log(storesAvailableStr); 190 | } 191 | 192 | /** 193 | * The main program loop 194 | * 195 | * Continuously check for the device availability until it is available somewhere. 196 | */ 197 | async function requestLoop() { 198 | // Fetch the storesAvailable array. 199 | const storesAvailable = await getStoresAvailable(); 200 | 201 | if (storesAvailable.length === 0) { 202 | // If the array is empty, update the status and after the 203 | // specified options.delay amount of seconds, try again. 204 | setTimeout(() => { 205 | requestLoop(); 206 | }, options.delay * 1000); 207 | } else { 208 | // The device is available. Show that information to the user and exit the program. 209 | displayStoresAvailable(storesAvailable); 210 | process.exit(); 211 | } 212 | } 213 | 214 | // Display program started message. 215 | console.log('Starting program with the following settings:'); 216 | console.log(`${JSON.stringify(options, null, 2)}`); 217 | 218 | // Update the display every second. 219 | setInterval(() => { 220 | updateStatus(); 221 | }, 1000); 222 | 223 | // Kick off request recursion. 224 | requestLoop(); 225 | --------------------------------------------------------------------------------