├── bot ├── src │ ├── app │ │ ├── version.js │ │ ├── actions │ │ │ ├── drawer.js │ │ │ ├── notification.js │ │ │ ├── menu.js │ │ │ ├── atc.js │ │ │ └── profiles.js │ │ ├── constants │ │ │ ├── Menus.js │ │ │ ├── ActionTypes.js │ │ │ ├── Styles.js │ │ │ └── Utils.js │ │ ├── reducers │ │ │ ├── index.js │ │ │ ├── drawer.js │ │ │ ├── notification.js │ │ │ ├── menu.js │ │ │ ├── atc.js │ │ │ └── profiles.js │ │ ├── components │ │ │ ├── FileInput.jsx │ │ │ ├── shops │ │ │ │ └── supreme │ │ │ │ │ ├── pages │ │ │ │ │ ├── DropProducts.jsx │ │ │ │ │ ├── Products.jsx │ │ │ │ │ ├── Drops.jsx │ │ │ │ │ ├── Restocks.jsx │ │ │ │ │ ├── Billing.jsx │ │ │ │ │ ├── Sizes.jsx │ │ │ │ │ ├── Atc.jsx │ │ │ │ │ └── Options.jsx │ │ │ │ │ ├── LocalChangeSelect.jsx │ │ │ │ │ ├── Configuration.jsx │ │ │ │ │ ├── ProductList.jsx │ │ │ │ │ └── AtcCreateForm.jsx │ │ │ ├── NotificationBar.jsx │ │ │ ├── ProfileToggle.jsx │ │ │ ├── ProfileImportForm.jsx │ │ │ ├── ProfileExportForm.jsx │ │ │ ├── ProfileCreateForm.jsx │ │ │ └── Profile.jsx │ │ ├── index.hbs │ │ ├── migrations.js │ │ ├── utils │ │ │ ├── Helpers.js │ │ │ ├── SupremeUtils.js │ │ │ ├── FuzzyStringMatcher.js │ │ │ └── FormValidators.js │ │ ├── routes.jsx │ │ ├── containers │ │ │ ├── App.jsx │ │ │ ├── Layout.jsx │ │ │ └── AppDrawer.jsx │ │ └── index.jsx │ ├── assets │ │ ├── img │ │ │ ├── icon.png │ │ │ ├── icon128.png │ │ │ ├── icon16.png │ │ │ ├── icon32.png │ │ │ └── icon48.png │ │ ├── fonts │ │ │ ├── roboto-v15-latin-300.eot │ │ │ ├── roboto-v15-latin-300.woff │ │ │ ├── roboto-v15-latin-500.eot │ │ │ ├── roboto-v15-latin-500.woff │ │ │ ├── roboto-v15-latin-300.woff2 │ │ │ ├── roboto-v15-latin-500.woff2 │ │ │ ├── roboto-v15-latin-regular.eot │ │ │ ├── roboto-v15-latin-regular.woff │ │ │ └── roboto-v15-latin-regular.woff2 │ │ └── css │ │ │ └── main.css │ ├── extension │ │ ├── content │ │ │ ├── supreme │ │ │ │ ├── processors │ │ │ │ │ ├── baseProcessor.js │ │ │ │ │ ├── checkoutProcessor.js │ │ │ │ │ ├── cartProcessor.js │ │ │ │ │ ├── atcProcessor.js │ │ │ │ │ └── productProcessor.js │ │ │ │ ├── index.js │ │ │ │ ├── notification.js │ │ │ │ ├── SupremeManager.js │ │ │ │ └── helpers.js │ │ │ └── index.js │ │ └── background │ │ │ ├── supreme │ │ │ ├── RestockMonitor.js │ │ │ └── index.js │ │ │ └── index.js │ ├── services │ │ ├── CryptoService.js │ │ ├── KeywordsService.js │ │ ├── supreme │ │ │ ├── RestocksService.js │ │ │ ├── DropsService.js │ │ │ ├── ProductsService.js │ │ │ ├── AtcService.js │ │ │ └── CheckoutService.js │ │ ├── ChromeService.js │ │ └── StorageService.js │ └── preload │ │ └── index.js ├── .flowconfig ├── .babelrc ├── .eslintrc.js ├── .editorconfig ├── manifest.json ├── package.json ├── webpack.config.js ├── webpack.config.prod.js └── test │ └── keywordmatcher.test.js ├── atc.gif ├── .gitignore ├── screenshot.jpg └── README.md /bot/src/app/version.js: -------------------------------------------------------------------------------- 1 | export default '2.10.1'; 2 | -------------------------------------------------------------------------------- /atc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/atc.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | **/node_modules 3 | .vscode 4 | .expo 5 | bot/build 6 | bot/build.zip -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /bot/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /bot/src/assets/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/img/icon.png -------------------------------------------------------------------------------- /bot/src/assets/img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/img/icon128.png -------------------------------------------------------------------------------- /bot/src/assets/img/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/img/icon16.png -------------------------------------------------------------------------------- /bot/src/assets/img/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/img/icon32.png -------------------------------------------------------------------------------- /bot/src/assets/img/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/img/icon48.png -------------------------------------------------------------------------------- /bot/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["flow", "react", "es2015", "es2017"], 3 | "plugins": ["transform-object-rest-spread", "transform-runtime"], 4 | } 5 | -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-300.eot -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-300.woff -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-500.eot -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-500.woff -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-300.woff2 -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-500.woff2 -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-regular.eot -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-regular.woff -------------------------------------------------------------------------------- /bot/src/assets/fonts/roboto-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YungVDev/Supreme-Auto-Checkout/HEAD/bot/src/assets/fonts/roboto-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /bot/src/app/actions/drawer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export function setDrawerOpen(open) { 4 | return { 5 | type: types.DRAWER_SET_OPEN, 6 | open 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /bot/src/app/actions/notification.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export default function addNotification(message) { 4 | return { 5 | type: types.NOTIFICATION_ADD, 6 | message, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /bot/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react", 5 | "jsx-a11y", 6 | "import" 7 | ], 8 | "env": { 9 | "browser": true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /bot/src/app/constants/Menus.js: -------------------------------------------------------------------------------- 1 | export const MENU_OPTIONS = 'Options'; 2 | export const MENU_BILLING = 'Billing'; 3 | export const MENU_SIZES = 'Sizes'; 4 | export const MENU_ATC = 'AutoCop'; 5 | export const MENU_PRODUCTS = 'Products'; 6 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/processors/baseProcessor.js: -------------------------------------------------------------------------------- 1 | export default class BaseProcessor { 2 | constructor(preferences, sizings, billing) { 3 | this.preferences = preferences; 4 | this.sizings = sizings; 5 | this.billing = billing; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /bot/src/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | export { default as menu } from './menu'; 2 | export { default as notification } from './notification'; 3 | export { default as profiles } from './profiles'; 4 | export { default as atc } from './atc'; 5 | export { default as drawer } from './drawer'; 6 | -------------------------------------------------------------------------------- /bot/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = null 12 | -------------------------------------------------------------------------------- /bot/src/app/reducers/drawer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export default function drawer(state = { 4 | open: true, 5 | }, action) { 6 | switch (action.type) { 7 | case types.DRAWER_SET_OPEN: 8 | return { open: action.open }; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bot/src/app/reducers/notification.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export default function notification(state = { 4 | message: null, 5 | }, action) { 6 | switch (action.type) { 7 | case types.NOTIFICATION_ADD: 8 | return { message: action.message }; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bot/src/services/CryptoService.js: -------------------------------------------------------------------------------- 1 | import CryptoJs from 'crypto-js'; 2 | 3 | export default class CryptoService { 4 | static encrypt(message, passphrase) { 5 | return CryptoJs.AES.encrypt(message, passphrase); 6 | } 7 | 8 | static decrypt(message, password) { 9 | return CryptoJs.AES.decrypt(message, password).toString(CryptoJs.enc.Utf8); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bot/src/extension/content/index.js: -------------------------------------------------------------------------------- 1 | import SupremeExtension from './supreme/index'; 2 | import StorageService from '../../services/StorageService'; 3 | 4 | 5 | async function start() { 6 | const profile = await StorageService.getCurrentProfileSettings(); 7 | const supremeExtension = new SupremeExtension(); 8 | supremeExtension.start(profile); 9 | } 10 | 11 | start(); 12 | -------------------------------------------------------------------------------- /bot/src/app/reducers/menu.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export default function menu(state = { 4 | currentMenu: null, 5 | }, action) { 6 | switch (action.type) { 7 | case types.CHANGE_MENU: 8 | return Object.assign({}, state, { 9 | currentMenu: action.menu, 10 | }); 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bot/src/app/actions/menu.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import * as menus from '../constants/Menus'; 3 | 4 | export function changeMenu(newMenu) { 5 | const keys = Object.keys(menus).map(x => menus[x]); 6 | if (keys.indexOf(newMenu) !== -1) { 7 | return { type: types.CHANGE_MENU, menu: newMenu }; 8 | } 9 | throw new Error(`Invalid menu : ${newMenu}`); 10 | } 11 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/index.js: -------------------------------------------------------------------------------- 1 | import SupremeManager from './SupremeManager'; 2 | import { notify } from './notification'; 3 | 4 | export default class SupremeExtension { 5 | start(profile) { 6 | if (!profile || !profile.Supreme) { 7 | notify('No profile configured', true); 8 | return; 9 | } 10 | const settings = profile.Supreme; 11 | const manager = new SupremeManager(settings.Options, settings.Sizes, settings.Billing); 12 | manager.start(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bot/src/preload/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function injectScript() { 4 | const script = document.createElement('script'); 5 | script.setAttribute('type', 'text/javascript'); 6 | script.innerText = ` 7 | window.document.mockedQuerySelector = document.querySelector; 8 | window.document.mockedQuerySelectorAll = document.querySelectorAll; 9 | `; 10 | document.body.appendChild(script); 11 | } 12 | 13 | document.addEventListener('DOMContentLoaded', function(event) { 14 | injectScript(); 15 | }); 16 | -------------------------------------------------------------------------------- /bot/src/app/components/FileInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class FileInput extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.onChange = this.onChange.bind(this); 7 | } 8 | 9 | onChange(e) { 10 | const { input: { onChange } } = this.props; 11 | onChange(e.target.files[0]); 12 | } 13 | 14 | render() { 15 | return (); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bot/src/app/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_MENU = 'CHANGE_MENU'; 2 | export const NOTIFICATION_ADD = 'NOTIFICATION_ADD'; 3 | export const PROFILE_CREATE = 'PROFILE_CREATE'; 4 | export const PROFILE_UPDATE_SETTINGS = 'PROFILE_UPDATE_SETTINGS'; 5 | export const PROFILE_REMOVE = 'PROFILE_REMOVE'; 6 | export const PROFILE_SET_ENABLED = 'PROFILE_SET_ENABLED'; 7 | export const ATC_PRODUCT_ADD = 'ATC_PRODUCT_ADD'; 8 | export const ATC_PRODUCT_REMOVE = 'ATC_PRODUCT_REMOVE'; 9 | export const ATC_PRODUCT_EDIT = 'ATC_PRODUCT_EDIT'; 10 | export const DRAWER_SET_OPEN = 'DRAWER_SET_OPEN'; 11 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/DropProducts.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import DropsService from '../../../../../services/supreme/DropsService'; 3 | import ProductList from '../ProductList'; 4 | 5 | export default class DropProducts extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | products: null, 10 | }; 11 | 12 | DropsService.fetchProducts(props.params.slug).then(products => this.setState({ products })); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bot/src/services/KeywordsService.js: -------------------------------------------------------------------------------- 1 | import FuzzyStringMatcher from '../app/utils/FuzzyStringMatcher'; 2 | 3 | export default class KeywordsService { 4 | static findBestMatch(products, keywords, category) { 5 | return KeywordsService.findMatches(products, keywords, category)[0]; 6 | } 7 | 8 | static findMatches(products, keywords, category) { 9 | const keys = Object.keys(products); 10 | const bestMatchingCategory = (new FuzzyStringMatcher(keys)).search(category)[0]; 11 | if (bestMatchingCategory === undefined) { 12 | return []; 13 | } 14 | const productsCategory = products[keys[bestMatchingCategory]]; 15 | const fuse = new FuzzyStringMatcher(productsCategory, { key: 'name' }); 16 | return fuse.search(keywords.join(' ')); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bot/src/app/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Supreme auto checkout 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /bot/src/services/supreme/RestocksService.js: -------------------------------------------------------------------------------- 1 | import request from 'browser-request'; 2 | import StorageService from '../StorageService'; 3 | 4 | export default class RestocksService { 5 | static async fetchCurrentStock() { 6 | const locale = await StorageService.getItem('locale') || 'eu'; 7 | return new Promise((resolve, reject) => { 8 | const url = locale === 'us' ? 'https://api.openaio.com/us/stock' : 'https://api.openaio.com/stock'; 9 | request({ url }, (error, response, body) => { 10 | if (!error && response.statusCode === 200) { 11 | try { 12 | resolve(JSON.parse(body)); 13 | } catch (e) { 14 | console.error(e); 15 | reject(e); 16 | } 17 | } else { 18 | reject({ error }); 19 | } 20 | }); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bot/src/app/components/NotificationBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Snackbar from 'material-ui/Snackbar'; 4 | 5 | function NotificationBar(props) { 6 | const { notification } = props; 7 | return ( 8 | 14 | ); 15 | } 16 | 17 | NotificationBar.propTypes = { 18 | notification: PropTypes.shape({ 19 | message: PropTypes.string, 20 | }), 21 | }; 22 | 23 | function mapStateToProps(state) { 24 | return { 25 | notification: state.notification, 26 | }; 27 | } 28 | 29 | export default connect(mapStateToProps)(NotificationBar); 30 | -------------------------------------------------------------------------------- /bot/src/app/migrations.js: -------------------------------------------------------------------------------- 1 | function migrateAtc(state) { 2 | const newState = Object.assign({}, state); 3 | if (newState.atc && newState.atc.atcProducts && newState.atc.atcProducts.length) { 4 | let id = 0; 5 | for (let i = 0; i < newState.atc.atcProducts.length; i += 1) { 6 | const product = newState.atc.atcProducts[i]; 7 | if (product.id === undefined || !product.product) { 8 | newState.atc.atcProducts[i] = { 9 | id: id += 1, 10 | product, 11 | }; 12 | } 13 | if (newState.atc.atcProducts[i].product.retryCount === undefined) { 14 | newState.atc.atcProducts[i].product.retryCount = 3; 15 | } 16 | 17 | if (newState.atc.atcProducts[i].product.soldOutAction === undefined) { 18 | newState.atc.atcProducts[i].product.soldOutAction = 'skip'; 19 | } 20 | } 21 | } 22 | return newState; 23 | } 24 | 25 | function migrate(state) { 26 | return migrateAtc(state); 27 | } 28 | 29 | export default migrate; 30 | -------------------------------------------------------------------------------- /bot/src/app/utils/Helpers.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export function isObjectEmpty(obj) { 4 | return Object.keys(obj).length === 0 && obj.constructor === Object; 5 | } 6 | 7 | export function timeToDate(time) { 8 | const d = moment(time, 'hh:mm:ss'); 9 | if (!d.isValid()) { 10 | return null; 11 | } 12 | return d.toDate(); 13 | } 14 | 15 | export function isValidTime(time) { 16 | return timeToDate(time) !== null; 17 | } 18 | 19 | export function sameDay(d1, d2) { 20 | return d1.getFullYear() === d2.getFullYear() && 21 | d1.getMonth() === d2.getMonth() && 22 | d1.getDate() === d2.getDate(); 23 | } 24 | 25 | export function slugify(text) { 26 | return text.toString().toLowerCase() 27 | .replace(/\s+/g, '-') // Replace spaces with - 28 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 29 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 30 | .replace(/^-+/, '') // Trim - from start of text 31 | .replace(/-+$/, ''); // Trim - from end of text 32 | } 33 | -------------------------------------------------------------------------------- /bot/src/app/actions/atc.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import addNotification from '../actions/notification'; 3 | 4 | export function addAtcProduct(data) { 5 | return function (dispatch) { 6 | dispatch(addNotification(`Atc product ${data.name} added`)); 7 | dispatch({ 8 | type: types.ATC_PRODUCT_ADD, 9 | name, 10 | data, 11 | }); 12 | }; 13 | } 14 | 15 | export function editAtcProduct(name, data) { 16 | return function (dispatch) { 17 | dispatch(addNotification(`Atc product ${name} edited`)); 18 | dispatch({ 19 | type: types.ATC_PRODUCT_EDIT, 20 | name, 21 | data, 22 | }); 23 | }; 24 | } 25 | 26 | export function removeAtcProduct(name) { 27 | return function (dispatch) { 28 | dispatch(addNotification(`Atc product ${name} removed`)); 29 | dispatch({ 30 | type: types.ATC_PRODUCT_REMOVE, 31 | name, 32 | }); 33 | }; 34 | } 35 | 36 | export function setAtcProductEnabled(name, enabled = true) { 37 | return { 38 | type: types.ATC_PRODUCT_EDIT, 39 | name, 40 | data: { enabled }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /bot/src/app/components/ProfileToggle.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Toggle from 'material-ui/Toggle'; 4 | import { setProfileEnabled } from '../actions/profiles'; 5 | 6 | class ProfileToggle extends Component { 7 | onSetProfile(profileName) { 8 | const { currentProfile } = this.props; 9 | if (currentProfile === profileName) { 10 | this.props.notify("Atleast 1 profile must be enabled"); 11 | return; 12 | } 13 | 14 | this.props.setProfileEnabled(profileName); 15 | } 16 | 17 | render() { 18 | const { profile, currentProfile } = this.props; 19 | return ( this.onSetProfile(profile.name)} />); 20 | } 21 | } 22 | 23 | function mapStateToProps(state) { 24 | return { 25 | currentProfile: state.profiles.currentProfile, 26 | }; 27 | } 28 | 29 | function mapDispatchToProps(dispatch) { 30 | return { 31 | setProfileEnabled: name => dispatch(setProfileEnabled(name)), 32 | }; 33 | } 34 | 35 | ProfileToggle.PropTypes = { 36 | profile: PropTypes.object.isRequired, 37 | }; 38 | 39 | export default connect(mapStateToProps, mapDispatchToProps)(ProfileToggle); 40 | -------------------------------------------------------------------------------- /bot/src/app/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router'; 3 | import App from './containers/App'; 4 | import Configuration from './components/shops/supreme/Configuration'; 5 | import Atc from './components/shops/supreme/pages/Atc'; 6 | import Products from './components/shops/supreme/pages/Products'; 7 | import Drops from './components/shops/supreme/pages/Drops'; 8 | import Restocks from './components/shops/supreme/pages/Restocks'; 9 | import DropProducts from './components/shops/supreme/pages/DropProducts'; 10 | import Profile from './components/Profile'; 11 | 12 | export default () => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/notification.js: -------------------------------------------------------------------------------- 1 | function createNotificationBar() { 2 | const notificationBar = document.createElement('div'); 3 | notificationBar.style.width = '100%'; 4 | notificationBar.style.marginLeft = 'auto'; 5 | notificationBar.style.marginRight = 'auto'; 6 | notificationBar.style.textAlign = 'center'; 7 | notificationBar.style.lineHeight = '60px'; 8 | notificationBar.style.height = '60px'; 9 | notificationBar.style.fontSize = '1.6em'; 10 | notificationBar.style.zIndex = '9999'; 11 | notificationBar.style.left = 0; 12 | notificationBar.style.top = 0; 13 | notificationBar.id = 'notification-bar'; 14 | document.body.prepend(notificationBar); 15 | return notificationBar; 16 | } 17 | 18 | export function getOrCreateNotificationBar() { 19 | if (!document.getElementById('notification-bar')) { 20 | createNotificationBar(); 21 | } 22 | return document.getElementById('notification-bar'); 23 | } 24 | 25 | export function notify(text, danger = false) { 26 | const notificationBar = getOrCreateNotificationBar(); 27 | if (danger) { 28 | notificationBar.style.backgroundColor = 'rgba(255, 58, 58, 0.42)'; 29 | } else { 30 | notificationBar.style.backgroundColor = 'rgba(8, 107, 185, 0.42)'; 31 | } 32 | notificationBar.textContent = text; 33 | } 34 | -------------------------------------------------------------------------------- /bot/src/services/ChromeService.js: -------------------------------------------------------------------------------- 1 | export default class ChromeService { 2 | static isPopup() { 3 | const urlParams = new URLSearchParams(window.location.search); 4 | return urlParams.get('popup') === 'true'; 5 | } 6 | 7 | static openOptionsPage(page = 'supreme') { 8 | window.open(chrome.runtime.getURL(`index.html#/${page}/`)); 9 | } 10 | 11 | static createNotification(title, message, onClick) { 12 | const notification = new Notification(title, { 13 | icon: 'assets/img/icon.png', 14 | body: message, 15 | }); 16 | if (onClick) { 17 | notification.onclick = () => onClick(notification); 18 | } 19 | setTimeout(() => { 20 | notification.close(); 21 | }, 6000); 22 | return notification; 23 | } 24 | 25 | static sendMessage(key, data) { 26 | return new Promise((resolve) => { 27 | chrome.runtime.sendMessage({ key, data }, resolve); 28 | }); 29 | } 30 | 31 | static addMessageBroadcastListener(func) { 32 | chrome.runtime.onMessage.addListener(func); 33 | } 34 | 35 | static addMessageListener(key, func) { 36 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 37 | if (request.key === key) { 38 | func(request, sender, sendResponse); 39 | } 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Supreme Auto-Checkout bot", 4 | "description": "Supreme Open-Source add to cart bot", 5 | "version": "2.10.1", 6 | "background": { 7 | "scripts": ["background.js"] 8 | }, 9 | "browser_action": { 10 | "default_popup": "index.html?popup=true", 11 | "default_icon": "assets/img/icon.png" 12 | }, 13 | "icons": { 14 | "16": "assets/img/icon16.png", 15 | "48": "assets/img/icon48.png", 16 | "128": "assets/img/icon128.png" 17 | }, 18 | "options_page": "index.html", 19 | "permissions": [ 20 | "activeTab", 21 | "contentSettings", 22 | "cookies", 23 | "storage", 24 | "webNavigation", 25 | "background", 26 | "notifications", 27 | "webRequest", 28 | "webRequestBlocking", 29 | "*://*.supremenewyork.com/*", 30 | "https://www.supremenewyork.com/mobile_stock.json", 31 | "downloads" 32 | ], 33 | "content_scripts": [ 34 | { 35 | "matches": [ 36 | "*://*.supremenewyork.com/*" 37 | ], 38 | "js": [ 39 | "extension.js" 40 | ] 41 | }, 42 | { 43 | "matches": [ 44 | "*://*.supremenewyork.com/*" 45 | ], 46 | "js": [ 47 | "preload.js" 48 | ], 49 | "run_at": "document_start" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /bot/src/app/actions/profiles.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import addNotification from '../actions/notification'; 3 | 4 | export function createProfile(name, description, settings = {}) { 5 | return function(dispatch) { 6 | dispatch(addNotification('Profile created')); 7 | dispatch({ 8 | type: types.PROFILE_CREATE, 9 | name, 10 | description, 11 | settings, 12 | }); 13 | }; 14 | } 15 | 16 | export function setProfileEnabled(name) { 17 | return function(dispatch) { 18 | dispatch(addNotification(`Profile set to ${name}`)); 19 | dispatch({ 20 | type: types.PROFILE_SET_ENABLED, 21 | name, 22 | }); 23 | }; 24 | } 25 | 26 | export function removeProfile(name) { 27 | return function(dispatch) { 28 | dispatch(addNotification('Profile deleted')); 29 | dispatch({ 30 | type: types.PROFILE_REMOVE, 31 | name, 32 | }); 33 | }; 34 | } 35 | 36 | export function updateProfileSettings(profileName, shop, key, value) { 37 | return function(dispatch) { 38 | dispatch(addNotification('Settings saved')); 39 | const obj = {}; 40 | obj[key] = value; 41 | dispatch({ 42 | type: types.PROFILE_UPDATE_SETTINGS, 43 | name: profileName, 44 | value: obj, 45 | shop, 46 | }); 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /bot/src/app/reducers/atc.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export default function atc(state = { 4 | atcProducts: [], 5 | }, action) { 6 | if (action.type === types.ATC_PRODUCT_ADD) { 7 | if (!action.data.name || state.atcProducts.filter(x => x.name === action.data.name).length) { 8 | return state; 9 | } 10 | const atcList = state.atcProducts.map(x => Object.assign({}, x)); 11 | const maxId = atcList.sort((a, b) => b.id - a.id)[0]; 12 | atcList.push({ product: action.data, id: maxId ? (maxId.id + 1) : 1 }); 13 | return { atcProducts: atcList }; 14 | } else if (action.type === types.ATC_PRODUCT_REMOVE) { 15 | if (!action.name) { 16 | return state; 17 | } 18 | return { 19 | atcProducts: state.atcProducts.map(x => Object.assign({}, x)).filter(x => x.product.name !== action.name), 20 | }; 21 | } else if (action.type === types.ATC_PRODUCT_EDIT) { 22 | const atcProduct = state.atcProducts.filter(x => x.product.name === action.name)[0]; 23 | if (!atcProduct) return state; 24 | 25 | const newList = state.atcProducts.map((x) => { 26 | if (x.product.name === action.name) { 27 | return Object.assign({}, x, { product: Object.assign({}, x.product, action.data) }); 28 | } 29 | return Object.assign({}, x); 30 | }); 31 | return { 32 | atcProducts: newList, 33 | }; 34 | } 35 | return state; 36 | } 37 | -------------------------------------------------------------------------------- /bot/src/services/supreme/DropsService.js: -------------------------------------------------------------------------------- 1 | import request from 'browser-request'; 2 | 3 | export default class DropsService { 4 | static fetchDrops() { 5 | return new Promise((resolve, reject) => { 6 | request({ url: 'https://api.openaio.com/drops/' }, (error, response, body) => { 7 | if (!error && response.statusCode === 200) { 8 | try { 9 | resolve(JSON.parse(body)); 10 | } catch (e) { 11 | console.error(e); 12 | reject(e); 13 | } 14 | } else { 15 | reject({ error }); 16 | } 17 | }); 18 | }); 19 | } 20 | 21 | static async fetchLatestDrop() { 22 | const drops = await DropsService.fetchDrops(); 23 | return drops.find(x => x.slug === 'latest'); 24 | } 25 | 26 | static async fetchLatestDropProducts() { 27 | const drop = await DropsService.fetchLatestDrop(); 28 | if (!drop) return []; 29 | return await DropsService.fetchProducts(drop.slug); 30 | } 31 | 32 | static fetchProducts(dropSlug) { 33 | return new Promise((resolve, reject) => { 34 | request({ url: `https://api.openaio.com/drops/${dropSlug}/products/` }, (error, response, body) => { 35 | if (!error && response.statusCode === 200) { 36 | try { 37 | resolve(JSON.parse(body)); 38 | } catch (e) { 39 | console.error(e); 40 | reject(e); 41 | } 42 | } else { 43 | reject({ error }); 44 | } 45 | }); 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/Products.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import StorageService from '../../../../../services/StorageService'; 3 | import ChromeService from '../../../../../services/ChromeService'; 4 | import ProductList from '../ProductList'; 5 | import LocalChangeSelect from '../LocalChangeSelect'; 6 | 7 | 8 | export default class Products extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | products: [], 13 | filter: '', 14 | buyModalOpen: false, 15 | selectedProduct: null, 16 | }; 17 | StorageService.getItem('stock').then((products) => { 18 | if (products) { 19 | this.setState({ products }); 20 | } 21 | }); 22 | ChromeService.addMessageListener('stockUpdated', (stock) => { 23 | this.setState({ products: stock.data }); 24 | }); 25 | } 26 | 27 | handleBuyProduct(product) { 28 | chrome.tabs.create({ url: product.url }); 29 | } 30 | 31 | render() { 32 | return ( 33 |
34 | Object.assign(x, { 36 | keywords: x.name.split(' ').filter(z => !!z), 37 | category: x.category === 'tops_sweaters' ? 'tops-sweaters' : x.category, 38 | }))} 39 | title={( 40 |
41 | 42 |

Click on a product to buy

43 |
44 | ) 45 | } 46 | onProductClick={this.handleBuyProduct} 47 | /> 48 |
49 | 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bot/src/app/constants/Styles.js: -------------------------------------------------------------------------------- 1 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 2 | import { grey50 } from 'material-ui/styles/colors'; 3 | import { spacing, typography } from 'material-ui/styles'; 4 | 5 | const mainColor = 'rgb(247, 70, 70)'; 6 | const styles = { 7 | paper: { 8 | margin: spacing.desktopGutter, 9 | padding: spacing.desktopGutter / 2, 10 | }, 11 | container: { 12 | display: 'flex', 13 | flexFlow: 'column', 14 | alignItems: 'center', 15 | minHeight: '100%', 16 | backgroundColor: grey50, 17 | }, 18 | content: { 19 | maxWidth: 1200, 20 | margin: '0 auto', 21 | zIndex: 101, 22 | }, 23 | tabs: { 24 | flex: 1, 25 | marginLeft: 16, 26 | }, 27 | tab: { 28 | height: 64, 29 | }, 30 | appBar: { 31 | height: 64, 32 | }, 33 | logo: { 34 | fontSize: 16, 35 | color: typography.textFullWhite, 36 | lineHeight: `${spacing.desktopKeylineIncrement}px`, 37 | fontWeight: typography.fontWeightLight, 38 | backgroundColor: mainColor, 39 | height: 64, 40 | textAlign: 'center', 41 | cursor: 'pointer', 42 | }, 43 | fields: { 44 | text: { 45 | width: '100%', 46 | }, 47 | }, 48 | colors: { 49 | main: 'rgb(72, 72, 72)', 50 | drawer: 'rgb(250, 250, 250)', 51 | tabs: 'rgb(137, 189, 191)', 52 | }, 53 | theme: { 54 | palette: { 55 | primary1Color: mainColor, 56 | primary2Color: mainColor, 57 | }, 58 | drawer: { 59 | color: 'rgb(250, 250, 250)', 60 | }, 61 | }, 62 | }; 63 | 64 | export function getTheme() { 65 | return getMuiTheme(styles.theme); 66 | } 67 | 68 | export default styles; 69 | -------------------------------------------------------------------------------- /bot/src/app/utils/SupremeUtils.js: -------------------------------------------------------------------------------- 1 | import StorageService from '../../services/StorageService'; 2 | 3 | export const OnSoldOutCartActions = { 4 | REMOVE_SOLD_OUT_PRODUCTS: 'REMOVE_SOLD_OUT_PRODUCTS', 5 | STOP: 'STOP', 6 | }; 7 | 8 | function createDesktopAddressCookie(profile) { 9 | let str = null; 10 | switch (profile.order_billing_country) { 11 | case 'CANADA': 12 | case 'USA': 13 | str = `${profile.order_billing_name}|${profile.bo}|${profile.oba3 || ''}|${profile.order_billing_city}|${profile.order_billing_state || ''}|${profile.order_billing_zip}|${profile.order_billing_country}|${profile.order_email}|${profile.order_tel}`; 14 | break; 15 | case 'JAPAN': 16 | str = `${profile.order_billing_name}|${profile.bo}|${profile.order_billing_city}|${profile.order_billing_state || ''}|${profile.order_billing_zip}|${profile.order_email}|${profile.order_tel}`; 17 | break; 18 | default: 19 | str = `${profile.order_billing_name}|${profile.bo}|${profile.oba3 || ''}||${profile.order_billing_city}|${profile.order_billing_state || ''}|${profile.order_billing_zip}|${profile.order_billing_country}|${profile.order_email}|${profile.order_tel}`; 20 | break; 21 | } 22 | 23 | return { 24 | name: 'address', 25 | value: encodeURIComponent(str), 26 | url: 'https://www.supremenewyork.com', 27 | }; 28 | } 29 | 30 | export async function updateSupremeCookies(profile) { 31 | console.log('Updating supreme address cookies'); 32 | if (profile) { 33 | return StorageService.setCookie(createDesktopAddressCookie(profile)); 34 | } 35 | 36 | console.warn('Couldnt update supreme cookies, profile isnt set'); 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /bot/src/app/containers/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 4 | import AppDrawer from './AppDrawer'; 5 | import NotificationBar from '../components/NotificationBar'; 6 | import { getTheme } from '../constants/Styles'; 7 | import { setDrawerOpen } from '../actions/drawer'; 8 | 9 | class App extends Component { 10 | componentDidMount() { 11 | window.onresize = () => this.handleResize(); 12 | } 13 | 14 | handleResize() { 15 | const { setDrawerOpen, drawerOpen } = this.props; 16 | if (window.innerWidth <= 1024) { 17 | setDrawerOpen(false); 18 | } else if (!drawerOpen) { 19 | setDrawerOpen(true); 20 | } 21 | } 22 | 23 | render() { 24 | const { children } = this.props; 25 | const drawerOpen = this.props.drawerOpen; 26 | return ( 27 | 28 |
29 | 30 |
{children}
31 | 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | App.propTypes = { 39 | children: PropTypes.element, 40 | drawerOpen: PropTypes.bool, 41 | setDrawerOpen: PropTypes.func, 42 | }; 43 | 44 | function mapStateToProps(state) { 45 | return { 46 | drawerOpen: state.drawer.open, 47 | }; 48 | } 49 | 50 | function mapDispatchToProps(dispatch) { 51 | return { 52 | setDrawerOpen: open => dispatch(setDrawerOpen(open)), 53 | }; 54 | } 55 | 56 | export default connect(mapStateToProps, mapDispatchToProps)(App); 57 | -------------------------------------------------------------------------------- /bot/src/app/utils/FuzzyStringMatcher.js: -------------------------------------------------------------------------------- 1 | import string_score from 'string_score'; 2 | 3 | export default class FuzzyStringMatcher { 4 | constructor(arr, options = {}) { 5 | this.data = arr; 6 | this.key = options.key; 7 | } 8 | 9 | search(str) { 10 | const keywords = str.split(' ').filter(x => !!x); 11 | const matches = []; 12 | for (let i = 0; i < this.data.length; i += 1) { 13 | const match = { 14 | name: this.key ? this.data[i][this.key] : this.data[i], 15 | obj: this.data[i], 16 | matches: 0, 17 | valid: true, 18 | }; 19 | for (let j = 0; j < keywords.length; j += 1) { 20 | let keyword = keywords[j].toLowerCase().trim(); 21 | const isNegative = keyword[0] === '!'; 22 | if (isNegative) { 23 | keyword = keyword.substr(1); 24 | } 25 | const regexp = new RegExp(keyword); 26 | const productName = match.name.toLowerCase().trim(); 27 | const splitted = productName.split(' ').filter(x => !!x); 28 | 29 | if (regexp.test(productName)) { 30 | match.matches += 1; 31 | if (isNegative) { 32 | match.valid = false; 33 | } 34 | } 35 | for (let k = 0; k < splitted.length; k += 1) { 36 | if (splitted[k].score(keyword, 0.1) >= 0.5) { 37 | match.matches += 1; 38 | if (isNegative) { 39 | match.valid = false; 40 | } 41 | } 42 | } 43 | } 44 | matches.push(match); 45 | } 46 | const bestMatches = matches.filter(x => x.matches >= 1 && x.valid).sort((a, b) => b.matches - a.matches); 47 | if (this.key) { 48 | return bestMatches.map(x => x.obj); 49 | } 50 | return bestMatches.map(x => this.data.indexOf(x.name)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bot/src/app/reducers/profiles.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export default function profiles(state = { 4 | currentProfile: 'default', 5 | profiles: [{ 6 | name: 'default', 7 | description: 'Default profile', 8 | settings: {}, 9 | }], 10 | }, action) { 11 | if (action.type === types.PROFILE_CREATE) { 12 | const list = state.profiles.map(x => Object.assign({}, x)); 13 | list.push({ 14 | name: action.name, 15 | description: action.description, 16 | settings: action.settings || {}, 17 | }); 18 | return Object.assign({}, state, { profiles: list }); 19 | } else if (action.type === types.PROFILE_SET_ENABLED) { 20 | if (!state.profiles.filter(x => x.name === action.name)) { 21 | return state; 22 | } 23 | return Object.assign({}, state, { currentProfile: action.name }); 24 | } else if (action.type === types.PROFILE_REMOVE) { 25 | if (action.name === 'default') { 26 | return state; 27 | } 28 | return Object.assign({}, state, { 29 | profiles: state.profiles.filter(x => x.name !== action.name), 30 | currentProfile: state.currentProfile === action.name ? 'default' : state.currentProfile, 31 | }); 32 | } else if (action.type === types.PROFILE_UPDATE_SETTINGS) { 33 | const profile = state.profiles.filter(x => x.name === action.name)[0]; 34 | if (!profile) { 35 | return state; 36 | } 37 | 38 | const settingsObj = Object.assign({}, profile.settings); 39 | settingsObj[action.shop] = Object.assign({}, profile.settings[action.shop], action.value); 40 | const profileList = state.profiles.map(x => { 41 | if (x.name === action.name) { 42 | return Object.assign({}, x, { settings: settingsObj }); 43 | } 44 | return x; 45 | }); 46 | return Object.assign({}, state, { profiles: profileList }); 47 | } 48 | return state; 49 | } 50 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/processors/checkoutProcessor.js: -------------------------------------------------------------------------------- 1 | import BaseProcessor from './baseProcessor'; 2 | import CheckoutService from '../../../../services/supreme/CheckoutService'; 3 | import { timeout } from '../helpers'; 4 | 5 | 6 | export default class CheckoutProcessor extends BaseProcessor { 7 | static start(preferences, sizings, billing) { 8 | const processor = new CheckoutProcessor(preferences, sizings, billing); 9 | processor.beginProcess(); 10 | return processor; 11 | } 12 | 13 | beginProcess() { 14 | this.processCheckout(); 15 | } 16 | 17 | /** 18 | * This function should be called when the user is on the 'checkout' page, it will fill 19 | * the checkout form with the values defined by the user in the options and then checkout after a delay 20 | */ 21 | async processCheckout() { 22 | const checkoutDelay = this.preferences.checkoutDelay; 23 | const querySelectorAll = (document.mockedQuerySelectorAll || document.querySelectorAll) 24 | .bind(document); 25 | const querySelector = (document.mockedQuerySelector || document.querySelector) 26 | .bind(document); 27 | 28 | const inputs = [...querySelectorAll('input, textarea, select')] 29 | .filter(x => ['hidden', 'submit', 'button', 'checkbox'].indexOf(x.type) === -1 30 | && (!x.value || x.type === 'select-one')); 31 | await CheckoutService.processFields(inputs, this.billing, checkoutDelay); 32 | const terms = querySelector('.terms'); 33 | if (terms) terms.click(); 34 | const checkboxes = querySelectorAll('input[type="checkbox"]'); 35 | if (checkboxes.length) checkboxes[checkboxes.length - 1].checked = true; 36 | if (this.preferences.autoPay) { 37 | timeout(() => { 38 | const commitBtn = querySelector('[name="commit"]'); 39 | if (commitBtn) { 40 | commitBtn.click(); 41 | } 42 | }, checkoutDelay, 'Checking out'); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/LocalChangeSelect.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import SelectField from 'material-ui/SelectField'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import StorageService from '../../../../services/StorageService'; 6 | import addNotification from '../../../actions/notification'; 7 | 8 | class LocalChangeSelect extends Component { 9 | 10 | constructor() { 11 | super(); 12 | this.state = { 13 | locale: 'eu', 14 | }; 15 | 16 | StorageService.getItem('locale').then((locale) => { 17 | if (locale) { 18 | this.setState({ locale }); 19 | } 20 | }); 21 | } 22 | 23 | handleLocaleChange(newLocale) { 24 | if (this.state.locale === newLocale) return; 25 | this.props.notify(`Store location changed to ${newLocale}`); 26 | StorageService.setItem('locale', newLocale).then(() => StorageService.setItem('stock', [])).then(() => this.setState({ locale: newLocale })); 27 | } 28 | 29 | render() { 30 | const { style } = this.props; 31 | return ( 32 |
33 | this.handleLocaleChange(v)} 38 | > 39 | 40 | 41 | 42 |
43 | ); 44 | } 45 | } 46 | 47 | LocalChangeSelect.defaultProps = { 48 | style: { 49 | textAlign: 'center', 50 | }, 51 | }; 52 | 53 | LocalChangeSelect.propTypes = { 54 | style: PropTypes.object, 55 | }; 56 | 57 | function mapDispatchToProps(dispatch) { 58 | return { 59 | notify: m => dispatch(addNotification(m)), 60 | }; 61 | } 62 | 63 | export default connect(undefined, mapDispatchToProps)(LocalChangeSelect); 64 | -------------------------------------------------------------------------------- /bot/src/app/utils/FormValidators.js: -------------------------------------------------------------------------------- 1 | import * as Helpers from './Helpers'; 2 | 3 | export function email(input) { 4 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 5 | return (!input || re.test(input)) ? undefined : 'Please enter a valid email'; 6 | } 7 | 8 | export function required(value) { 9 | const empty = typeof value !== 'number' && !value; 10 | return empty ? 'Required field' : undefined; 11 | } 12 | 13 | export function notEmpty(arr) { 14 | if (!Array.isArray(arr) || !arr.length) { 15 | return 'This field must not be empty'; 16 | } 17 | } 18 | 19 | export function simpleText(value) { 20 | return value && !value.match(/^[0-9a-zA-Z_]{1,64}$/) ? 'Only use characters (a-z, A-Z, 0-9, _)' : undefined; 21 | } 22 | 23 | export function number(value) { 24 | return value && (isNaN(Number(value)) || +value < 0) ? 'This field must be a positive number' : undefined; 25 | } 26 | 27 | export function minValue(min) { 28 | return value => (value && value < min ? `The value must be greater than ${min}` : undefined); 29 | } 30 | 31 | export function maxValue(max) { 32 | return value => (value && value > max ? `The value must be less than ${max}` : undefined); 33 | } 34 | 35 | export function fullName(value) { 36 | return value && value.split(' ').filter(x => x !== '').length < 2 ? 'Please enter a valid full name (firstname + lastname)' : undefined; 37 | } 38 | 39 | export function unique(candidates) { 40 | return value => { 41 | if (candidates.indexOf(value) !== -1) { 42 | return 'This value already exists and must be unique'; 43 | } 44 | return undefined; 45 | }; 46 | } 47 | 48 | export function date(val) { 49 | const d = new Date(val); 50 | return isNaN(d.getTime()) ? 'Required date' : undefined; 51 | } 52 | 53 | export function time24(val) { 54 | const msg = 'Invalid time'; 55 | if (!Helpers.isValidTime(val)) { 56 | return msg; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bot/src/app/containers/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import AppBar from 'material-ui/AppBar'; 3 | import Tabs from 'material-ui/Tabs/Tabs'; 4 | import Paper from 'material-ui/Paper'; 5 | import { connect } from 'react-redux'; 6 | import Styles from '../constants/Styles'; 7 | import { setDrawerOpen } from '../actions/drawer'; 8 | 9 | class Layout extends Component { 10 | toggleMenu() { 11 | this.props.setDrawerOpen(!this.props.drawerOpen); 12 | } 13 | 14 | render() { 15 | const { children, title, contentStyle, tabs, currentTab } = this.props; 16 | const appBarStyles = Object.assign({}, Styles.appBar); 17 | const tabContainer = ( 18 |
19 | 20 | { tabs } 21 | 22 |
23 | ); 24 | return ( 25 |
26 | this.toggleMenu()} /> 27 |
28 |
29 | 30 | {children} 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | Layout.propTypes = { 40 | children: PropTypes.element, 41 | title: PropTypes.string, 42 | contentStyle: PropTypes.object, 43 | tabs: PropTypes.arrayOf(PropTypes.node), 44 | currentTab: PropTypes.any, 45 | drawerOpen: PropTypes.bool, 46 | setDrawerOpen: PropTypes.func, 47 | }; 48 | 49 | function mapStateToProps(state) { 50 | return { 51 | drawerOpen: state.drawer.open, 52 | }; 53 | } 54 | 55 | function mapDispatchToProps(dispatch) { 56 | return { 57 | setDrawerOpen: open => dispatch(setDrawerOpen(open)), 58 | }; 59 | } 60 | 61 | export default connect(mapStateToProps, mapDispatchToProps)(Layout); 62 | -------------------------------------------------------------------------------- /bot/src/services/supreme/ProductsService.js: -------------------------------------------------------------------------------- 1 | import request from 'browser-request'; 2 | 3 | export default class ProductsService { 4 | static fetchProducts() { 5 | return new Promise((resolve, reject) => { 6 | const time = (new Date()).getTime(); 7 | request({ url: `https://www.supremenewyork.com/mobile_stock.json?_=${time}` }, (error, response, body) => { 8 | if (!error && response.statusCode === 200) { 9 | try { 10 | const data = JSON.parse(body); 11 | resolve(data.products_and_categories); 12 | } catch (e) { 13 | console.error(e); 14 | reject(e); 15 | } 16 | } else { 17 | reject({ error }); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | static fetchProductInfo(id) { 24 | return new Promise((resolve, reject) => { 25 | const time = (new Date()).getTime(); 26 | const url = `https://www.supremenewyork.com/shop/${id}.json`; 27 | request({ url: `${url}?_=${time}` }, (error, response, body) => { 28 | if (!error && response.statusCode === 200) { 29 | try { 30 | const data = JSON.parse(body); 31 | resolve(Object.assign({ id }, data)); 32 | } catch (e) { 33 | console.error(e); 34 | reject(e); 35 | } 36 | } else { 37 | reject({ error }); 38 | } 39 | }); 40 | }); 41 | } 42 | } 43 | 44 | if (chrome && chrome.webRequest) { 45 | chrome.webRequest.onBeforeSendHeaders.addListener( 46 | function (details) { 47 | const userAgent = details.requestHeaders.filter(x => x.name === 'User-Agent')[0]; 48 | if (userAgent) { 49 | userAgent.value = 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_3 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13G34'; 50 | } 51 | return { requestHeaders: details.requestHeaders }; 52 | }, 53 | { urls: ['*://*.supremenewyork.com/mobile/*', '*://*.supremenewyork.com/mobile_stock.json*', '*://*.supremenewyork.com/shop/*.json'] }, 54 | ['blocking', 'requestHeaders'], 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /bot/src/extension/background/supreme/RestockMonitor.js: -------------------------------------------------------------------------------- 1 | import RestocksService from '../../../services/supreme/RestocksService'; 2 | import StorageService from '../../../services/StorageService'; 3 | import ChromeService from '../../../services/ChromeService'; 4 | 5 | export default class RestockMonitor { 6 | constructor(intervalMs) { 7 | this.intervalMs = intervalMs; 8 | this.onNewProductsCallbacks = []; 9 | this.onProductsRestockCallbacks = []; 10 | } 11 | 12 | addOnNewProductsListener(func) { 13 | this.onNewProductsCallbacks.push(func); 14 | } 15 | 16 | addOnProductsRestockListener(func) { 17 | this.onProductsRestockCallbacks.push(func); 18 | } 19 | 20 | async update() { 21 | const newStock = await RestocksService.fetchCurrentStock(); 22 | const savedStock = await StorageService.getItem('stock'); 23 | if (!savedStock) { 24 | for (const callback of this.onNewProductsCallbacks) callback(newStock); 25 | await StorageService.setItem('stock', newStock); 26 | return; 27 | } 28 | const newProducts = newStock.filter(x => !savedStock.find(z => z.url === x.url)); 29 | if (newProducts.length) { 30 | for (const callback of this.onNewProductsCallbacks) callback(newProducts); 31 | } 32 | 33 | let restockedProducts = []; 34 | for (let i = 0; i < newStock.length; i += 1) { 35 | const product = newStock[i]; 36 | const existingProduct = savedStock.find(x => x.url === product.url); 37 | if (existingProduct && !product.soldOut && existingProduct.soldOut) { 38 | restockedProducts.push(product); 39 | } 40 | } 41 | if (restockedProducts.length) { 42 | for (const callback of this.onProductsRestockCallbacks) callback(restockedProducts); 43 | } 44 | await StorageService.setItem('stock', newStock); 45 | ChromeService.sendMessage('stockUpdated', newStock); 46 | } 47 | 48 | start() { 49 | this.interval = setInterval(async () => await this.update(), this.intervalMs); 50 | this.update(); 51 | } 52 | 53 | stop() { 54 | if (this.interval) { 55 | clearInterval(this.interval); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/processors/cartProcessor.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import * as SupremeUtils from '../../../../app/utils/SupremeUtils'; 3 | import BaseProcessor from './baseProcessor'; 4 | import { notify } from '../notification'; 5 | import { timeout } from '../helpers'; 6 | 7 | export default class CartProcessor extends BaseProcessor { 8 | static start(preferences, sizings, billing) { 9 | const processor = new CartProcessor(preferences, sizings, billing); 10 | processor.beginProcess(); 11 | return processor; 12 | } 13 | 14 | beginProcess() { 15 | this.processCart(); 16 | } 17 | 18 | /** 19 | * This function should be called when the user is on the 'cart' page, it will then redirect the user 20 | * to the checkout page after the delay configured in the options 21 | */ 22 | processCart() { 23 | if (!this.preferences.autoCheckout) { 24 | return; 25 | } 26 | const outOfStockItems = document.querySelectorAll('.out_of_stock'); 27 | const outOfStockAction = this.preferences.onCartSoldOut; 28 | if (!outOfStockItems.length) { 29 | timeout(() => { 30 | document.location.href = '/checkout'; 31 | }, 100, 'Going to checkout'); 32 | return; 33 | } 34 | if (outOfStockAction === SupremeUtils.OnSoldOutCartActions.STOP) { 35 | notify('Product was sold out, aborting...', true); 36 | } else if (outOfStockAction === SupremeUtils.OnSoldOutCartActions.REMOVE_SOLD_OUT_PRODUCTS) { 37 | const promises = []; 38 | for (const product of outOfStockItems) { 39 | const form = product.querySelector('form'); 40 | if (form) { 41 | promises.push(new Promise((resolve, reject) => { 42 | $.ajax({ 43 | type: 'POST', 44 | url: $(form).attr('action'), 45 | data: $(form).serializeArray(), 46 | success: resolve, 47 | error: reject, 48 | }); 49 | })); 50 | } 51 | } 52 | Promise.all(promises).then(() => { 53 | timeout(() => { 54 | document.location.href = '/checkout'; 55 | }, 100, 'Going to checkout'); 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/Drops.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import Divider from 'material-ui/Divider'; 4 | import CircularProgress from 'material-ui/CircularProgress'; 5 | import { Card, CardText } from 'material-ui/Card'; 6 | import Layout from '../../../../containers/Layout'; 7 | import DropsService from '../../../../../services/supreme/DropsService'; 8 | 9 | export default class Drops extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | drops: {}, 14 | }; 15 | this.fetchDrops(); 16 | } 17 | 18 | fetchDrops() { 19 | DropsService.fetchDrops().then(drops => this.setState({ drops })); 20 | } 21 | 22 | handleRequestClose() { 23 | this.setState({ 24 | buyModalOpen: false, 25 | }); 26 | } 27 | 28 | getDropCard(drop) { 29 | const style = { 30 | width: 200, 31 | minWidth: 250, 32 | flex: 1, 33 | margin: 20, 34 | }; 35 | 36 | return ( 37 | 38 | 39 |
40 | 41 |

{ drop.name }

42 |
43 |
44 | 45 |
46 | 47 | ); 48 | } 49 | 50 | render() { 51 | const children = this.props.children; 52 | if (children) { 53 | return children; 54 | } 55 | const drops = this.state.drops; 56 | if (!drops.length) { 57 | return ( 58 | 59 |
60 |

Loading...

61 | 62 |
63 |
64 | ); 65 | } 66 | const cards = drops.map(x => this.getDropCard(x)); 67 | const style = { 68 | display: 'flex', 69 | flexDirection: 'row', 70 | justifyContent: 'space-evenly', 71 | flexWrap: 'wrap', 72 | cursor: 'pointer', 73 | }; 74 | return ( 75 | 76 |
77 | {cards} 78 |
79 |
80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /bot/src/app/components/ProfileImportForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import { 6 | TextField, 7 | } from 'redux-form-material-ui'; 8 | import FlatButton from 'material-ui/FlatButton'; 9 | import FileInput from './FileInput'; 10 | import * as Validators from '../utils/FormValidators'; 11 | import Styles from '../constants/Styles'; 12 | 13 | 14 | class ProfileImportForm extends Component { 15 | render() { 16 | const { handleSubmit, pristine, submitting, onRequestClose } = this.props; 17 | const buttonStyle = { 18 | margin: 6, 19 | float: 'right', 20 | }; 21 | 22 | return ( 23 |
24 |
25 | 33 |
34 |
35 | 44 |
45 |
46 | 52 | { 56 | if (onRequestClose) { 57 | onRequestClose(); 58 | } 59 | }} 60 | /> 61 |
62 |
63 | ); 64 | } 65 | } 66 | 67 | ProfileImportForm.propTypes = { 68 | onRequestClose: PropTypes.function, 69 | }; 70 | 71 | const Form = reduxForm({ 72 | form: 'profile-export-form', 73 | })(ProfileImportForm); 74 | 75 | export default Form; 76 | -------------------------------------------------------------------------------- /bot/src/assets/css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: 'Roboto', sans-serif; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-size: 13px; 8 | line-height: 20px; 9 | min-width: 700px; 10 | min-height: 600px; 11 | } 12 | 13 | form > div { 14 | width: 100% !important; 15 | } 16 | 17 | html, body, #app { 18 | height: 100%; 19 | } 20 | 21 | #app { 22 | overflow-y: scroll; 23 | } 24 | 25 | /* Fonts */ 26 | /* roboto-300 - latin */ 27 | @font-face { 28 | font-family: 'Roboto'; 29 | font-style: normal; 30 | font-weight: 300; 31 | src: url('/assets/fonts/roboto-v15-latin-300.eot'); /* IE9 Compat Modes */ 32 | src: local('Roboto Light'), local('Roboto-Light'), 33 | url('/assets/fonts/roboto-v15-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 34 | url('/assets/fonts/roboto-v15-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ 35 | url('/assets/fonts/roboto-v15-latin-300.woff') format('woff'), /* Modern Browsers */ 36 | url('/assets/fonts/roboto-v15-latin-300.svg#Roboto') format('svg'); /* Legacy iOS */ 37 | } 38 | /* roboto-regular - latin */ 39 | @font-face { 40 | font-family: 'Roboto'; 41 | font-style: normal; 42 | font-weight: 400; 43 | src: url('/assets/fonts/roboto-v15-latin-regular.eot'); /* IE9 Compat Modes */ 44 | src: local('Roboto'), local('Roboto-Regular'), 45 | url('/assets/fonts/roboto-v15-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 46 | url('/assets/fonts/roboto-v15-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ 47 | url('/assets/fonts/roboto-v15-latin-regular.woff') format('woff'), /* Modern Browsers */ 48 | url('/assets/fonts/roboto-v15-latin-regular.svg#Roboto') format('svg'); /* Legacy iOS */ 49 | } 50 | /* roboto-500 - latin */ 51 | @font-face { 52 | font-family: 'Roboto'; 53 | font-style: normal; 54 | font-weight: 500; 55 | src: url('/assets/fonts/roboto-v15-latin-500.eot'); /* IE9 Compat Modes */ 56 | src: local('Roboto Medium'), local('Roboto-Medium'), 57 | url('/assets/fonts/roboto-v15-latin-500.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 58 | url('/assets/fonts/roboto-v15-latin-500.woff2') format('woff2'), /* Super Modern Browsers */ 59 | url('/assets/fonts/roboto-v15-latin-500.woff') format('woff'), /* Modern Browsers */ 60 | url('/assets/fonts/roboto-v15-latin-500.svg#Roboto') format('svg'); /* Legacy iOS */ 61 | } 62 | -------------------------------------------------------------------------------- /bot/src/app/components/ProfileExportForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { 5 | TextField, 6 | } from 'redux-form-material-ui'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import FlatButton from 'material-ui/FlatButton'; 9 | import * as Validators from '../utils/FormValidators'; 10 | import Styles from '../constants/Styles'; 11 | 12 | 13 | class ProfileExportForm extends Component { 14 | render() { 15 | const { handleSubmit, pristine, submitting, onRequestClose } = this.props; 16 | const buttonStyle = { 17 | margin: 6, 18 | float: 'right', 19 | }; 20 | 21 | return ( 22 |
23 |

You must choose a password to encrypt your settings.

24 |

You will be asked to enter this password when you later import this profile.

25 |
26 | 34 |
35 |
36 | 45 |
46 |
47 | 53 | { 57 | if (onRequestClose) { 58 | onRequestClose(); 59 | } 60 | }} 61 | /> 62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | ProfileExportForm.propTypes = { 69 | onRequestClose: PropTypes.function, 70 | }; 71 | 72 | const Form = reduxForm({ 73 | form: 'profile-export-form', 74 | })(ProfileExportForm); 75 | 76 | export default Form; 77 | -------------------------------------------------------------------------------- /bot/src/app/components/ProfileCreateForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | import { 5 | TextField, 6 | } from 'redux-form-material-ui'; 7 | import RaisedButton from 'material-ui/RaisedButton'; 8 | import FlatButton from 'material-ui/FlatButton'; 9 | import * as Validators from '../utils/FormValidators'; 10 | import Styles from '../constants/Styles'; 11 | 12 | 13 | class ProfileCreateForm extends Component { 14 | render() { 15 | const { handleSubmit, pristine, submitting, onRequestClose, profiles } = this.props; 16 | const buttonStyle = { 17 | margin: 6, 18 | float: 'right', 19 | }; 20 | 21 | return ( 22 |
23 |
24 | x.name))]} 27 | component={TextField} 28 | floatingLabelText="Name" 29 | hintText="Name" 30 | style={Styles.fields.text} 31 | /> 32 |
33 | 34 |
35 | 43 |
44 |
45 | 51 | { 55 | if (onRequestClose) { 56 | onRequestClose(); 57 | } 58 | }} 59 | /> 60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | ProfileCreateForm.propTypes = { 67 | onRequestClose: PropTypes.function, 68 | }; 69 | 70 | function mapStateToProps(state) { 71 | return { 72 | profiles: state.profiles.profiles, 73 | currentProfile: state.profiles.currentProfile, 74 | }; 75 | } 76 | 77 | const Form = reduxForm({ 78 | form: 'profile-form', 79 | })(ProfileCreateForm); 80 | 81 | export default connect(mapStateToProps)(Form); 82 | -------------------------------------------------------------------------------- /bot/src/extension/background/supreme/index.js: -------------------------------------------------------------------------------- 1 | import * as menus from '../../../app/constants/Menus'; 2 | import * as Helpers from '../../../app/utils/Helpers'; 3 | import StorageService from '../../../services/StorageService'; 4 | import AtcService from '../../../services/supreme/AtcService'; 5 | 6 | 7 | async function sleep(ms) { 8 | return new Promise((resolve) => { 9 | setTimeout(() => resolve(), ms); 10 | }); 11 | } 12 | 13 | async function timeout(ms, callback) { 14 | await sleep(ms); 15 | callback(); 16 | } 17 | 18 | async function getSettings() { 19 | try { 20 | const profile = await StorageService.getCurrentProfileSettings(); 21 | return profile.Supreme; 22 | } catch(e) { 23 | console.info('Error while getting settings'); 24 | console.info(e); 25 | return null; 26 | } 27 | } 28 | 29 | async function processByMonitor() { 30 | await AtcService.runAllMonitor(); 31 | } 32 | 33 | function isEnabled(settings) { 34 | return settings && settings[menus.MENU_OPTIONS] && settings[menus.MENU_OPTIONS].atcEnabled; 35 | } 36 | 37 | function getAtcStartTime(settings) { 38 | if (!settings || !settings[menus.MENU_OPTIONS]) { 39 | return null; 40 | } 41 | const atcStartTime = settings[menus.MENU_OPTIONS].atcStartTime; 42 | const atcStartDate = settings[menus.MENU_OPTIONS].atcStartDate; 43 | if (!atcStartTime || !atcStartDate) { 44 | return null; 45 | } 46 | const time = Helpers.timeToDate(atcStartTime); 47 | const currDate = new Date(atcStartDate); 48 | currDate.setHours(time.getHours()); 49 | currDate.setMinutes(time.getMinutes()); 50 | currDate.setSeconds(time.getSeconds()); 51 | return currDate; 52 | } 53 | 54 | async function loop() { 55 | const settings = await getSettings(); 56 | if (!isEnabled(settings)) { 57 | await timeout(1000, () => loop()); 58 | return; 59 | } 60 | const now = new Date(); 61 | const startTime = getAtcStartTime(settings); 62 | if (!startTime || !Helpers.sameDay(now, startTime)) { 63 | await timeout(1000, () => loop()); 64 | return; 65 | } 66 | const diffTime = (startTime.getTime() - now.getTime()) / 1000; 67 | console.log(`ATC starting in ${diffTime} seconds...`); 68 | 69 | if (diffTime <= 0 && Math.abs(diffTime) < 3) { 70 | if (settings.Options.atcUseMonitor) { 71 | await processByMonitor(); 72 | } else { 73 | await AtcService.runAll(); 74 | } 75 | await timeout(4000, () => loop()); 76 | return; 77 | } 78 | await timeout(1000, () => loop()); 79 | } 80 | 81 | 82 | export default async function start() { 83 | await loop(); 84 | } 85 | -------------------------------------------------------------------------------- /bot/src/services/StorageService.js: -------------------------------------------------------------------------------- 1 | import * as Helpers from '../app/utils/Helpers'; 2 | import version from '../app/version'; 3 | 4 | export default class StorageService { 5 | static getItem(key) { 6 | return new Promise((resolve) => { 7 | chrome.storage.local.get(key, async (settings) => { 8 | resolve(Helpers.isObjectEmpty(settings) ? null : settings[key]); 9 | }); 10 | }); 11 | } 12 | 13 | static setItem(key, value) { 14 | return new Promise((resolve) => { 15 | const obj = {}; 16 | obj[key] = value; 17 | chrome.storage.local.set(obj, () => { 18 | resolve(); 19 | }); 20 | }); 21 | } 22 | 23 | static setCookie(cookieObj) { 24 | return new Promise((res, rej) => { 25 | chrome.cookies.set(cookieObj, (cookie) => { 26 | if (!cookie) { 27 | return rej(new Error('Couldnt set cookie')); 28 | } 29 | 30 | return res(cookie); 31 | }); 32 | }); 33 | } 34 | 35 | static initializeStorageState(initialState) { 36 | return this.setItem('state', { value: initialState || {}, version }); 37 | } 38 | 39 | static async getOrCreateState() { 40 | try { 41 | const state = await this.loadSavedState(); 42 | if (!state) { 43 | await this.initializeStorageState({}); 44 | return {}; 45 | } 46 | return state; 47 | } catch (err) { 48 | await this.initializeStorageState({}); 49 | return {}; 50 | } 51 | } 52 | 53 | static async loadSavedState() { 54 | const state = await this.getItem('state') || {}; 55 | return state.value || null; 56 | } 57 | 58 | static async saveState(state) { 59 | try { 60 | // If no state is saved, we create it using the current version number 61 | if (!await this.getItem('state')) { 62 | await this.initializeStorageState(state); 63 | } else { 64 | let currentState = await this.getItem('state'); 65 | // If a state for the current version already exists, we update it 66 | if (currentState.version === version) { 67 | currentState.value = state; 68 | } else { 69 | currentState = { value: state, version }; 70 | } 71 | await this.setItem('state', currentState); 72 | } 73 | } catch (err) { 74 | console.info(err); 75 | console.info('Possible corrupted or incompatible state found in the localstorage, reinitializing the state...'); 76 | await this.initializeStorageState({}); 77 | } 78 | } 79 | 80 | static async getCurrentProfileSettings() { 81 | const state = await this.getOrCreateState(); 82 | if (!state || !state.profiles) { 83 | return null; 84 | } 85 | const currentProfile = state.profiles.currentProfile; 86 | return state.profiles.profiles.filter(x => x.name === currentProfile)[0].settings; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /bot/src/app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router, browserHistory, hashHistory } from 'react-router'; 4 | import { combineReducers, createStore, applyMiddleware } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import createLogger from 'redux-logger'; 7 | import { syncHistoryWithStore, routerReducer, routerMiddleware } from 'react-router-redux'; 8 | import injectTapEventPlugin from 'react-tap-event-plugin'; 9 | import { reducer as formReducer } from 'redux-form'; 10 | import thunk from 'redux-thunk'; 11 | import * as reducers from './reducers'; 12 | import getRoutes from './routes'; 13 | import StorageService from '../services/StorageService'; 14 | import migrate from './migrations'; 15 | 16 | 17 | injectTapEventPlugin(); 18 | 19 | const middleware = [thunk, routerMiddleware(browserHistory)]; 20 | if (process.env.NODE_ENV !== 'production') { 21 | middleware.push(createLogger()); 22 | } 23 | async function init() { 24 | let savedState = await StorageService.getOrCreateState(); 25 | savedState = migrate(savedState); 26 | const appReducer = combineReducers(Object.assign({}, reducers, { 27 | routing: routerReducer, 28 | form: formReducer, 29 | })); 30 | 31 | const rootReducer = (state, action) => { 32 | if (action.type === 'PROFILE_CHANGE') { 33 | return appReducer(undefined, action); 34 | } 35 | return appReducer(state, action); 36 | }; 37 | 38 | const store = createStore(rootReducer, savedState, applyMiddleware(...middleware)); 39 | 40 | let imageSettings = 'allow'; 41 | 42 | // Save state in localStorage automatically 43 | store.subscribe(async () => { 44 | const state = store.getState(); 45 | if (state) { 46 | await StorageService 47 | .saveState({ menu: state.menu, profiles: state.profiles, atc: state.atc }); 48 | 49 | try { 50 | const settings = await StorageService.getCurrentProfileSettings(); 51 | if (!settings || !settings.Supreme || !settings.Supreme.Options) { 52 | return; 53 | } 54 | 55 | const newImageSettings = settings.Supreme.Options.disableImages ? 'block' : 'allow'; 56 | if (newImageSettings !== imageSettings) { 57 | chrome.contentSettings.images.set({ 58 | primaryPattern: 'https://www.supremenewyork.com/*', 59 | setting: newImageSettings, 60 | }); 61 | imageSettings = newImageSettings; 62 | } 63 | } catch(e) { 64 | console.warn(e); 65 | console.warn('Error while trying to set image settings'); 66 | } 67 | } 68 | }); 69 | 70 | const history = syncHistoryWithStore(hashHistory, store); 71 | 72 | ReactDOM.render( 73 | 74 | 75 | {getRoutes(store)} 76 | 77 | , 78 | document.getElementById('app'), 79 | ); 80 | } 81 | 82 | init(); 83 | -------------------------------------------------------------------------------- /bot/src/extension/background/index.js: -------------------------------------------------------------------------------- 1 | import SupremeBackground from './supreme/index'; 2 | import RestockMonitor from './supreme/RestockMonitor'; 3 | import StorageService from '../../services/StorageService'; 4 | import ChromeService from '../../services/ChromeService'; 5 | 6 | async function updateRestockList(products, type) { 7 | let restockList = await StorageService.getItem('restocks'); 8 | const entries = []; 9 | for (const product of products) { 10 | entries.push({ 11 | type, 12 | product, 13 | timestamp: new Date().getTime(), 14 | }); 15 | } 16 | 17 | if (!restockList) { 18 | await StorageService.setItem('restocks', entries); 19 | return; 20 | } 21 | restockList.push(...entries); 22 | restockList = restockList.sort((a, b) => { 23 | return new Date(b.timestamp) - new Date(a.timestamp); 24 | }).splice(0, 100); 25 | await StorageService.setItem('restocks', restockList); 26 | } 27 | 28 | async function getSettings() { 29 | try { 30 | const profile = await StorageService.getCurrentProfileSettings(); 31 | return profile.Supreme; 32 | } catch(e) { 33 | console.info('Restock monitor ---- Error while getting settings, bot is not yet configured'); 34 | console.info(e); 35 | return null; 36 | } 37 | } 38 | 39 | async function canDisplayNotifications() { 40 | const settings = await getSettings(); 41 | if (!settings) return false; 42 | return settings.Options && settings.Options.showNotifications; 43 | } 44 | 45 | async function createNotification(title, content, callback) { 46 | if (await canDisplayNotifications()) { 47 | ChromeService.createNotification(title, content, callback); 48 | } 49 | } 50 | 51 | async function onNewProducts(products) { 52 | await updateRestockList(products, 'new'); 53 | if(products.length > 3) { 54 | await createNotification('New products', `${products.length} new products just landed on the store!`, (notif) => { 55 | window.open('https://www.supremenewyork.com/shop/new'); 56 | notif.close(); 57 | }); 58 | } else { 59 | for (let product of products) { 60 | await createNotification('New product', `Product ${product.name} just landed on the store!`, (notif) => { 61 | window.open(product.url); 62 | notif.close(); 63 | }); 64 | } 65 | } 66 | ChromeService.sendMessage('productsAdded', { products }); 67 | } 68 | 69 | async function onProductsRestock(products) { 70 | await updateRestockList(products, 'restock'); 71 | for (let i = 0; i < products.length; i += 1) { 72 | const product = products[i]; 73 | ChromeService.sendMessage('productRestocked', { product }); 74 | await createNotification('Restock alert', `${product.name} in ${product.color} is back in stock!`, (notif) => { 75 | window.open(product.url); 76 | notif.close(); 77 | }); 78 | } 79 | } 80 | 81 | async function start() { 82 | const monitor = new RestockMonitor(10000); 83 | monitor.addOnNewProductsListener(async products => await onNewProducts(products)); 84 | monitor.addOnProductsRestockListener(async products => await onProductsRestock(products)); 85 | monitor.start(); 86 | await SupremeBackground(); 87 | } 88 | start(); 89 | -------------------------------------------------------------------------------- /bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supreme-auto-checkout", 3 | "version": "2.10.1", 4 | "description": "Supreme Bot", 5 | "scripts": { 6 | "watch": "webpack --env=prod --progress --colors --config ./webpack.config.prod.js --watch", 7 | "build": "webpack --env=prod --progress --colors --config ./webpack.config.prod.js", 8 | "lint": "eslint src && echo \"eslint: no lint errors\"", 9 | "test": "./node_modules/.bin/mocha --compilers js:babel-core/register" 10 | }, 11 | "private": true, 12 | "devDependencies": { 13 | "autobind-decorator": "^1.3.3", 14 | "babel-cli": "^6.26.0", 15 | "babel-core": "^6.24.1", 16 | "babel-eslint": "^7.1.1", 17 | "babel-loader": "^7.1.1", 18 | "babel-plugin-syntax-async-functions": "^6.13.0", 19 | "babel-plugin-transform-async-to-generator": "^6.24.1", 20 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 21 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 22 | "babel-plugin-transform-regenerator": "^6.8.0", 23 | "babel-plugin-transform-runtime": "^6.23.0", 24 | "babel-polyfill": "^6.23.0", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-es2017": "^6.24.1", 27 | "babel-preset-flow": "^6.23.0", 28 | "babel-preset-react": "^6.3.13", 29 | "babel-preset-react-hmre": "^1.1.1", 30 | "babel-preset-stage-0": "^6.5.0", 31 | "babel-runtime": "^6.23.0", 32 | "babelify": "^7.2.0", 33 | "browser-sync": "^2.11.0", 34 | "browserify": "^13.0.0", 35 | "chai": "^4.1.2", 36 | "copy-webpack-plugin": "^4.0.1", 37 | "css-loader": "^0.28.4", 38 | "css-modules-require-hook": "^4.0.6", 39 | "envify": "^4.0.0", 40 | "eslint": "^3.12.2", 41 | "eslint-config-airbnb": "^13.0.0", 42 | "eslint-plugin-import": "^2.2.0", 43 | "eslint-plugin-jsx-a11y": "^2.2.3", 44 | "eslint-plugin-react": "^6.8.0", 45 | "flow-bin": "^0.55.0", 46 | "html-webpack-plugin": "^2.29.0", 47 | "mocha": "^3.5.3", 48 | "pretty-hrtime": "^1.0.1", 49 | "proxy-middleware": "^0.15.0", 50 | "require-dir": "^0.3.2", 51 | "style-loader": "^0.18.2", 52 | "underscore": "^1.7.0", 53 | "vinyl-source-stream": "^1.0.0", 54 | "watchify": "^3.6.1", 55 | "webpack": "^3.0.0", 56 | "webpack-dev-middleware": "^1.11.0", 57 | "webpack-dev-server": "^2.5.0", 58 | "webpack-hot-middleware": "^2.18.0" 59 | }, 60 | "dependencies": { 61 | "browser-request": "^0.3.3", 62 | "crypto-js": "^3.1.9-1", 63 | "fuse.js": "^3.1.0", 64 | "jquery": "^3.2.1", 65 | "lodash": "^4.17.4", 66 | "material-ui": "^0.16.7", 67 | "material-ui-chip-input": "^0.17.0", 68 | "moment": "^2.18.1", 69 | "react": "^15.6.1", 70 | "react-dom": "^15.4.0", 71 | "react-redux": "^4.4.6", 72 | "react-router": "^3.0.0", 73 | "react-router-redux": "^4.0.7", 74 | "react-tap-event-plugin": "^2.0.1", 75 | "react-tools": "^0.13.3", 76 | "react-transform": "^0.0.3", 77 | "redux": "^3.0.5", 78 | "redux-form": "^6.3.2", 79 | "redux-form-material-ui": "^4.2.0", 80 | "redux-localstorage": "^0.4.1", 81 | "redux-logger": "^2.7.4", 82 | "redux-thunk": "^2.2.0", 83 | "string_score": "^0.1.22" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/SupremeManager.js: -------------------------------------------------------------------------------- 1 | import * as Helpers from './helpers'; 2 | import ProductProcessor from './processors/productProcessor'; 3 | import CartProcessor from './processors/cartProcessor'; 4 | import CheckoutProcessor from './processors/checkoutProcessor'; 5 | import AtcProcessor from './processors/atcProcessor'; 6 | import { notify } from './notification'; 7 | 8 | export default class SupremeManager { 9 | constructor(preferences, sizings, billing) { 10 | this.preferences = preferences; 11 | this.sizings = sizings; 12 | this.billing = billing; 13 | } 14 | 15 | start() { 16 | // Checks for page change by repeatedly checking the current page location and tracking change 17 | (() => { 18 | let currentPage = window.location.href; 19 | setInterval(() => { 20 | if (currentPage !== window.location.href) { 21 | currentPage = window.location.href; 22 | setTimeout(() => this.onPageChange(), 100); 23 | } 24 | }, 50); 25 | this.onPageChange(); 26 | })(); 27 | } 28 | 29 | /** 30 | * This function is called whenever a new page change occurs 31 | */ 32 | async onPageChange() { 33 | SupremeManager.processLinks(); 34 | 35 | // if stores are not configured yet.. 36 | if (!this.isConfigured()) { 37 | notify('Extension not configured', true); 38 | return; 39 | } 40 | Array.prototype.forEach.call(document.getElementsByClassName('sold_out_tag'), x => x.style.display = 'block'); 41 | if (this.preferences.hideSoldOut) { 42 | SupremeManager.hideSoldOutProducts(); 43 | } 44 | 45 | const autoCheckout = this.preferences.autoCheckout; 46 | const autoPay = this.preferences.autoPay; 47 | notify('Auto-checkout ' + (autoCheckout ? 'enabled' : 'disabled') + ', Auto-payment ' + (autoPay ? 'enabled' : 'disabled')); 48 | 49 | if (Helpers.isProductPage()) { 50 | ProductProcessor.start(this.preferences, this.sizings, this.billing); 51 | } else if (Helpers.isCart()) { 52 | CartProcessor.start(this.preferences, this.sizings, this.billing); 53 | } else if (Helpers.isCheckout()) { 54 | CheckoutProcessor.start(this.preferences, this.sizings, this.billing); 55 | } else if (Helpers.isShopCategoryPage()) { 56 | AtcProcessor.start(this.preferences, this.sizings, this.billing); 57 | } 58 | } 59 | 60 | isConfigured() { 61 | return !([this.preferences, this.sizings, this.billing].some(x => x === undefined)); 62 | } 63 | 64 | /** 65 | * Attach an event on product links of the page to reload the page instead of loading in ajax 66 | */ 67 | static processLinks() { 68 | const links = document.links; 69 | document.body.setAttribute('data-no-turbolink', true); 70 | for (const link of links) { 71 | link.addEventListener('click', function (e) { 72 | window.location.href = this.href; 73 | if (!e) { 74 | e = window.event; 75 | } 76 | 77 | if (e.stopPropagation) { 78 | e.stopPropagation(); 79 | } else { 80 | e.cancelBubble = true; 81 | } 82 | }); 83 | } 84 | } 85 | 86 | static hideSoldOutProducts() { 87 | const soldOuts = Array.prototype.filter.call(document.getElementsByTagName('article'), x => x.getElementsByClassName('sold_out_tag').length); 88 | for (let node of soldOuts) { 89 | node.remove(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/helpers.js: -------------------------------------------------------------------------------- 1 | import { notify } from './notification'; 2 | 3 | /** 4 | * Helper timeout function to add a timer in the notification bar 5 | * @param {Function} fn Function to be called after the delay 6 | * @param {Number} ms Delay before calling the function 7 | * @param {String} actionName Optional, an action name that will be displayed in the notification bar 8 | */ 9 | export function timeout(fn, ms, actionName, danger = false) { 10 | const now = new Date(); 11 | let shouldAbort = false; 12 | const currentLocation = document.location.href; 13 | 14 | const interval = setInterval(() => { 15 | if (currentLocation !== document.location.href) { 16 | shouldAbort = true; 17 | clearInterval(interval); 18 | return; 19 | } 20 | const d = new Date(); 21 | const diff = (d.getTime() - now.getTime()); 22 | notify((actionName || 'Action') + ' in ' + ((ms - diff) / 1000), danger); 23 | }, 100); 24 | 25 | setTimeout(() => { 26 | clearInterval(interval); 27 | if (shouldAbort || currentLocation !== document.location.href) { 28 | return; 29 | } 30 | notify('Finished'); 31 | fn(); 32 | }, ms); 33 | } 34 | 35 | export function getQueryStringValue(key) { 36 | const urlParams = new URLSearchParams(window.location.search); 37 | return urlParams.get(key) || undefined; 38 | } 39 | 40 | export function pageHasNodeOfClass(className) { 41 | return document.getElementsByClassName(className).length > 0; 42 | } 43 | 44 | export function hasStringInPath(value) { 45 | return location.pathname.substring(1).split('/').filter(x => !!x && x === value).length > 0; 46 | } 47 | 48 | export function pathCount() { 49 | return location.pathname.substring(1).split('/').length; 50 | } 51 | 52 | export function isShopCategoryPage() { 53 | return hasStringInPath('shop') && hasStringInPath('all') && pathCount() === 3; 54 | } 55 | 56 | /** 57 | * Check if the user is currently on a product page 58 | */ 59 | export function isProductPage() { 60 | return hasStringInPath('shop') && (pageHasNodeOfClass('styles') 61 | || pageHasNodeOfClass('price') 62 | || pageHasNodeOfClass('style')); 63 | } 64 | 65 | /** 66 | * Check if the user is currently on the 'cart' page 67 | */ 68 | export function isCart() { 69 | return pageHasNodeOfClass('cart') && hasStringInPath('cart'); 70 | } 71 | 72 | /** 73 | * Check if the user is currently at the checkout page 74 | */ 75 | export function isCheckout() { 76 | return hasStringInPath('checkout'); 77 | } 78 | 79 | export function getArticleName(articleNode) { 80 | const nameNode = articleNode.querySelector('h1') || articleNode.querySelector('a.nl') || articleNode.querySelector('a'); 81 | return nameNode ? nameNode.innerText.toLowerCase().trim() : null; 82 | } 83 | 84 | export function getArticleColor(articleNode) { 85 | const colorNode = articleNode.querySelector('.sn') || articleNode.querySelector('.nl') || articleNode.querySelector('p .name-link'); 86 | return colorNode ? colorNode.innerText.toLowerCase().trim() : null; 87 | } 88 | 89 | export function updateQueryStringParameter(uri, key, value) { 90 | const re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i'); 91 | const separator = uri.indexOf('?') !== -1 ? '&' : '?'; 92 | if (uri.match(re)) { 93 | return uri.replace(re, `$1${key}=${value}$2`); 94 | } 95 | else { 96 | return `${uri + separator + key}=${value}`; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/Restocks.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SelectField from 'material-ui/SelectField'; 3 | import MenuItem from 'material-ui/MenuItem'; 4 | import { connect } from 'react-redux'; 5 | import { green300, red300 } from 'material-ui/styles/colors'; 6 | import { List, ListItem } from 'material-ui/List'; 7 | import RestockAction from 'material-ui/svg-icons/action/restore'; 8 | import NewAction from 'material-ui/svg-icons/alert/add-alert'; 9 | import moment from 'moment'; 10 | import Avatar from 'material-ui/Avatar'; 11 | import RaisedButton from 'material-ui/RaisedButton'; 12 | import Layout from '../../../../containers/Layout'; 13 | import StorageService from '../../../../../services/StorageService'; 14 | import ChromeService from '../../../../../services/ChromeService'; 15 | import addNotification from '../../../../actions/notification'; 16 | import LocalChangeSelect from '../LocalChangeSelect'; 17 | 18 | 19 | class Restocks extends Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | restocks: [], 24 | }; 25 | 26 | this.updateRestockList(); 27 | 28 | ChromeService.addMessageListener('productRestocked', () => { 29 | this.updateRestockList(); 30 | }); 31 | ChromeService.addMessageListener('productsAdded', () => { 32 | this.updateRestockList(); 33 | }); 34 | } 35 | 36 | updateRestockList() { 37 | StorageService.getItem('restocks').then((restocks) => { 38 | if (restocks) { 39 | this.setState({ restocks }); 40 | } 41 | }); 42 | } 43 | 44 | handleClearAll() { 45 | this.props.notify('Restock list cleared'); 46 | StorageService.setItem('restocks', []).then(() => this.setState({ restocks: [] })); 47 | } 48 | 49 | render() { 50 | if (!this.state.restocks) return (
loading...
); 51 | const items = this.state.restocks.map((x, i) => { 52 | const icon = x.type === 'new' ? : ; 53 | const color = x.type === 'new' ? red300 : green300; 54 | const text = x.type === 'new' ? `${x.product.name} in ${x.product.color} dropped` : `${x.product.name} in ${x.product.color} restocked`; 55 | return ( 56 | window.open(x.product.url)} 58 | key={i} 59 | leftAvatar={} 60 | primaryText={text} 61 | secondaryText={moment(new Date(x.timestamp)).fromNow()} 62 | /> 63 | ); 64 | }); 65 | const style = ChromeService.isPopup() ? { maxWidth: '350px', marginLeft: 'auto', marginRight: 'auto' } : {}; 66 | return ( 67 | 68 | 69 |
70 | { 71 | !items.length &&

No restocks yet

72 | } 73 | {items.length > 0 && ( 74 |
75 |
76 | this.handleClearAll()} primary /> 77 |
78 |
79 | 80 | {items} 81 | 82 |
83 |
84 | )} 85 |
86 | ); 87 | } 88 | } 89 | 90 | function mapDispatchToProps(dispatch) { 91 | return { 92 | notify: msg => dispatch(addNotification(msg)), 93 | }; 94 | } 95 | 96 | export default connect(undefined, mapDispatchToProps)(Restocks); 97 | 98 | -------------------------------------------------------------------------------- /bot/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | const extConfig = { 6 | entry: [ 7 | './src/extension/content/index.js', 8 | ], 9 | output: { 10 | path: __dirname + '/dist', 11 | filename: 'extension.js', 12 | publicPath: '/', 13 | }, 14 | resolve: { 15 | extensions: ['.js', '.jsx'], 16 | alias: { 17 | request: 'browser-request', 18 | }, 19 | }, 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.(js|jsx)$/, 24 | loader: 'babel-loader', 25 | exclude: /node_modules/, 26 | query: { 27 | presets: ['es2015', 'react'], 28 | plugins: [ 29 | 'transform-runtime', 30 | ], 31 | }, 32 | }, 33 | ], 34 | }, 35 | }; 36 | 37 | const backgroundConfig = { 38 | entry: [ 39 | './src/extension/background/index.js', 40 | ], 41 | output: { 42 | path: __dirname + '/dist', 43 | filename: 'background.js', 44 | publicPath: '/', 45 | }, 46 | resolve: { 47 | extensions: ['.js', '.jsx'], 48 | alias: { 49 | request: 'browser-request', 50 | }, 51 | }, 52 | module: { 53 | loaders: [ 54 | { 55 | test: /\.(js|jsx)$/, 56 | loader: 'babel-loader', 57 | exclude: /node_modules/, 58 | query: { 59 | presets: ['es2015', 'react'], 60 | plugins: [ 61 | 'transform-runtime', 62 | ], 63 | }, 64 | }, 65 | ], 66 | }, 67 | }; 68 | 69 | const optionsConfig = { 70 | devtool: '#eval-source-map', 71 | entry: [ 72 | './src/app/index.jsx', 73 | ], 74 | output: { 75 | path: __dirname + '/dist', 76 | filename: 'bundle.js', 77 | publicPath: '/', 78 | }, 79 | devServer: { 80 | historyApiFallback: true, 81 | contentBase: './src/', 82 | hot: true, 83 | }, 84 | plugins: [ 85 | new HtmlWebpackPlugin({ 86 | title: 'Supreme Auto Checkout', 87 | template: __dirname + '/src/app/index.hbs', 88 | }), 89 | new webpack.HotModuleReplacementPlugin(), 90 | new webpack.NoEmitOnErrorsPlugin(), 91 | new CopyWebpackPlugin([ 92 | { from: './src/assets/', to: 'assets' }, 93 | { from: './manifest.json', to: './manifest.json' } 94 | ]), 95 | new webpack.DefinePlugin({ 96 | 'process.env': { 97 | NODE_ENV: JSON.stringify('development'), 98 | }, 99 | }), 100 | ], 101 | resolve: { 102 | extensions: ['.js', '.jsx'], 103 | alias: { 104 | request: 'browser-request', 105 | }, 106 | }, 107 | module: { 108 | loaders: [ 109 | { 110 | test: /\.(jsx|js)$/, 111 | loader: 'babel-loader', 112 | exclude: /node_modules/, 113 | query: { 114 | presets: ['es2015', 'react'], 115 | plugins: [ 116 | 'transform-runtime', 117 | ], 118 | env: { 119 | development: { 120 | presets: ['react-hmre'], 121 | plugins: [ 122 | ['react-transform', { 123 | transforms: [{ 124 | transform: 'react-transform-hmr', 125 | imports: ['react'], 126 | locals: ['module'], 127 | }], 128 | }], 129 | ], 130 | }, 131 | }, 132 | }, 133 | }, 134 | { test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/, loader: 'file' }, 135 | ], 136 | }, 137 | }; 138 | 139 | module.exports = [optionsConfig, extConfig, backgroundConfig]; 140 | -------------------------------------------------------------------------------- /bot/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const webpack = require('webpack'); 5 | 6 | const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ 7 | template: __dirname + '/src/app/index.hbs', 8 | filename: 'index.html', 9 | inject: 'body', 10 | }); 11 | 12 | const CopyWebPackPluginConfig = new CopyWebpackPlugin([ 13 | { from: './src/assets/', to: 'assets' }, 14 | { from: './manifest.json', to: './manifest.json' }, 15 | ]); 16 | 17 | const DefinePlugin = new webpack.DefinePlugin({ 18 | 'process.env': { 19 | NODE_ENV: "'production'", 20 | }, 21 | }); 22 | 23 | const extConfig = { 24 | entry: [ 25 | './src/extension/content/index.js', 26 | ], 27 | output: { 28 | path: path.resolve('build'), 29 | filename: 'extension.js', 30 | publicPath: '/', 31 | }, 32 | resolve: { 33 | extensions: ['.js', '.jsx'], 34 | alias: { 35 | request: 'browser-request', 36 | }, 37 | }, 38 | module: { 39 | loaders: [ 40 | { 41 | test: /\.(js|jsx)$/, 42 | loader: 'babel-loader', 43 | exclude: /node_modules/, 44 | query: { 45 | presets: ['es2015', 'react'], 46 | plugins: [ 47 | 'transform-runtime', 48 | ], 49 | }, 50 | }, 51 | ], 52 | }, 53 | }; 54 | 55 | const preloadConfig = { 56 | entry: [ 57 | './src/preload/index.js', 58 | ], 59 | output: { 60 | path: path.resolve('build'), 61 | filename: 'preload.js', 62 | publicPath: '/', 63 | }, 64 | resolve: { 65 | extensions: ['.js', '.jsx'], 66 | alias: { 67 | request: 'browser-request', 68 | }, 69 | }, 70 | module: { 71 | loaders: [ 72 | { 73 | test: /\.(js|jsx)$/, 74 | loader: 'babel-loader', 75 | exclude: /node_modules/, 76 | query: { 77 | presets: ['es2015', 'react'], 78 | plugins: [ 79 | 'transform-runtime', 80 | ], 81 | }, 82 | }, 83 | ], 84 | }, 85 | }; 86 | 87 | const backgroundConfig = { 88 | entry: [ 89 | './src/extension/background/index.js', 90 | ], 91 | output: { 92 | path: path.resolve('build'), 93 | filename: 'background.js', 94 | publicPath: '/', 95 | }, 96 | resolve: { 97 | extensions: ['.js', '.jsx'], 98 | alias: { 99 | request: 'browser-request', 100 | }, 101 | }, 102 | module: { 103 | loaders: [ 104 | { 105 | test: /\.(js|jsx)$/, 106 | loader: 'babel-loader', 107 | exclude: /node_modules/, 108 | query: { 109 | presets: ['es2015', 'react'], 110 | plugins: [ 111 | 'transform-runtime', 112 | ], 113 | }, 114 | }, 115 | ], 116 | }, 117 | }; 118 | 119 | const optionsConfig = { 120 | entry: './src/app/index.jsx', 121 | output: { 122 | path: path.resolve('build'), 123 | filename: 'bundle.js' 124 | }, 125 | resolve: { 126 | extensions: ['.js', '.jsx'], 127 | alias: { 128 | request: 'browser-request', 129 | }, 130 | }, 131 | module: { 132 | loaders: [ 133 | { 134 | test: /\.(jsx|js)$/, 135 | loader: 'babel-loader', 136 | exclude: /node_modules/, 137 | query: { 138 | presets: ['es2015', 'react'], 139 | plugins: [ 140 | 'transform-runtime', 141 | ], 142 | }, 143 | }, 144 | { test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/, loader: 'file' }, 145 | ], 146 | }, 147 | plugins: [HtmlWebpackPluginConfig, CopyWebPackPluginConfig, DefinePlugin], 148 | }; 149 | 150 | module.exports = [optionsConfig, extConfig, backgroundConfig, preloadConfig]; 151 | -------------------------------------------------------------------------------- /bot/test/keywordmatcher.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | import FuzzyStringMatcher from '../src/app/utils/FuzzyStringMatcher'; 4 | 5 | describe('Keyword matcher tests', () => { 6 | it('should match correct keywords with Polartec', () => { 7 | const testData = ['Hooded Stripe Denim Zip Up Shirt', 'Polartec Pullover Shirt', '100 Dollar Bill Overalls']; 8 | const matcher = new FuzzyStringMatcher(testData); 9 | const result = matcher.search('polartec'); 10 | expect(result).to.have.length(1); 11 | expect(testData[result[0]]).to.equal('Polartec Pullover Shirt'); 12 | }); 13 | 14 | it('should match with invalid keywords', () => { 15 | const testData = ['Hooded Stripe Denim Zip Up Shirt', 'Polartec Pullover Shirt', '100 Dollar Bill Overalls']; 16 | const matcher = new FuzzyStringMatcher(testData); 17 | const result = matcher.search('this doesnt exists polartec'); 18 | expect(result).to.have.length(1); 19 | }); 20 | 21 | it('should match multiple products with the same keywords', () => { 22 | const testData = ['Hooded Stripe Denim Zip Up Shirt', 'Polartec Pullover Shirt', '100 Dollar Bill Overalls']; 23 | const matcher = new FuzzyStringMatcher(testData); 24 | const result = matcher.search('shirt'); 25 | expect(result).to.have.length(2); 26 | }); 27 | 28 | it('shouldnt return a result with non matching keywords', () => { 29 | const testData = ['Hooded Stripe Denim Zip Up Shirt', 'Polartec Pullover Shirt', '100 Dollar Bill Overalls']; 30 | const matcher = new FuzzyStringMatcher(testData); 31 | const result = matcher.search('this doesnt exist'); 32 | expect(result).to.have.length(0); 33 | }); 34 | 35 | it('should match small mistakes in keywords', () => { 36 | const testData = ['Hooded Stripe Denim Zip Up Shirt', 'Polartec Pullover Shirt', '100 Dollar Bill Overalls']; 37 | const matcher = new FuzzyStringMatcher(testData); 38 | const result = matcher.search('polatec'); 39 | expect(result).to.have.length(1); 40 | expect(testData[result[0]]).to.equal('Polartec Pullover Shirt'); 41 | }); 42 | 43 | it('should match the correct category with tops/sweaters', () => { 44 | const category = 'Tops/Sweaters'; 45 | const categories = ['accessories', 't-shirts', 'pants', 'shorts', 'sweatshirts', 'tops-sweaters', 'shirts', 'jackets', 'shoes', 'skate', 'hats', 'bags']; 46 | const matcher = new FuzzyStringMatcher(categories); 47 | const result = matcher.search(category); 48 | expect(result).to.have.length(1); 49 | expect(categories[result[0]]).to.equal('tops-sweaters'); 50 | }); 51 | 52 | it('should work with a list of objects', () => { 53 | const data = [{ name: 'Tops/Sweaters' }, { name: 'accessories' }, { name: 'shirts' }]; 54 | const matcher = new FuzzyStringMatcher(data, { key: 'name' }); 55 | const result = matcher.search('tops_sweaters'); 56 | expect(result).to.have.length(1); 57 | expect(result[0]).to.equal(data[0]); 58 | }); 59 | 60 | it('should work with negative keywords', () => { 61 | const testData = ['Hooded Stripe Denim Zip Up Shirt', 'Polartec Pullover Shirt', '100 Dollar Bill Overalls', 'Hanes Boxer Briefs', 'Hanes Tagless Tees', 'Hanes Socks']; 62 | const matcher = new FuzzyStringMatcher(testData); 63 | const result = matcher.search('hanes !boxer !tee'); 64 | expect(result).to.have.length(1); 65 | expect(testData[result[0]]).to.equal('Hanes Socks'); 66 | }); 67 | 68 | it('should return no result with negative keywords', () => { 69 | const testData = ['Hooded Stripe Denim Zip Up Shirt', 'Polartec Pullover Shirt', '100 Dollar Bill Overalls', 'Hanes Boxer Briefs', 'Hanes Tagless Tees', 'Hanes Socks']; 70 | const matcher = new FuzzyStringMatcher(testData); 71 | const result = matcher.search('hanes !boxer !tee !socks'); 72 | expect(result).to.have.length(0); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/processors/atcProcessor.js: -------------------------------------------------------------------------------- 1 | import BaseProcessor from './baseProcessor'; 2 | import * as Helpers from '../helpers'; 3 | import FuzzyStringMatcher from '../../../../app/utils/FuzzyStringMatcher'; 4 | import AtcService from '../../../../services/supreme/AtcService'; 5 | 6 | export default class AtcProcessor extends BaseProcessor { 7 | static start(preferences, sizings, billing) { 8 | const processor = new AtcProcessor(preferences, sizings, billing); 9 | processor.beginProcess(); 10 | return processor; 11 | } 12 | 13 | beginProcess() { 14 | this.processAtc(); 15 | } 16 | 17 | static findArticles() { 18 | let articles = document.querySelectorAll('.inner-article'); 19 | if (!articles.length) { 20 | articles = document.querySelectorAll('.inner-item'); 21 | } 22 | return [...articles].map(x => ({ 23 | name: Helpers.getArticleName(x), 24 | color: Helpers.getArticleColor(x), 25 | soldOut: !!x.getElementsByClassName('sold_out_tag').length, 26 | url: x.querySelector('a').href, 27 | })); 28 | } 29 | 30 | async handleRetry(atcId, maxRetry, currentRetryCount) { 31 | if (maxRetry === 'inf') { 32 | Helpers.timeout(() => window.location.reload(), 500, 'Product is not available, refreshing...', true); 33 | return; 34 | } 35 | if (!currentRetryCount && maxRetry > 0) { 36 | window.location.href = `${window.location.href}&atc-retry-count=1`; 37 | return; 38 | } else if (currentRetryCount < maxRetry) { 39 | setTimeout(() => { 40 | window.location.href = Helpers.updateQueryStringParameter(window.location.href, 'atc-retry-count', currentRetryCount + 1); 41 | }, 600); 42 | return; 43 | } 44 | const nextProduct = await AtcService.getNextEnabledAtcProduct(atcId); 45 | if (nextProduct) { 46 | window.location.href = AtcService.getAtcUrl(nextProduct, true); 47 | } else { 48 | window.location.href = this.preferences.autoCheckout ? '/checkout' : '/shop/cart'; 49 | } 50 | } 51 | 52 | async processAtc() { 53 | const queryString = Helpers.getQueryStringValue('atc-id'); 54 | const atcRetryCount = Math.abs(Number(Helpers.getQueryStringValue('atc-retry-count'))); 55 | if (!queryString || isNaN(Number(queryString))) { 56 | return; 57 | } 58 | const atcId = Number(queryString); 59 | const atcProduct = await AtcService.getAtcProductById(atcId); 60 | if (!atcProduct) return; 61 | 62 | let match = null; 63 | const keywords = atcProduct.product.keywords; 64 | const kwColor = atcProduct.product.color; 65 | const innerArticles = AtcProcessor.findArticles(); 66 | const fuse = new FuzzyStringMatcher(innerArticles, { key: 'name' }); 67 | const bestMatches = fuse.search(keywords.join(' ')); 68 | const maxRetryCount = atcProduct.product.retryCount; 69 | if (kwColor) { 70 | const fuseColor = new FuzzyStringMatcher(bestMatches, { key: 'color' }); 71 | const matchesColor = fuseColor.search(kwColor); 72 | if (matchesColor.length) { 73 | match = matchesColor[0]; 74 | } 75 | } else if (bestMatches) { 76 | match = bestMatches.find(x => !x.soldOut) || bestMatches[0]; 77 | } 78 | const atcRunAll = Helpers.getQueryStringValue('atc-run-all'); 79 | if (!match) { 80 | if (!isNaN(atcId) && atcRunAll) { 81 | return await this.handleRetry(atcId, maxRetryCount, atcRetryCount); 82 | } 83 | setTimeout(() => { 84 | window.location.reload(); 85 | }, 600); 86 | } else if (match.soldOut) { 87 | if (!isNaN(atcId) && atcRunAll) { 88 | const soldOutAction = atcProduct.product.soldOutAction; 89 | return await this.handleRetry(atcId, soldOutAction, atcRetryCount); 90 | } 91 | setTimeout(() => { 92 | window.location.reload(); 93 | }, 600); 94 | } else { 95 | if (atcRunAll) { 96 | match.url = `${match.url}?atc-run-all=true&atc-id=${atcId}`; 97 | } else { 98 | match.url = `${match.url}?atc-id=${atcId}`; 99 | } 100 | window.location.href = match.url; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/Configuration.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Tab from 'material-ui/Tabs/Tab'; 3 | import { connect } from 'react-redux'; 4 | import FontIcon from 'material-ui/FontIcon'; 5 | import * as menus from '../../../constants/Menus'; 6 | import Billing from './pages/Billing'; 7 | import Options from './pages/Options'; 8 | import Sizes from './pages/Sizes'; 9 | import Layout from '../../../containers/Layout'; 10 | import { changeMenu } from '../../../actions/menu'; 11 | import { updateProfileSettings } from '../../../actions/profiles'; 12 | import * as Styles from '../../../constants/Styles'; 13 | 14 | const SHOP_NAME = 'Supreme'; 15 | 16 | class Supreme extends Component { 17 | getContainerForMenu(menu) { 18 | switch (menu) { 19 | case menus.MENU_BILLING: 20 | return ( this.onSubmit(menu, data)} shop={SHOP_NAME} />); 21 | case menus.MENU_OPTIONS: 22 | return ( this.onSubmit(menu, data)} shop={SHOP_NAME} />); 23 | case menus.MENU_SIZES: 24 | return ( this.onSubmit(menu, data)} shop={SHOP_NAME} />); 25 | default: 26 | return null; 27 | } 28 | } 29 | 30 | strToNumberReducer(menu, key, value) { 31 | // Don't process values for billing 32 | if (typeof value === 'string' && !isNaN(value) && menu !== menus.MENU_BILLING) { 33 | return +(value); 34 | } 35 | if (value instanceof Date) { 36 | return value.toString(); 37 | } 38 | return value; 39 | } 40 | 41 | transform(menu, obj, reducer) { 42 | const keys = Object.keys(obj); 43 | const newObj = {}; 44 | for (let i = 0; i < keys.length; i++) { 45 | const key = keys[i]; 46 | const value = obj[key]; 47 | newObj[key] = reducer(menu, key, value); 48 | } 49 | return newObj; 50 | } 51 | 52 | onSubmit(menu, data) { 53 | const newObj = this.transform(menu, data, this.strToNumberReducer); 54 | this.props.updateSettings(this.props.currentProfile, SHOP_NAME, menu, newObj); 55 | } 56 | 57 | componentWillMount() { 58 | if (this.props.menu === null) { 59 | this.props.changeMenu(Supreme.getDefaultMenu()); 60 | } 61 | } 62 | 63 | getIconForTabMenu(menu) { 64 | const isIncomplete = (!this.props.settings[SHOP_NAME] || !this.props.settings[SHOP_NAME][menu]); 65 | return ({isIncomplete ? 'error' : 'done'}); 66 | } 67 | 68 | static getDefaultMenu() { 69 | return menus.MENU_BILLING; 70 | } 71 | 72 | getTabs() { 73 | return [ 74 | this.props.changeMenu(menus.MENU_BILLING)} 81 | />, 82 | this.props.changeMenu(menus.MENU_OPTIONS)} 88 | />, 89 | this.props.changeMenu(menus.MENU_SIZES)} 95 | />, 96 | ]; 97 | } 98 | 99 | render() { 100 | const { menu } = this.props; 101 | return ( 102 | 103 | { this.getContainerForMenu(menu) } 104 | 105 | ); 106 | } 107 | } 108 | 109 | function mapStateToProps(state) { 110 | const currentProfile = state.profiles.currentProfile; 111 | const settings = state.profiles.profiles.filter(x => x.name === currentProfile)[0].settings; 112 | return { 113 | menu: state.menu.currentMenu, 114 | settings: settings, 115 | currentProfile, 116 | }; 117 | } 118 | 119 | function mapDispatchToProps(dispatch) { 120 | return { 121 | changeMenu: menu => dispatch(changeMenu(menu)), 122 | updateSettings: (currentProfile, shop, key, value) => dispatch(updateProfileSettings(currentProfile, shop, key, value)), 123 | }; 124 | } 125 | 126 | export default connect(mapStateToProps, mapDispatchToProps)(Supreme); 127 | -------------------------------------------------------------------------------- /bot/src/services/supreme/AtcService.js: -------------------------------------------------------------------------------- 1 | import KeywordsService from '../KeywordsService'; 2 | import StorageService from '../StorageService'; 3 | import ProductsService from './ProductsService'; 4 | 5 | export default class AtcService { 6 | static getAtcUrl(atcProduct, runAll = false) { 7 | const category = atcProduct.product.category === 'tops-sweaters' ? 'tops_sweaters' : atcProduct.product.category; 8 | let url = `https://www.supremenewyork.com/shop/all/${category}?atc-id=${atcProduct.id}`; 9 | if (runAll) { 10 | url = `${url}&atc-run-all=true`; 11 | } 12 | return url; 13 | } 14 | 15 | static async getAtcMonitorUrl(atcProduct, runAll = false) { 16 | const productList = await ProductsService.fetchProducts(); 17 | if (!atcProduct || !productList) return null; 18 | 19 | const bestMatch = KeywordsService.findBestMatch(productList, atcProduct.product.keywords, atcProduct.product.category); 20 | const atcColor = atcProduct.product.color || 'any'; 21 | if (bestMatch) { 22 | let url = `https://www.supremenewyork.com/shop/${bestMatch.id}?atc-color=${atcColor}&atc-id=${atcProduct.id}&atc-monitor=true`; 23 | if (runAll) { 24 | url = `${url}&atc-run-all=true`; 25 | } 26 | return url; 27 | } 28 | return null; 29 | } 30 | 31 | static async getNextAtcMonitorProduct(atcId) { 32 | const product = await this.getAtcProductById(atcId); 33 | const monitoredProducts = await ProductsService.fetchProducts(); 34 | if (!monitoredProducts || !product) return null; 35 | 36 | let products = await this.getEnabledAtcProducts(); 37 | products = products.sort((a, b) => a.id - b.id).filter(x => x.id > product.id && x.id !== product.id); 38 | for (let i = 0; i < products.length; i += 1) { 39 | const atcProduct = products[i]; 40 | const bestMatch = KeywordsService.findBestMatch(monitoredProducts, atcProduct.product.keywords, atcProduct.product.category); 41 | if (bestMatch) { 42 | return atcProduct; 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | static async openAtcTab(product, runAll = false) { 49 | if (!product) return false; 50 | const url = this.getAtcUrl(product, runAll); 51 | const win = window.open(url, '_blank'); 52 | win.focus(); 53 | } 54 | 55 | static async openAtcTabById(atcId, runAll = false) { 56 | const product = await this.getAtcProductById(atcId); 57 | return await this.openAtcTab(product, runAll); 58 | } 59 | 60 | static async openAtcTabMonitor(product, runAll = false) { 61 | const url = await this.getAtcMonitorUrl(product, runAll); 62 | if (!product || !url) return false; 63 | 64 | chrome.tabs.create({ url }); 65 | return true; 66 | } 67 | 68 | static async openAtcTabMonitorById(productList, atcId) { 69 | const product = await this.getAtcProductById(atcId); 70 | return this.openAtcTabMonitor(productList, product); 71 | } 72 | 73 | static async runAll() { 74 | const productList = await this.getEnabledAtcProducts(); 75 | const firstProduct = productList.sort((a, b) => a.id - b.id)[0]; 76 | if (!firstProduct) return false; 77 | 78 | return await this.openAtcTab(firstProduct, true); 79 | } 80 | 81 | static async runAllMonitor() { 82 | const enabledAtcProducts = await this.getEnabledAtcProducts(); 83 | const products = enabledAtcProducts.sort((a, b) => a.id - b.id); 84 | if (!enabledAtcProducts) return false; 85 | let found = false; 86 | for (let i = 0; i < products.length; i += 1) { 87 | const product = products[i]; 88 | if (product && await this.openAtcTabMonitor(product, true)) { 89 | found = true; 90 | break; 91 | } 92 | } 93 | return found; 94 | } 95 | 96 | static async getAtcProductById(id) { 97 | const products = await this.getAtcProducts(); 98 | return products.atcProducts.find(x => x.id === id); 99 | } 100 | 101 | static async getNextEnabledAtcProduct(atcId) { 102 | let products = await this.getEnabledAtcProducts(); 103 | products = products.sort((a, b) => a.id - b.id); 104 | return products.find(x => x.id > atcId && x.id !== atcId); 105 | } 106 | 107 | static async getAtcProducts() { 108 | const state = await StorageService.loadSavedState(); 109 | if (state && state.atc) { 110 | return state.atc; 111 | } 112 | return null; 113 | } 114 | 115 | static async getEnabledAtcProducts() { 116 | const products = await this.getAtcProducts(); 117 | return products.atcProducts.filter(x => x.product.enabled); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/ProductList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Divider from 'material-ui/Divider'; 4 | import CircularProgress from 'material-ui/CircularProgress'; 5 | import { Card, CardMedia, CardText } from 'material-ui/Card'; 6 | import TextField from 'material-ui/TextField'; 7 | import Dialog from 'material-ui/Dialog'; 8 | import Layout from '../../../containers/Layout'; 9 | import addNotification from '../../../actions/notification'; 10 | import { addAtcProduct } from '../../../actions/atc'; 11 | import AtcCreateForm from './AtcCreateForm'; 12 | import FuzzyStringMatcher from '../../../utils/FuzzyStringMatcher'; 13 | 14 | class ProductList extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | selectedProduct: null, 19 | modalOpen: false, 20 | filter: '', 21 | }; 22 | } 23 | 24 | handleSubmit(data) { 25 | this.props.addAtcProduct(data); 26 | this.handleRequestClose(); 27 | } 28 | 29 | handleClickProduct(product) { 30 | if (this.props.onProductClick) { 31 | this.props.onProductClick(product); 32 | return; 33 | } 34 | this.setState({ 35 | modalOpen: true, 36 | selectedProduct: product, 37 | }); 38 | } 39 | 40 | handleRequestClose() { 41 | this.setState({ 42 | modalOpen: false, 43 | selectedProduct: null, 44 | }); 45 | } 46 | 47 | productToAtc(product) { 48 | if (!product) return {}; 49 | return { 50 | name: product.name, 51 | keywords: product.keywords, 52 | category: product.category, 53 | }; 54 | } 55 | 56 | getProductCard(product, onTouchTap) { 57 | const style = { 58 | width: 200, 59 | minWidth: 250, 60 | flex: 1, 61 | margin: 20, 62 | }; 63 | 64 | const imgStyle = {}; 65 | 66 | if (product.soldOut) { 67 | imgStyle.filter = 'grayscale(100%)'; 68 | } 69 | 70 | return ( 71 | 72 |
73 |

74 | {product.name} 75 | {product.color &&

{product.color}

} 76 |

77 | 78 | {product.name} 79 | 80 | 81 | {product.price &&

Est price: {product.price}

} 82 |
83 |
84 | 85 |
86 | ); 87 | } 88 | 89 | render() { 90 | const products = this.props.products; 91 | if (!products) { 92 | return ( 93 | 94 |
95 |

Loading...

96 | 97 |
98 |
99 | ); 100 | } 101 | let allProducts = [...products]; 102 | if (this.state.filter) { 103 | const fuse = new FuzzyStringMatcher(allProducts, { key: 'name' }); 104 | allProducts = fuse.search(this.state.filter); 105 | } 106 | allProducts = allProducts.sort((a, b) => a.soldOut - b.soldOut); 107 | const cards = allProducts.map(x => this.getProductCard(x, () => this.handleClickProduct(x))); 108 | const style = { 109 | display: 'flex', 110 | flexDirection: 'row', 111 | justifyContent: 'space-evenly', 112 | flexWrap: 'wrap', 113 | cursor: 'pointer', 114 | }; 115 | return ( 116 | 117 | this.handleRequestClose()} 120 | open={this.state.modalOpen} 121 | autoScrollBodyContent 122 | > 123 | this.handleRequestClose()} onSubmit={data => this.handleSubmit(data)} initialValues={this.productToAtc(this.state.selectedProduct)} /> 124 | 125 |
126 | {this.props.title ||

Click on a product to add to AutoCop

} 127 |
128 |
129 | this.setState({ filter: val })} 133 | /> 134 |
135 |
136 | {cards} 137 |
138 |
139 | ); 140 | } 141 | } 142 | 143 | ProductList.propTypes = { 144 | products: PropTypes.arrayOf(PropTypes.object), 145 | onProductClick: PropTypes.func, 146 | title: PropTypes.string, 147 | }; 148 | 149 | function mapDispatchToProps(dispatch) { 150 | return { 151 | addAtcProduct: data => dispatch(addAtcProduct(data)), 152 | notify: msg => dispatch(addNotification(msg)), 153 | }; 154 | } 155 | 156 | export default connect(undefined, mapDispatchToProps)(ProductList); 157 | -------------------------------------------------------------------------------- /bot/src/app/constants/Utils.js: -------------------------------------------------------------------------------- 1 | export const countries = [{"text":"USA","value":"USA"},{"text":"CANADA","value":"CANADA"},{"text":"JAPAN","value":"JAPAN"},{"text":"UK","value":"GB"},{"text":"UK (N. IRELAND)","value":"NB"},{"text":"AUSTRIA","value":"AT"},{"text":"BELARUS","value":"BY"},{"text":"BELGIUM","value":"BE"},{"text":"BULGARIA","value":"BG"},{"text":"CROATIA","value":"HR"},{"text":"CZECH REPUBLIC","value":"CZ"},{"text":"DENMARK","value":"DK"},{"text":"ESTONIA","value":"EE"},{"text":"FINLAND","value":"FI"},{"text":"FRANCE","value":"FR"},{"text":"GERMANY","value":"DE"},{"text":"GREECE","value":"GR"},{"text":"HUNGARY","value":"HU"},{"text":"ICELAND","value":"IS"},{"text":"IRELAND","value":"IE"},{"text":"ITALY","value":"IT"},{"text":"LATVIA","value":"LV"},{"text":"LITHUANIA","value":"LT"},{"text":"LUXEMBOURG","value":"LU"},{"text":"MONACO","value":"MC"},{"text":"NETHERLANDS","value":"NL"},{"text":"NORWAY","value":"NO"},{"text":"POLAND","value":"PL"},{"text":"PORTUGAL","value":"PT"},{"text":"ROMANIA","value":"RO"},{"text":"RUSSIA","value":"RU"},{"text":"SLOVAKIA","value":"SK"},{"text":"SLOVENIA","value":"SI"},{"text":"SPAIN","value":"ES"},{"text":"SWEDEN","value":"SE"},{"text":"SWITZERLAND","value":"CH"},{"text":"TURKEY","value":"TR"}]; 2 | export const sizes = ["Any", "Small", "Medium", "Large", "XLarge"]; 3 | export const sizesPants = ["Any", "Small", "Medium", "Large", "XLarge", 28, 30, 32, 34, 36, 38, 40]; 4 | export const shoeSizes = ["Any", 4, 4.5, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5]; 5 | export const skateSizes = ["Any", 7.5, 7.7, 7.75, 7.8, 7.875, 8, 8.06, 8.125, 8.13, 8.18, 8.2, 8.25, 8.375, 8.38, 8.5, 8.6, 8.625, 8.75, 8.8, 9, 9.25, 9.5, 10, 10]; 6 | export const hatsSizes = ["Any", "7 1/4", "7 3/8", "7 1/2", "7 5/8", "7 3/4", "8"]; 7 | export const usaRegions = [{"text":"AL","value":"AL"},{"text":"AK","value":"AK"},{"text":"AS","value":"AS"},{"text":"AZ","value":"AZ"},{"text":"AR","value":"AR"},{"text":"CA","value":"CA"},{"text":"CO","value":"CO"},{"text":"CT","value":"CT"},{"text":"DE","value":"DE"},{"text":"DC","value":"DC"},{"text":"FM","value":"FM"},{"text":"FL","value":"FL"},{"text":"GA","value":"GA"},{"text":"GU","value":"GU"},{"text":"HI","value":"HI"},{"text":"ID","value":"ID"},{"text":"IL","value":"IL"},{"text":"IN","value":"IN"},{"text":"IA","value":"IA"},{"text":"KS","value":"KS"},{"text":"KY","value":"KY"},{"text":"LA","value":"LA"},{"text":"ME","value":"ME"},{"text":"MH","value":"MH"},{"text":"MD","value":"MD"},{"text":"MA","value":"MA"},{"text":"MI","value":"MI"},{"text":"MN","value":"MN"},{"text":"MS","value":"MS"},{"text":"MO","value":"MO"},{"text":"MT","value":"MT"},{"text":"NE","value":"NE"},{"text":"NV","value":"NV"},{"text":"NH","value":"NH"},{"text":"NJ","value":"NJ"},{"text":"NM","value":"NM"},{"text":"NY","value":"NY"},{"text":"NC","value":"NC"},{"text":"ND","value":"ND"},{"text":"MP","value":"MP"},{"text":"OH","value":"OH"},{"text":"OK","value":"OK"},{"text":"OR","value":"OR"},{"text":"PW","value":"PW"},{"text":"PA","value":"PA"},{"text":"PR","value":"PR"},{"text":"RI","value":"RI"},{"text":"SC","value":"SC"},{"text":"SD","value":"SD"},{"text":"TN","value":"TN"},{"text":"TX","value":"TX"},{"text":"UT","value":"UT"},{"text":"VT","value":"VT"},{"text":"VI","value":"VI"},{"text":"VA","value":"VA"},{"text":"WA","value":"WA"},{"text":"WV","value":"WV"},{"text":"WI","value":"WI"},{"text":"WY","value":"WY"}]; 8 | export const canadaRegions = [{"text":"AB","value":"AB"},{"text":"BC","value":"BC"},{"text":"MB","value":"MB"},{"text":"NB","value":"NB"},{"text":"NL","value":"NL"},{"text":"NT","value":"NT"},{"text":"NS","value":"NS"},{"text":"NU","value":"NU"},{"text":"ON","value":"ON"},{"text":"PE","value":"PE"},{"text":"QC","value":"QC"},{"text":"SK","value":"SK"},{"text":"YT","value":"YT"}]; 9 | export const japanRegions = [{"text":" 北海道","value":" 北海道"},{"text":" 青森県","value":" 青森県"},{"text":" 岩手県","value":" 岩手県"},{"text":" 宮城県","value":" 宮城県"},{"text":" 秋田県","value":" 秋田県"},{"text":" 山形県","value":" 山形県"},{"text":" 福島県","value":" 福島県"},{"text":" 茨城県","value":" 茨城県"},{"text":" 栃木県","value":" 栃木県"},{"text":" 群馬県","value":" 群馬県"},{"text":" 埼玉県","value":" 埼玉県"},{"text":" 千葉県","value":" 千葉県"},{"text":" 東京都","value":" 東京都"},{"text":" 神奈川県","value":" 神奈川県"},{"text":" 新潟県","value":" 新潟県"},{"text":" 富山県","value":" 富山県"},{"text":" 石川県","value":" 石川県"},{"text":" 福井県","value":" 福井県"},{"text":" 山梨県","value":" 山梨県"},{"text":" 長野県","value":" 長野県"},{"text":" 岐阜県","value":" 岐阜県"},{"text":" 静岡県","value":" 静岡県"},{"text":" 愛知県","value":" 愛知県"},{"text":" 三重県","value":" 三重県"},{"text":" 滋賀県","value":" 滋賀県"},{"text":" 京都府","value":" 京都府"},{"text":" 大阪府","value":" 大阪府"},{"text":" 兵庫県","value":" 兵庫県"},{"text":" 奈良県","value":" 奈良県"},{"text":" 和歌山県","value":" 和歌山県"},{"text":" 鳥取県","value":" 鳥取県"},{"text":" 島根県","value":" 島根県"},{"text":" 岡山県","value":" 岡山県"},{"text":" 広島県","value":" 広島県"},{"text":" 山口県","value":" 山口県"},{"text":" 徳島県","value":" 徳島県"},{"text":" 香川県","value":" 香川県"},{"text":" 愛媛県","value":" 愛媛県"},{"text":" 高知県","value":" 高知県"},{"text":" 福岡県","value":" 福岡県"},{"text":" 佐賀県","value":" 佐賀県"},{"text":" 長崎県","value":" 長崎県"},{"text":" 熊本県","value":" 熊本県"},{"text":" 大分県","value":" 大分県"},{"text":" 宮崎県","value":" 宮崎県"},{"text":" 鹿児島県","value":" 鹿児島県"},{"text":" 沖縄県","value":" 沖縄県"}]; 10 | export const categories = ['accessories', 't-shirts', 'pants', 'shorts', 'sweatshirts', 'tops-sweaters', 'shirts', 'jackets', 'shoes', 'skate', 'hats', 'bags']; 11 | export const creditCards = [{ text: 'Visa', value: 'visa' }, { text: 'American Express', value: 'american_express' }, { text: 'Mastercard', value: 'master' }, { text: 'Solo', value: 'solo'}, { text: 'PayPal', value: 'paypal'}]; 12 | export const usCreditCards = creditCards.filter(x => x.value !== 'paypal'); 13 | export const japanCreditCards = creditCards.concat([ { text: 'JCB', value: 'jcb' }, { text: '代金引換', value: 'cod' }]).filter(x => x.value !== 'solo'); 14 | -------------------------------------------------------------------------------- /bot/src/app/containers/AppDrawer.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import { List, ListItem, makeSelectable } from 'material-ui/List'; 5 | import Drawer from 'material-ui/Drawer'; 6 | import Subheader from 'material-ui/Subheader'; 7 | import ShopIcon from 'material-ui/svg-icons/action/shop'; 8 | import SettingsIcon from 'material-ui/svg-icons/action/add-shopping-cart'; 9 | import CartIcon from 'material-ui/svg-icons/action/settings'; 10 | import ShoppingCartIcon from 'material-ui/svg-icons/action/shopping-cart'; 11 | import AlarmIcon from 'material-ui/svg-icons/action/alarm'; 12 | import ListIcon from 'material-ui/svg-icons/action/view-list'; 13 | import AccountIcon from 'material-ui/svg-icons/action/account-circle'; 14 | import PaymentIcon from 'material-ui/svg-icons/action/payment'; 15 | import NewIcon from 'material-ui/svg-icons/av/new-releases'; 16 | import IncompleteIcon from 'material-ui/svg-icons/alert/error'; 17 | import Styles from '../constants/Styles'; 18 | import * as Menus from '../constants/Menus'; 19 | import version from '../version'; 20 | 21 | const SelectableList = makeSelectable(List); 22 | 23 | function openUrlInNewTab(url) { 24 | const win = window.open(url, '_blank'); 25 | win.focus(); 26 | } 27 | 28 | function isIncomplete(settings, shopName) { 29 | const menus = Object.keys(Menus).map(x => Menus[x]).filter(x => x !== 'AutoCop' && x !== 'Products'); 30 | return !settings[shopName] || menus.some(x => settings[shopName][x] === undefined); 31 | } 32 | 33 | function getIconForShop(settings, shopName) { 34 | if (isIncomplete(settings, shopName)) { 35 | return ; 36 | } 37 | return ; 38 | } 39 | 40 | class AppDrawer extends Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { 44 | supremeMenuOpen: true, 45 | }; 46 | } 47 | 48 | toggleSupremeMenu() { 49 | this.setState({ 50 | supremeMenuOpen: !this.state.supremeMenuOpen, 51 | }); 52 | } 53 | 54 | render() { 55 | const { location, currentProfile, settings, open } = this.props; 56 | const paths = location.pathname.split('/').filter(x => !!x); 57 | const currentPage = paths[0]; 58 | const subPage = paths[1]; 59 | const page = subPage ? `${currentPage}/${subPage}` : currentPage; 60 | return ( 61 | 62 |
openUrlInNewTab('https://github.com/val92130/Supreme-Auto-Checkout')}> 63 | Supreme Auto Checkout 64 | { version } 65 |
66 | 67 | Shops 68 | } 72 | leftIcon={getIconForShop(settings, 'Supreme')} 73 | open={this.state.supremeMenuOpen} 74 | onTouchTap={() => this.toggleSupremeMenu()} 75 | onNestedListToggle={() => this.toggleSupremeMenu()} 76 | nestedItems={[ 77 | } 80 | value="supreme/configuration" 81 | primaryText="Configuration" 82 | leftIcon={isIncomplete(settings, 'Supreme') ? : } 83 | />, 84 | } 88 | primaryText="AutoCop" 89 | leftIcon={} 90 | />, 91 | } 95 | primaryText="Product listing" 96 | leftIcon={} 97 | />, 98 | } 102 | primaryText="Drops" 103 | leftIcon={} 104 | />, 105 | } 109 | primaryText="Restocks" 110 | leftIcon={} 111 | />, 112 | ]} 113 | /> 114 | Other 115 | } 119 | leftIcon={} 120 | /> 121 | openUrlInNewTab('https://www.paypal.me/vchatelain')} 125 | leftIcon={} 126 | /> 127 | NEW! AIO Bot} 130 | onTouchTap={() => openUrlInNewTab('https://rocketcop.io')} 131 | leftIcon={} 132 | /> 133 | 134 |
135 | ); 136 | } 137 | } 138 | 139 | AppDrawer.propTypes = { 140 | location: PropTypes.shape({ 141 | pathname: PropTypes.string.isRequired, 142 | }), 143 | currentProfile: PropTypes.string, 144 | open: PropTypes.bool.isRequired, 145 | }; 146 | 147 | function mapStateToProps(state) { 148 | const currentProfile = state.profiles.currentProfile; 149 | return { 150 | settings: state.profiles.profiles.filter(x => x.name === currentProfile)[0].settings, 151 | currentProfile, 152 | }; 153 | } 154 | 155 | export default connect(mapStateToProps)(AppDrawer); 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Supreme Auto Checkout 2 | ===================== 3 | 4 | Supreme Auto Checkout is a Google Chrome extension to automate the process of buying a product from the Supreme shop. 5 | 6 | ![Bot](https://github.com/val92130/Supreme-Auto-Checkout/blob/develop/screenshot.jpg "Bot") 7 | 8 | # NEW 9 | 10 | Check out [RocketCop](https://rocketcop.io), our new AIO bot! 11 | 12 | Features of RocketCop includes: 13 | - Faster checkout 14 | - Browser simulation mode 15 | - Run multiple tasks at the same time 16 | - Captcha support 17 | - Proxy support 18 | - ... 19 | 20 | # Requirements 21 | - Chrome for desktop release 55 22 | 23 | # Features 24 | 25 | Supreme Auto Checkout provides many features such as: 26 | - **Auto Checkout** - The product will automatically be added to your cart and will straight go to the checkout page. 27 | - **AutoFill** - All of your billing informations will be automaticaly filled on the checkout page. 28 | - **Multi profiles** - You can setup multiple profiles to quickly switch settings during drop day! 29 | - **Configurable delays for every steps of the checkout process.** 30 | - **Size choice for every category of product.** 31 | - **Option to set a minimum/maximum price for a product.** 32 | - **Option to hide sold out products.** 33 | - **Easy configuration.** 34 | - **AutoCop** - You can set keywords for products and they will automatically be added to cart when they are available on the shop. 35 | - **Product Monitor** - Be notified when a product goes back in stock! 36 | - **Profile Export** - Export your profiles and securely import them onto another computer (Your data will be encrypted using AES). 37 | - **Drop list** 38 | 39 | # Coming soon 40 | Features that will be added in the future: 41 | - **Proxies support** 42 | 43 | # Installation 44 | 45 | You can download the bot on the Chrome Web Store [here](https://chrome.google.com/webstore/detail/supreme-auto-checkout-bot/lokkgfofiabinmcohpdalkcmfjpepkjb?hl=fr&gl=FR). 46 | 47 | Or you can also download the latest release [here](https://github.com/val92130/Supreme-Auto-Checkout/releases). 48 | 49 | Once it's done, follow these steps: 50 | >- Extract the downloaded release to a folder of your choice. 51 | >- Open Google Chrome and type in your address bar **chrome://extensions/** and enable the **Developer** mode option at the top of the page. 52 | >- Drag the extracted folder containing the extension into Google Chrome. 53 | 54 | # Configuration 55 | 56 | After the extension is installed, the extension logo should appear on the Chrome menu. 57 | 58 | Make sure to configure all the required options otherwise the bot won't load. 59 | 60 | It is also recommended to clear your cookies on the Supreme shop before using the bot. 61 | 62 | # Usage 63 | 64 | After you have configured the extension, go to the Supreme website. If the extension is installed correctly you should see a message on the top of the webpage stating that the bot is running. 65 | 66 | If you have a message stating that the bot is not configured, check that every required option is filled in the bot configuration page. 67 | 68 | Once on a product page, if **AutoCheckout** is enabled, the bot will automatically try to add to your cart your desired size for the product category. 69 | If you have enabled the **Strict size choice** option, the product won't be added to your cart if your desired size isn't available, otherwise, it will try to add the first available size for the product. 70 | 71 | If you have set the **Minimum Price** or **Maximum Price** option, the product won't be added to your cart if the price is higher or lower than what you set. 72 | 73 | If **Auto Payment** is enabled, the bot will automatically submit the checkout form once you're on the checkout page, otherwise, your billing infos will still be automatically filled but you will need to manually click on the checkout button. 74 | 75 | # AutoCop 76 | 77 | Autocop is a feature to automatically add to cart products who matches some specific keywords. 78 | 79 | You **MUST** keep your browser open for this feature to work. 80 | 81 | To set it up, you first must enable **AutoCheckout** and **Enable AutoCop** in the Options tab. 82 | 83 | You'll then have to setup an **ATC Start Time**, which would most likely be the time when the online shop updates (11AM). 84 | 85 | You will have to set the time as a 24hour format (hh:mm:ss), note that it will be based on your system's time. 86 | 87 | A recommended value would be **11:00:10** to make sure that the shop has updated before running the ATC. 88 | 89 | Once the ATC time is reached, the bot will automatically open a tab to add to cart matching products. 90 | 91 | The product who matches the most the keywords will be added to the cart. 92 | 93 | ![Atc](https://github.com/val92130/Supreme-Auto-Checkout/blob/develop/atc.gif "Atc") 94 | 95 | # Setting up AutoCop products 96 | 97 | To setup AutoCop products, you need to go in the **AutoCop** tab of your bot then click on the **Add new** button. 98 | 99 | This will open a form requesting the following informations: 100 | - **Name** - A name to distinct your atc products, not really important. 101 | - **Product keywords** - This field is a multi-selectable input, you can add keywords by pressing the Enter key, 102 | thoses keywords are **case insensitive** and white-spaces at the end and start of the keyword will be removed. 103 | - **Color** (optional) - If set, the ATC will try to checkout the product matching this color, also **case insensitive**. 104 | - **Product category** - The category of the product. 105 | - **Retry count if not found** - If the product is not found during an Autocop, the page will tryo to find the product again for N times before skipping to the next Autocop product. 106 | - **Enabled** 107 | 108 | You can manually trigger the AutoCop for a product by clicking on the `Run now` button in the Atc product list. 109 | 110 | # Development 111 | 112 | Requirements: 113 | - node.js 7+ 114 | - yarn 115 | 116 | First, you'll need to install the npm dependencies: 117 | ```bash 118 | $ yarn 119 | ``` 120 | 121 | To build and watch for changes run the following command: 122 | ```bash 123 | $ yarn watch 124 | ``` 125 | 126 | To build the project, run: 127 | ```bash 128 | $ yarn build 129 | ``` 130 | 131 | Those commands will create a `build` folder containing the packaged application. 132 | 133 | To run unit tests: 134 | ```bash 135 | $ yarn test 136 | ``` 137 | 138 | ## Donation 139 | This project will always be supported for free but any donation would be greatly appreciated! 140 | 141 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/vchatelain) 142 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/Billing.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { reduxForm, Field, formValueSelector } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | import Divider from 'material-ui/Divider'; 7 | import { 8 | SelectField, 9 | TextField, 10 | } from 'redux-form-material-ui'; 11 | import Styles from '../../../../constants/Styles'; 12 | import * as Utils from '../../../../constants/Utils'; 13 | import * as Validators from '../../../../utils/FormValidators'; 14 | import * as menus from '../../../../constants/Menus'; 15 | 16 | function getStatesForCountry(country) { 17 | switch (country) { 18 | case 'USA': 19 | return Utils.usaRegions; 20 | case 'JAPAN': 21 | return Utils.japanRegions; 22 | case 'CANADA': 23 | return Utils.canadaRegions; 24 | default: 25 | return []; 26 | } 27 | } 28 | 29 | function getCreditCardsForCountry(country) { 30 | switch (country) { 31 | case 'CANADA': 32 | case 'USA': 33 | return Utils.usCreditCards; 34 | case 'JAPAN': 35 | return Utils.japanCreditCards; 36 | default: 37 | return Utils.creditCards; 38 | } 39 | } 40 | 41 | const Billing = props => { 42 | const { handleSubmit, pristine, submitting, country } = props; 43 | return ( 44 |
45 |

Configure your billing infos

46 | 47 |
48 |
49 | 57 |
58 | 59 |
60 | 68 |
69 | 70 |
71 | 78 |
79 | 80 |
81 | 88 |
89 | 90 |
91 | 98 |
99 | 100 |
101 | 108 |
109 | 110 |
111 | 118 | { 119 | Utils.countries.map(x => ) 120 | } 121 | 122 |
123 | { 124 | ['JAPAN', 'USA', 'CANADA'].indexOf(country) !== -1 && 125 | 132 | { 133 | getStatesForCountry(country).map(x => ) 134 | } 135 | 136 | } 137 | 138 |
139 | 145 |
146 | 147 |
148 | 154 | { 155 | getCreditCardsForCountry(country).map(x => 156 | , 157 | ) 158 | } 159 | 160 |
161 | 162 |
163 | 170 |
171 | 172 |
173 | 180 | { 181 | Array.apply(null, new Array(12)).map((x, i) => { 182 | const month = ++i < 10 ? `0${i}` : i; 183 | return ; 184 | }) 185 | } 186 | 187 |
188 | 189 |
190 | 197 | { 198 | Array.apply(null, new Array(10)).map((x, i) => { 199 | const year = new Date().getFullYear() + i; 200 | return ; 201 | }) 202 | } 203 | 204 |
205 | 206 |
207 | 214 |
215 |
216 | 221 |
222 |
223 |
224 | ); 225 | }; 226 | 227 | const BillingForm = reduxForm({ 228 | form: 'billing', 229 | })(Billing); 230 | 231 | Billing.propTypes = { 232 | shop: PropTypes.string.isRequired, 233 | }; 234 | const selector = formValueSelector('billing'); 235 | 236 | function mapStateToProps(state, ownProps) { 237 | const currentProfile = state.profiles.currentProfile; 238 | const settings = state.profiles.profiles.filter(x => x.name === currentProfile)[0].settings; 239 | return { 240 | initialValues: (settings[ownProps.shop] || {})[menus.MENU_BILLING] || {}, 241 | country: selector(state, 'order_billing_country'), 242 | }; 243 | } 244 | 245 | export default connect(mapStateToProps)(BillingForm); 246 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/Sizes.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { reduxForm, Field, change } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | import Divider from 'material-ui/Divider'; 7 | import { 8 | SelectField, 9 | } from 'redux-form-material-ui'; 10 | import Styles from '../../../../constants/Styles'; 11 | import * as Utils from '../../../../constants/Utils'; 12 | import * as Validators from '../../../../utils/FormValidators'; 13 | import * as menus from '../../../../constants/Menus'; 14 | 15 | class Sizes extends Component { 16 | handleSetAny() { 17 | for (let field of ['accessories', 't-shirts', 'pants', 'shorts', 'sweatshirts', 'tops-sweaters', 'shirts', 'jackets', 'shoes', 'skate', 'hats']) { 18 | this.props.changeFieldValue(field, 'Any'); 19 | } 20 | } 21 | 22 | render() { 23 | const { handleSubmit, pristine, submitting } = this.props; 24 | return ( 25 |
26 |

Configure your desired sizes

27 |
28 | this.handleSetAny()} 30 | label="Use any size for all" 31 | /> 32 |
33 | 34 |
35 |
36 | 44 | { 45 | Utils.sizes.map(x => ) 46 | } 47 | 48 |
49 | 50 |
51 | 59 | { 60 | Utils.sizes.map(x => ) 61 | } 62 | 63 |
64 | 65 |
66 | 74 | { 75 | Utils.sizesPants.map(x => ) 76 | } 77 | 78 |
79 | 80 |
81 | 89 | { 90 | Utils.sizesPants.map(x => ) 91 | } 92 | 93 |
94 | 95 |
96 | 104 | { 105 | Utils.sizes.map(x => ) 106 | } 107 | 108 |
109 | 110 |
111 | 119 | { 120 | Utils.sizes.map(x => ) 121 | } 122 | 123 |
124 | 125 |
126 | 134 | { 135 | Utils.sizes.map(x => ) 136 | } 137 | 138 |
139 | 140 |
141 | 149 | { 150 | Utils.sizes.map(x => ) 151 | } 152 | 153 |
154 | 155 |
156 | 164 | { 165 | Utils.shoeSizes.map(x => ) 166 | } 167 | 168 |
169 | 170 |
171 | 179 | { 180 | Utils.skateSizes.map(x => ) 181 | } 182 | 183 |
184 | 185 |
186 | 194 | { 195 | Utils.hatsSizes.map(x => ) 196 | } 197 | 198 |
199 |
200 | 205 |
206 |
207 |
208 | ); 209 | } 210 | 211 | } 212 | 213 | Sizes.propTypes = { 214 | shop: PropTypes.string.isRequired, 215 | }; 216 | 217 | function mapStateToProps(state, ownProps) { 218 | const currentProfile = state.profiles.currentProfile; 219 | const settings = state.profiles.profiles.filter(x => x.name === currentProfile)[0].settings; 220 | return { 221 | form: 'sizings-form', 222 | initialValues: (settings[ownProps.shop] || {})[menus.MENU_SIZES] || {}, 223 | }; 224 | } 225 | 226 | function mapDispatchToProps(dispatch) { 227 | return { 228 | changeFieldValue: (field, value) => dispatch(change('sizings-form', field, value)), 229 | }; 230 | } 231 | 232 | export default connect(mapStateToProps, mapDispatchToProps)(reduxForm({ 233 | form: 'sizes', 234 | })(Sizes)); 235 | -------------------------------------------------------------------------------- /bot/src/app/components/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | Table, 5 | TableBody, 6 | TableHeader, 7 | TableHeaderColumn, 8 | TableRow, 9 | TableRowColumn, 10 | } from 'material-ui/Table'; 11 | import { red300 } from 'material-ui/styles/colors'; 12 | import Dialog from 'material-ui/Dialog'; 13 | import IconButton from 'material-ui/IconButton'; 14 | import DeleteButton from 'material-ui/svg-icons/action/delete'; 15 | import DownloadButton from 'material-ui/svg-icons/action/backup'; 16 | import DuplicateButton from 'material-ui/svg-icons/content/content-copy'; 17 | import RaisedButton from 'material-ui/RaisedButton'; 18 | import { createProfile, setProfileEnabled, removeProfile } from '../actions/profiles'; 19 | import addNotification from '../actions/notification'; 20 | import Layout from '../containers/Layout'; 21 | import ProfileCreateForm from './ProfileCreateForm'; 22 | import ProfileExportForm from './ProfileExportForm'; 23 | import ProfileImportForm from './ProfileImportForm'; 24 | import ProfileToggle from './ProfileToggle'; 25 | import CryptoService from '../../services/CryptoService'; 26 | import ChromeService from '../../services/ChromeService'; 27 | import { slugify } from '../utils/Helpers'; 28 | 29 | class Profile extends Component { 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | createModalOpen: false, 34 | exportModalOpen: false, 35 | importModalOpen: false, 36 | exportingProfile: null, 37 | }; 38 | } 39 | 40 | onSetProfile(profileName) { 41 | this.props.setProfileEnabled(profileName); 42 | } 43 | 44 | requestCloseModal() { 45 | this.setState({ 46 | createModalOpen: false, 47 | }); 48 | } 49 | 50 | requestModalOpen() { 51 | this.setState({ 52 | createModalOpen: true, 53 | }); 54 | } 55 | 56 | requestExportModalOpen(profile) { 57 | this.setState({ 58 | exportModalOpen: true, 59 | exportingProfile: profile, 60 | }); 61 | } 62 | 63 | requestCloseExportModal() { 64 | this.setState({ 65 | exportModalOpen: false, 66 | exportingProfile: null, 67 | }); 68 | } 69 | 70 | requestImportModalOpen() { 71 | if (ChromeService.isPopup()) { 72 | ChromeService.openOptionsPage('profiles'); 73 | return; 74 | } 75 | this.setState({ 76 | importModalOpen: true, 77 | }); 78 | } 79 | 80 | requestCloseImportModal() { 81 | this.setState({ 82 | importModalOpen: false, 83 | }); 84 | } 85 | 86 | onRequestDeleteProfile(name) { 87 | this.props.removeProfile(name); 88 | } 89 | 90 | handleSubmit(data) { 91 | this.props.createProfile(data.name, data.description); 92 | this.requestCloseModal(); 93 | } 94 | 95 | handleExport(data) { 96 | this.exportProfile(data.password, data.name); 97 | this.requestCloseExportModal(); 98 | } 99 | 100 | async handleImport(data) { 101 | const { file, password } = data; 102 | const { notify, profiles } = this.props; 103 | try { 104 | const promise = new Promise((resolve, reject) => { 105 | const reader = new FileReader(); 106 | reader.onload = e => resolve(e.target.result); 107 | reader.onerror = reject; 108 | reader.readAsText(file); 109 | }); 110 | const result = await promise; 111 | const decrypted = CryptoService.decrypt(result, password); 112 | const json = JSON.parse(decrypted); 113 | if (!json.name || !json.profile) throw new Error('Malformed json data'); 114 | const existingNames = profiles.filter(x => x.name === json.name); 115 | if (existingNames.length) { 116 | json.name = `${json.name}(${existingNames.length})`; 117 | } 118 | this.props.createProfile(json.name, json.profile.description, json.profile.settings); 119 | notify('Profile successfully imported'); 120 | } catch (e) { 121 | console.error(e); 122 | notify('Error while importing profile, corrupted data or invalid password'); 123 | } finally { 124 | this.requestCloseImportModal(); 125 | } 126 | } 127 | 128 | exportProfile(password, name) { 129 | const { exportingProfile } = this.state; 130 | const data = { 131 | name, 132 | profile: exportingProfile, 133 | }; 134 | const result = CryptoService.encrypt(JSON.stringify(data), password); 135 | // Save as file 136 | const url = `data:application/json,${result}`; 137 | chrome.downloads.download({ 138 | url, 139 | filename: slugify(name) + '.pfl', 140 | }); 141 | } 142 | 143 | duplicateProfile(profile) { 144 | const { notify, profiles, createProfile } = this.props; 145 | let profileName = `${profile.name} - copy`; 146 | const existingNames = profiles.filter(x => x.name === profileName); 147 | if (existingNames.length) { 148 | profileName = `${profileName}(${existingNames.length})`; 149 | } 150 | createProfile(profileName, profile.description, profile.settings); 151 | notify('Profile duplicated'); 152 | } 153 | 154 | render() { 155 | const { profiles } = this.props; 156 | 157 | return ( 158 | 159 | this.requestCloseModal()} 164 | > 165 | this.handleSubmit(data)} 167 | onRequestClose={() => this.requestCloseModal()} 168 | /> 169 | 170 | this.requestCloseExportModal()} 175 | > 176 | this.handleExport(data)} 178 | onRequestClose={() => this.requestCloseExportModal()} 179 | /> 180 | 181 | this.requestCloseImportModal()} 186 | > 187 | await this.handleImport(data)} 189 | onRequestClose={() => this.requestCloseImportModal()} 190 | /> 191 | 192 |
193 |

Create, import or export profiles to manage your different configurations.

194 |

Note: AutoCop products are independent of profiles.

195 | this.requestModalOpen()} primary /> 196 | this.requestImportModalOpen()} /> 197 |
198 | 199 | 200 | 201 | Name 202 | {!ChromeService.isPopup() && Description} 203 | Enabled 204 | Export 205 | Duplicate 206 | Delete 207 | 208 | 209 | 210 | { 211 | profiles.map((x, i) => { 212 | return ( 213 | 214 | {x.name} 215 | {!ChromeService.isPopup() && {x.description}} 216 | 217 | 218 | 219 | 220 | this.requestExportModalOpen(x)}> 221 | 222 | 223 | 224 | 225 | this.duplicateProfile(x)}> 226 | 227 | 228 | 229 | 230 | this.onRequestDeleteProfile(x.name)} disabled={x.name === 'default'}> 231 | 232 | 233 | 234 | 235 | ); 236 | }) 237 | } 238 | 239 |
240 |
241 | ); 242 | } 243 | } 244 | 245 | function mapStateToProps(state) { 246 | return { 247 | profiles: state.profiles.profiles, 248 | currentProfile: state.profiles.currentProfile, 249 | }; 250 | } 251 | 252 | function mapDispatchToProps(dispatch) { 253 | return { 254 | createProfile: (name, description, settings) => dispatch(createProfile(name, description, settings)), 255 | setProfileEnabled: name => dispatch(setProfileEnabled(name)), 256 | removeProfile: name => dispatch(removeProfile(name)), 257 | notify: e => dispatch(addNotification(e)), 258 | }; 259 | } 260 | 261 | export default connect(mapStateToProps, mapDispatchToProps)(Profile); 262 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/Atc.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { 4 | Table, 5 | TableBody, 6 | TableHeader, 7 | TableHeaderColumn, 8 | TableRow, 9 | TableRowColumn, 10 | } from 'material-ui/Table'; 11 | import RaisedButton from 'material-ui/RaisedButton'; 12 | import Divider from 'material-ui/Divider'; 13 | import { red300 } from 'material-ui/styles/colors'; 14 | import Dialog from 'material-ui/Dialog'; 15 | import IconButton from 'material-ui/IconButton'; 16 | import EditIcon from 'material-ui/svg-icons/image/edit'; 17 | import DeleteButton from 'material-ui/svg-icons/action/delete'; 18 | import Toggle from 'material-ui/Toggle'; 19 | import LaunchIcon from 'material-ui/svg-icons/action/launch'; 20 | import Layout from '../../../../containers/Layout'; 21 | import { addAtcProduct, removeAtcProduct, setAtcProductEnabled, editAtcProduct } from '../../../../actions/atc'; 22 | import AtcCreateForm from '../AtcCreateForm'; 23 | import StorageService from '../../../../../services/StorageService'; 24 | import AtcService from '../../../../../services/supreme/AtcService'; 25 | import ProductsService from '../../../../../services/supreme/ProductsService'; 26 | import version from '../../../../version'; 27 | import addNotification from '../../../../actions/notification'; 28 | import * as Helpers from '../../../../utils/Helpers'; 29 | import { updateSupremeCookies } from '../../../../utils/SupremeUtils'; 30 | 31 | class Atc extends Component { 32 | constructor(props) { 33 | super(props); 34 | const interval = setInterval(() => this.updateTimer(), 500); 35 | this.state = { 36 | createModalOpen: false, 37 | editingAtc: null, 38 | remainingTimeAtc: null, 39 | interval: interval, 40 | }; 41 | } 42 | 43 | updateTimer() { 44 | if (!this.props.atcStartTime || this.state.createModalOpen) return; 45 | const now = new Date().getTime(); 46 | 47 | const time = Helpers.timeToDate(this.props.atcStartTime); 48 | const currDate = new Date(this.props.atcStartDate); 49 | currDate.setHours(time.getHours()); 50 | currDate.setMinutes(time.getMinutes()); 51 | currDate.setSeconds(time.getSeconds()); 52 | 53 | const distance = currDate - now; 54 | // Time calculations for days, hours, minutes and seconds 55 | const days = Math.floor(distance / (1000 * 60 * 60 * 24)); 56 | const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); 57 | const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); 58 | const seconds = Math.floor((distance % (1000 * 60)) / 1000); 59 | this.setState({ 60 | remainingTimeAtc: `${days}d ${hours}h ${minutes}m ${seconds}s`, 61 | }); 62 | } 63 | 64 | componentWillUnmount() { 65 | if (this.state.interval) { 66 | clearInterval(this.state.interval); 67 | } 68 | } 69 | 70 | requestCloseModal() { 71 | this.setState({ 72 | createModalOpen: false, 73 | editingAtc: null, 74 | }); 75 | } 76 | 77 | requestModalOpen(editingAtc = null) { 78 | this.setState({ 79 | createModalOpen: true, 80 | editingAtc, 81 | }); 82 | } 83 | 84 | onRequestDeleteAtc(atcName) { 85 | this.props.removeAtcProduct(atcName); 86 | } 87 | 88 | handleSubmit(data) { 89 | if (this.state.editingAtc) { 90 | this.props.editAtcProduct(this.state.editingAtc.name, data); 91 | } else { 92 | this.props.addAtcProduct(data); 93 | } 94 | this.requestCloseModal(); 95 | } 96 | 97 | toggleAtc(name, enabled) { 98 | this.props.setAtcProductEnabled(name, enabled); 99 | } 100 | 101 | async runAll() { 102 | const profile = await StorageService.getCurrentProfileSettings(version); 103 | if (!profile || !profile.Supreme) { 104 | this.props.notify('Please configure your bot before running ATC'); 105 | return false; 106 | } 107 | 108 | await updateSupremeCookies(profile.Supreme.Billing); 109 | const useMonitor = profile.Supreme.Options.atcUseMonitor; 110 | if (!useMonitor) { 111 | return await AtcService.runAll(); 112 | } 113 | const hasFound = await AtcService.runAllMonitor(); 114 | if (!hasFound) { 115 | this.props.notify('No matching product found'); 116 | } 117 | return hasFound; 118 | } 119 | 120 | async runNow(atcProduct) { 121 | const profile = await StorageService.getCurrentProfileSettings(version); 122 | if (!profile || !profile.Supreme) { 123 | this.props.notify('Please configure your bot before running ATC'); 124 | return false; 125 | } 126 | 127 | await updateSupremeCookies(profile.Supreme.Billing); 128 | const useMonitor = profile.Supreme.Options.atcUseMonitor; 129 | if (!useMonitor) { 130 | return await AtcService.openAtcTab(atcProduct); 131 | } 132 | const hasFound = await AtcService.openAtcTabMonitor(atcProduct); 133 | if (!hasFound) { 134 | this.props.notify('No matching product found'); 135 | } 136 | return hasFound; 137 | } 138 | 139 | render() { 140 | const { atcProducts } = this.props; 141 | const isEditing = this.state.editingAtc !== null; 142 | const title = isEditing ? `Edit ${this.state.editingAtc.name}` : 'Add a new product'; 143 | return ( 144 | 145 | this.requestCloseModal()} 150 | autoScrollBodyContent 151 | > 152 | this.requestCloseModal()} onSubmit={data => this.handleSubmit(data)} initialValues={this.state.editingAtc} editing={isEditing} /> 153 | 154 |
155 |

Each product you add will be automatically added to your cart by the AutoCop once the timer reaches its end.

156 |

Click 'Run now' to manually trigger AutoCop for a single product

157 |

Autocop status: {this.props.atcEnabled ? 'ENABLED' : 'DISABLED'}

158 | { this.state.remainingTimeAtc && 159 |

Autocop starting in: {this.state.remainingTimeAtc}

160 | } 161 | this.requestModalOpen()} primary /> 162 | this.runAll()} /> 163 |
164 | 165 | 166 | 167 | 168 | Name 169 | Enabled 170 | Run now 171 | Edit 172 | Delete 173 | 174 | 175 | 176 | { 177 | (() => { 178 | if (!atcProducts || !atcProducts.length) { 179 | return (

Click "Add new" to add a new Autocop Product"

); 180 | } 181 | return atcProducts.map((x, i) => { 182 | const product = x.product; 183 | return ( 184 | 185 | {product.name} 186 | 187 | await this.toggleAtc(product.name, !product.enabled)} /> 188 | 189 | 190 | await this.runNow(x)}> 191 | 192 | 193 | 194 | 195 | this.requestModalOpen(product)}> 196 | 197 | 198 | 199 | 200 | this.onRequestDeleteAtc(product.name)}> 201 | 202 | 203 | 204 | 205 | ); 206 | }); 207 | })() 208 | } 209 |
210 |
211 |
212 | ); 213 | } 214 | } 215 | 216 | function mapStateToProps(state) { 217 | const currentProfile = state.profiles.currentProfile; 218 | const settings = state.profiles.profiles.filter(x => x.name === currentProfile)[0].settings; 219 | let props = { 220 | atcProducts: state.atc.atcProducts.sort((a, b) => a.id - b.id), 221 | }; 222 | if (settings && settings.Supreme && settings.Supreme.Options) { 223 | props = Object.assign({}, props, { 224 | atcEnabled: settings.Supreme.Options.atcEnabled, 225 | atcStartTime: settings.Supreme.Options.atcStartTime, 226 | atcStartDate: settings.Supreme.Options.atcStartDate, 227 | }); 228 | } 229 | return props; 230 | } 231 | 232 | function mapDispatchToProps(dispatch) { 233 | return { 234 | addAtcProduct: data => dispatch(addAtcProduct(data)), 235 | editAtcProduct: (name, data) => dispatch(editAtcProduct(name, data)), 236 | removeAtcProduct: data => dispatch(removeAtcProduct(data)), 237 | setAtcProductEnabled: (name, enabled) => dispatch(setAtcProductEnabled(name, enabled)), 238 | notify: msg => dispatch(addNotification(msg)), 239 | }; 240 | } 241 | 242 | export default connect(mapStateToProps, mapDispatchToProps)(Atc); 243 | -------------------------------------------------------------------------------- /bot/src/services/supreme/CheckoutService.js: -------------------------------------------------------------------------------- 1 | const ignoredIds = ['g-recaptcha-response', 'number_v', 'order_billing_address_3']; 2 | 3 | export default class CheckoutService{ 4 | static async processFields(inputs, settings, delay) { 5 | const successes = await Promise.all(inputs.map(x => this.processField(x, settings, delay))); 6 | await this.cleanup(inputs, settings); 7 | return successes.every(x => x === true); 8 | } 9 | 10 | static async processField(input, settings, delay) { 11 | const id = input.id; 12 | if (ignoredIds.indexOf(id) !== -1) { 13 | return true; 14 | } 15 | if (typeof (settings[id]) !== 'undefined') { 16 | await this.setInputValue(input, settings[id], delay); 17 | return true; 18 | } 19 | return this.processUnknownField(input, settings); 20 | } 21 | 22 | static type(input, value, delay){ 23 | return new Promise((resolve) => { 24 | (function writer(i){ 25 | if(value.length <= i++){ 26 | input.value = value; 27 | resolve(); 28 | return; 29 | } 30 | input.value = value.substring(0,i); 31 | setTimeout(function(){writer(i);}, delay/100 - 10); 32 | })(0) 33 | }) 34 | } 35 | 36 | static async setInputValue(input, value, delay, dispatchEvent=true) { 37 | if (value === undefined) return input; 38 | 39 | input.type === 'select-one' ? input.value = value : await this.type(input, value, delay); 40 | 41 | if (dispatchEvent) { 42 | input.dispatchEvent(new Event('change')); 43 | } 44 | return input; 45 | } 46 | 47 | static async processUnknownField(input, settings) { 48 | const splittedName = settings['order_billing_name'].split(' '); 49 | if (input.name === 'credit_card[last_name]') { 50 | await this.setInputValue(input, splittedName[0]); 51 | return true; 52 | } 53 | if (input.name === 'credit_card[first_name]') { 54 | await this.setInputValue(input, splittedName[1]); 55 | return true; 56 | } 57 | if (input.name === 'order[billing_name]') { 58 | await this.setInputValue(input, settings['order_billing_name']); 59 | return true; 60 | } 61 | 62 | if (input.name === 'order[email]') { 63 | await this.setInputValue(input, settings['order_email']); 64 | return true; 65 | } 66 | 67 | if (input.name === 'order[tel]') { 68 | await this.setInputValue(input, settings['order_tel']); 69 | return true; 70 | } 71 | 72 | if (input.name === 'order[billing_address]') { 73 | await this.setInputValue(input, settings['bo']); 74 | return true; 75 | } 76 | 77 | if (input.name === 'order[billing_address_2]') { 78 | await this.setInputValue(input, settings['oba3']); 79 | return true; 80 | } 81 | 82 | if (input.name === 'order[billing_city]') { 83 | await this.setInputValue(input, settings['order_billing_city']); 84 | return true; 85 | } 86 | 87 | if (input.name === 'order[billing_zip]') { 88 | await this.setInputValue(input, settings['order_billing_zip']); 89 | return true; 90 | } 91 | 92 | if (input.name === 'order[billing_state]') { 93 | await this.setInputValue(input, settings['order_billing_state']); 94 | return true; 95 | } 96 | 97 | if (input.name === 'order[billing_country]') { 98 | await this.setInputValue(input, settings['order_billing_country']); 99 | return true; 100 | } 101 | 102 | if (input.name === 'credit_card[type]') { 103 | await this.setInputValue(input, settings['credit_card_type']); 104 | return true; 105 | } 106 | 107 | if (input.name === 'credit_card[month]') { 108 | await this.setInputValue(input, settings['credit_card_month']); 109 | return true; 110 | } 111 | 112 | if (input.name === 'credit_card[year]') { 113 | await this.setInputValue(input, settings['credit_card_year']); 114 | return true; 115 | } 116 | 117 | if (input.name === 'credit_card[nlb]' || input.name === 'credit_card[cnb]') { 118 | await this.setInputValue(input, settings['cnb']); 119 | return true; 120 | } 121 | 122 | if (input.name === 'credit_card[rvv]' || input.name === 'credit_card[vval]') { 123 | await this.setInputValue(input, settings['vval']); 124 | return true; 125 | } 126 | 127 | return this.processByLabel(input, settings); 128 | } 129 | 130 | static async processByLabel(input, settings) { 131 | let parent = input.parentNode; 132 | let label = parent.getElementsByTagName('label')[0] || parent.querySelector('.sr-label'); 133 | if (!label) { 134 | const children = parent.childNodes; 135 | for (let i = 0; i < children.length; i += 1) { 136 | const hasClass = [...children[i].classList].filter(x => x.toLowerCase().indexOf('label') !== -1).length > 0; 137 | if (hasClass) { 138 | label = children[i]; 139 | break; 140 | } 141 | } 142 | } 143 | if (!label) return false; 144 | 145 | const text = label.innerText; 146 | if (!text) { 147 | return; 148 | } 149 | 150 | const inArray = function(txt, arr) { 151 | return arr.map(x => x.toLowerCase()).indexOf(txt.toLowerCase()) !== -1; 152 | }; 153 | 154 | const hasText = arr => inArray(text, arr); 155 | 156 | if (hasText(['nom', 'name', 'firstname', 'lastname', 'nom', 'prenom'])) { 157 | await this.setInputValue(input, settings['order_billing_name']); 158 | return true; 159 | } 160 | if (hasText(['email', 'Eメール'])) { 161 | await this.setInputValue(input, settings['order_email']); 162 | return true; 163 | } 164 | if (hasText(['tel', 'phone', 'phone number', '電話番号'])) { 165 | await this.setInputValue(input, settings['order_tel']); 166 | return true; 167 | } 168 | if (hasText(['address', 'adresse', 'addresse', '住所'])) { 169 | await this.setInputValue(input, settings['bo']); 170 | return true; 171 | } 172 | if (hasText(['address 2'])) { 173 | await this.setInputValue(input, settings['oba3']); 174 | return true; 175 | } 176 | if (hasText(['city', 'ville', '区市町村'])) { 177 | await this.setInputValue(input, settings['order_billing_city']); 178 | return true; 179 | } 180 | if (hasText(['zip', 'code postal', 'codepostal', 'code_postal', 'postal code', 'postalcode', '郵便番号'])) { 181 | await this.setInputValue(input, settings['order_billing_zip']); 182 | return true; 183 | } 184 | if (hasText(['country', 'pays'])) { 185 | await this.setInputValue(input, settings['order_billing_country']); 186 | return true; 187 | } 188 | if (hasText(['state', 'état', 'etat', 'province', '都道府県'])) { 189 | await this.setInputValue(input, settings['order_billing_state']); 190 | return true; 191 | } 192 | if (hasText(['type', 'type de carte', 'credit card type', '支払い方法'])) { 193 | await this.setInputValue(input, settings['credit_card_type']); 194 | return true; 195 | } 196 | if (hasText(['numéro', 'number', 'numero', 'カード番号'])) { 197 | await this.setInputValue(input, settings['cnb']); 198 | return true; 199 | } 200 | if (hasText(['exp. date', 'exp date', 'expiry date', 'date d’exp.', 'date d\'exp.', 'date d\'expiration', '有効期限'])) { 201 | if (input.type === 'select-one') { 202 | const isMonth = input.options && input.options[0] && input.options[0].value[0] === '0'; 203 | await this.setInputValue(input, settings[isMonth ? 'credit_card_month' : 'credit_card_year']); 204 | return true; 205 | } 206 | } 207 | if (hasText(['CVV', 'CVV番号'])) { 208 | await this.setInputValue(input, settings['vval']); 209 | return true; 210 | } 211 | return this.processByName(input, settings); 212 | } 213 | 214 | static async processByName(input, settings) { 215 | if (Object.keys(settings).indexOf(input.name) !== -1) { 216 | await this.setInputValue(input, settings[input.name]); 217 | return true; 218 | } 219 | return this.processByParent(input, settings); 220 | } 221 | 222 | static async processByParent(input, settings) { 223 | const parent = input.parentNode; 224 | if (!parent) { 225 | return false; 226 | } 227 | const classList = [...parent.classList]; 228 | if (classList.indexOf('credit_card_verification_value') !== -1) { 229 | await this.setInputValue(input, settings['vval']); 230 | return true; 231 | } 232 | 233 | if (classList.indexOf('credit_card_number') !== -1) { 234 | await this.setInputValue(input, settings['cnb']); 235 | return true; 236 | } 237 | 238 | // probably a card detail input 239 | if(parent.parentNode && parent.parentNode.id === 'card_details') { 240 | // either month/year of expiry date 241 | if (input.type === 'select-one') { 242 | const isMonth = input.options && input.options[0] && input.options[0].value[0] === '0'; 243 | await this.setInputValue(input, settings[isMonth ? 'credit_card_month' : 'credit_card_year']); 244 | return true; 245 | } 246 | 247 | // probably cvv 248 | if (input.attributes['maxlength']) { 249 | await this.setInputValue(input, settings['vval']); 250 | return true; 251 | } 252 | 253 | // otherwise its probably cc number 254 | await this.setInputValue(input, settings['cnb']); 255 | } 256 | } 257 | 258 | static async cleanup(inputs, settings) { 259 | const input = document.getElementById('order_billing_state') || document.querySelector('[name="order[billing_state]"]'); 260 | if (input) { 261 | await this.setInputValue(input, settings['order_billing_state'], false); 262 | return true; 263 | } 264 | const stateLabel = document.querySelector('#state_label'); 265 | if (stateLabel && stateLabel.parentNode) { 266 | const stateSelect = stateLabel.parentNode.querySelector('select'); 267 | if (stateLabel) { 268 | await this.setInputValue(stateSelect, settings['order_billing_state'], false); 269 | return true; 270 | } 271 | } 272 | 273 | const stateInputParent = document.querySelector('.order_billing_state'); 274 | if (stateInputParent) { 275 | const selector = stateInputParent.querySelector('select'); 276 | if (selector) { 277 | await this.setInputValue(selector, settings['order_billing_state'], false); 278 | return true; 279 | } 280 | } 281 | return false; 282 | } 283 | 284 | } 285 | -------------------------------------------------------------------------------- /bot/src/extension/content/supreme/processors/productProcessor.js: -------------------------------------------------------------------------------- 1 | import { notify } from '../notification'; 2 | import * as Helpers from '../helpers'; 3 | import BaseProcessor from './baseProcessor'; 4 | import FuzzyStringMatcher from '../../../../app/utils/FuzzyStringMatcher'; 5 | import AtcService from '../../../../services/supreme/AtcService'; 6 | 7 | export default class ProductProcessor extends BaseProcessor { 8 | static start(preferences, sizings, billing) { 9 | const processor = new ProductProcessor(preferences, sizings, billing); 10 | processor.beginProcess(); 11 | return processor; 12 | } 13 | 14 | beginProcess() { 15 | this.processProduct(); 16 | } 17 | 18 | /** 19 | * Check if the current product is sold out 20 | * @return {Boolean} 21 | */ 22 | static isSoldOut() { 23 | return document.querySelector('input[name=commit]') === null; 24 | } 25 | 26 | /** 27 | * Returns the product category when the user is on a product page 28 | */ 29 | static getProductCategory() { 30 | const category = Helpers.getQueryStringValue('atc-category'); 31 | return !category ? location.pathname.substring(1).split('/')[1] : category; 32 | } 33 | 34 | static sizeMatch(sA, sB, category) { 35 | const sizeA = sA.toString().toLowerCase(); 36 | const sizeB = sB.toString().toLowerCase(); 37 | if (!sizeB || !sizeA) return false; 38 | 39 | if (sizeA === sizeB) { 40 | return true; 41 | } 42 | 43 | if (category === 'shoes') { 44 | // Match sizes like UK10/US10.5' 45 | const a = sizeA.split(/(?:\/)+/); 46 | const b = sizeB.split(/(?:\/)+/); 47 | 48 | if (a.some(x => b.indexOf(x) !== -1)) { 49 | return true; 50 | } 51 | return a[0].replace(/\D/g, '') === b[0].replace(/\D/g, ''); 52 | } 53 | 54 | if (!isNaN(sizeA) || !isNaN(sizeB)) return false; 55 | 56 | // Match sizes like 'S/M'; 57 | const splitA = sizeA.split('/'); 58 | const splitB = sizeB.split('/'); 59 | 60 | return splitA.some(x => sizeB[0] === x[0]) || splitB.some(x => sizeA[0] === x[0]); 61 | } 62 | 63 | /** 64 | * Return the available sizes for the current product 65 | * @return {Array} 66 | */ 67 | static getSizesOptions() { 68 | const sizes = document.getElementById('size') || document.querySelector('[name=size]') || (document.querySelector('form.add').querySelector('select')); 69 | if (!sizes || !sizes.options) { 70 | return []; 71 | } 72 | return [...sizes.options]; 73 | } 74 | 75 | static getAvailableColors() { 76 | const colors = Array.from(document.querySelectorAll('[data-style-name]')).map(x => ({ 77 | name: x.attributes['data-style-name'].value, 78 | node: x 79 | })); 80 | const data = []; 81 | // remove dups 82 | for (let i = 0; i < colors.length; i += 1) { 83 | if (!data.find(x => x.name === colors[i].name)) { 84 | data.push(colors[i]); 85 | } 86 | } 87 | return data; 88 | } 89 | 90 | trySelectProductSize(desiredSize = null) { 91 | const productCategory = ProductProcessor.getProductCategory(); 92 | const sizesOptions = ProductProcessor.getSizesOptions(); 93 | 94 | // If sizes options are available 95 | if (sizesOptions.length) { 96 | let categorySize = desiredSize || this.sizings[productCategory]; 97 | if (categorySize === undefined) { 98 | notify(`Unknown category "${productCategory}", cannot process`, true); 99 | return false; 100 | } 101 | let targetOption = sizesOptions.find(x => ProductProcessor.sizeMatch(categorySize, x.text, productCategory)); 102 | 103 | if (!targetOption) { 104 | if (this.preferences.strictSize && categorySize !== 'Any') { 105 | notify('Size not available or sold out', true); 106 | return false; 107 | } 108 | targetOption = sizesOptions[0]; 109 | } 110 | targetOption.selected = true; 111 | } 112 | return true; 113 | } 114 | 115 | isPriceInRange() { 116 | const maxPrice = this.preferences.maxPrice; 117 | const minPrice = this.preferences.minPrice; 118 | const itemPrice = document.querySelector('[itemprop=price]'); 119 | 120 | if (itemPrice === null) return false; 121 | const price = +(itemPrice.innerHTML.replace(/\D/g, '')); 122 | if (isNaN(price)) return false; 123 | 124 | if (maxPrice !== undefined && price > maxPrice) { 125 | notify('Product price is too high, cancelling', true); 126 | return false; 127 | } 128 | 129 | if (minPrice !== undefined && price < minPrice) { 130 | notify('Product price is too low, cancelling', true); 131 | return false; 132 | } 133 | return true; 134 | } 135 | 136 | addToCart(nextUrl) { 137 | const atcDelay = this.preferences.addToCartDelay; 138 | const forms = document.querySelectorAll('form'); 139 | const form = forms[forms.length - 1]; 140 | const submitBtn = document.querySelector(`#${form.id} [name="commit"]`); 141 | 142 | Helpers.timeout(() => { 143 | const process = () => { 144 | if (document.querySelector('.in-cart') && document.getElementById('cart')) { 145 | if (nextUrl) { 146 | window.location.href = nextUrl; 147 | Helpers.timeout(() => window.location.href = nextUrl, 200, 'Going to next step...'); 148 | return false; 149 | } 150 | } else { 151 | submitBtn.click(); 152 | Helpers.timeout(() => process(), 500, 'Waiting for product to be in cart...'); 153 | } 154 | }; 155 | 156 | process(); 157 | }, atcDelay, 'Adding to cart'); 158 | } 159 | 160 | static async handleRetry(maxRetry, currentRetryCount, nextUrl = null) { 161 | if (maxRetry === 'inf') { 162 | Helpers.timeout(() => window.location.reload(), 500, 'Product is not available, refreshing...', true); 163 | return; 164 | } 165 | if (!currentRetryCount && maxRetry > 0) { 166 | window.location.href = `${window.location.href}&atc-retry-count=1`; 167 | return; 168 | } else if (currentRetryCount < maxRetry) { 169 | setTimeout(() => { 170 | window.location.href = Helpers.updateQueryStringParameter(window.location.href, 'atc-retry-count', currentRetryCount + 1); 171 | }, 600); 172 | return; 173 | } 174 | if (nextUrl) { 175 | window.location.href = nextUrl; 176 | Helpers.timeout(() => window.location.href = nextUrl, 500, 'Product is not available, going to next atc product...', true); 177 | } 178 | } 179 | 180 | async processProductMonitor() { 181 | const atcRunAll = Helpers.getQueryStringValue('atc-run-all'); 182 | const atcId = Number(Helpers.getQueryStringValue('atc-id')); 183 | const atcProduct = await AtcService.getAtcProductById(atcId); 184 | const atcColor = Helpers.getQueryStringValue('atc-color'); 185 | const atcRetryCount = Math.abs(Number(Helpers.getQueryStringValue('atc-retry-count'))); 186 | 187 | if (!atcProduct) return notify('Invalid product id'); 188 | 189 | let nextUrl = this.preferences.autoCheckout ? '/checkout' : '/shop/cart'; 190 | if (atcRunAll) { 191 | const nextProduct = await AtcService.getNextAtcMonitorProduct(atcId); 192 | if (nextProduct) { 193 | nextUrl = await AtcService.getAtcMonitorUrl(nextProduct, true); 194 | } 195 | } 196 | 197 | const colors = ProductProcessor.getAvailableColors(); 198 | const firstAvailableColor = colors.find(x => { 199 | const soldOutAttr = x.node.attributes['data-sold-out']; 200 | if (soldOutAttr) { 201 | return soldOutAttr.value === 'false'; 202 | } 203 | return true; 204 | }); 205 | if (atcColor) { 206 | if (firstAvailableColor && atcColor === 'any') { 207 | if (atcRunAll) { 208 | firstAvailableColor.node.href = `${firstAvailableColor.node.href}?atc-id=${atcId}&atc-run-all=true&atc-monitor=true`; 209 | } else { 210 | firstAvailableColor.node.href = `${firstAvailableColor.node.href}?atc-id=${atcId}&atc-monitor=true`; 211 | } 212 | firstAvailableColor.node.click(); 213 | return false; 214 | } 215 | const fuse = new FuzzyStringMatcher(colors, {key: 'name'}); 216 | const matches = fuse.search(atcColor); 217 | if (matches.length) { 218 | if (atcRunAll) { 219 | window.location.href = `${matches[0].node.href}?atc-id=${atcId}&atc-run-all=true&atc-monitor=true`; 220 | } else { 221 | window.location.href = `${matches[0].node.href}?atc-id=${atcId}`; 222 | } 223 | return false; 224 | } 225 | } 226 | if (ProductProcessor.isSoldOut()) { 227 | const soldOutRetryCount = atcProduct.product.soldOutAction; 228 | return ProductProcessor.handleRetry(soldOutRetryCount, atcRetryCount, nextUrl); 229 | } 230 | 231 | const hasSelectedSize = this.trySelectProductSize(atcProduct.product.size); 232 | if (!this.isPriceInRange() || !hasSelectedSize) { 233 | if (nextUrl) { 234 | window.location.href = nextUrl; 235 | Helpers.timeout(() => window.location.href = nextUrl, 500, 'Continuing to next step...', true); 236 | return false; 237 | } 238 | } 239 | 240 | this.addToCart(nextUrl); 241 | return true; 242 | } 243 | 244 | /** 245 | * This function should be called when the user is on a product page, it will 246 | * try to figure out if the product is sold out or not, and if not, it will find the best available size 247 | * based on the user's preferences and then it will add the item to cart 248 | */ 249 | async processProduct() { 250 | const atcRunAll = Helpers.getQueryStringValue('atc-run-all'); 251 | const atcId = Number(Helpers.getQueryStringValue('atc-id')); 252 | const atcMonitor = Helpers.getQueryStringValue('atc-monitor'); 253 | const atcRetryCount = Math.abs(Number(Helpers.getQueryStringValue('atc-retry-count'))); 254 | 255 | if (isNaN(atcId) && !this.preferences.autoCheckout) return false; 256 | 257 | if (!isNaN(atcId) && atcMonitor) { 258 | return await this.processProductMonitor(); 259 | } 260 | let nextUrl = null; 261 | if (atcRunAll) { 262 | nextUrl = this.preferences.autoCheckout ? '/checkout' : '/shop/cart'; 263 | } 264 | if (!isNaN(atcId) && atcRunAll) { 265 | const nextProduct = await AtcService.getNextEnabledAtcProduct(atcId); 266 | if (nextProduct) { 267 | nextUrl = AtcService.getAtcUrl(nextProduct, true); 268 | } 269 | } 270 | 271 | if (ProductProcessor.isSoldOut()) { 272 | if (!isNaN(atcId)) { 273 | const atcProduct = await AtcService.getAtcProductById(atcId); 274 | const soldOutRetryCount = atcProduct.product.soldOutAction; 275 | return ProductProcessor.handleRetry(soldOutRetryCount, atcRetryCount, nextUrl); 276 | } 277 | return false; 278 | } 279 | 280 | let desiredSize = null; 281 | if (atcId) { 282 | const atcProduct = await AtcService.getAtcProductById(atcId); 283 | desiredSize = atcProduct.product.size; 284 | } 285 | 286 | const hasSelectedSize = this.trySelectProductSize(desiredSize); 287 | if (!this.isPriceInRange() || !hasSelectedSize) { 288 | if (nextUrl) { 289 | window.location.href = nextUrl; 290 | Helpers.timeout(() => window.location.href = nextUrl, 500, 'Continuing to next step...', true); 291 | } 292 | return false; 293 | } 294 | 295 | this.addToCart(nextUrl || (this.preferences.autoCheckout ? '/checkout' : '/shop/cart')); 296 | return true; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/pages/Options.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { reduxForm, Field, formValueSelector } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import IconButton from 'material-ui/IconButton'; 6 | import IconHelp from 'material-ui/svg-icons/action/help'; 7 | import Dialog from 'material-ui/Dialog'; 8 | import Divider from 'material-ui/Divider'; 9 | import { 10 | TextField, 11 | Toggle, 12 | DatePicker, 13 | SelectField, 14 | } from 'redux-form-material-ui'; 15 | import MenuItem from 'material-ui/MenuItem'; 16 | import Styles from '../../../../constants/Styles'; 17 | import * as Validators from '../../../../utils/FormValidators'; 18 | import * as menus from '../../../../constants/Menus'; 19 | import * as SupremeUtils from '../../../../utils/SupremeUtils'; 20 | 21 | const defaultValues = { 22 | autoCheckout: false, 23 | autoPay: false, 24 | strictSize: true, 25 | hideSoldOut: false, 26 | addToCartDelay: 200, 27 | checkoutDelay: 2000, 28 | showNotifications: false, 29 | disableImages: false, 30 | }; 31 | 32 | class HelperField extends Component { 33 | constructor(props) { 34 | super(props); 35 | this.state = { 36 | open: false, 37 | }; 38 | } 39 | 40 | setOpen(val) { 41 | this.setState({ open: val }); 42 | } 43 | 44 | render() { 45 | const { field, title, helperText } = this.props; 46 | return ( 47 |
48 | this.setOpen(false)} 53 | > 54 | { helperText } 55 | 56 |
57 | this.setOpen(true)} style={{ paddingLeft: 0 }} iconStyle={{ height: 16, width: 16 }}>{title} 58 | { field } 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | const Options = props => { 66 | const { handleSubmit, pristine, submitting, atcEnabled } = props; 67 | return ( 68 |
69 |

Configure options and features of the bot

70 | 71 |
72 |
73 | 80 | } 81 | title="Enable Auto Checkout" 82 | helperText={ 83 |
84 |

85 | Autocheckout will automatically add to cart a product whenever you click on a product page and 86 | automatically fill the checkout form using the informations in the "Billing" tab. 87 |
88 |
89 | The product will be added to cart using the sizes you chose in the "Sizes" tab. 90 |

91 |
92 | } 93 | /> 94 |
95 | 96 |
97 | 104 | } 105 | title="Enable Auto Payment" 106 | helperText={ 107 |
108 |

109 | Autopayment option will automatically click the "Process payment" button in the checkout form. 110 |
111 | Autocheckout must be enabled otherwise this option will not have any effect. 112 |

113 |
114 | } 115 | /> 116 | 117 |
118 |
119 | 126 | } 127 | title="Enable strict size checking" 128 | helperText={ 129 |
130 |

131 | If this option is enabled a product will not be added to the cart unless the selected size from the "Sizes" tab is available. 132 |

133 |
134 | } 135 | /> 136 |
137 | 138 |
139 | 146 | } 147 | title="Hide sold out products" 148 | helperText={ 149 |
150 |

151 | This option will hide sold out products from the Supreme online shop. 152 |

153 |
154 | } 155 | /> 156 |
157 | 158 |
159 | 166 | } 167 | title="Display restock notifications" 168 | helperText={ 169 |
170 |

171 | If this option is enabled you will receive a notification whenever a product restocks or land on the shop. 172 |

173 |
174 | } 175 | /> 176 |
177 | 178 |
179 | 186 | } 187 | title="Use product monitor for AutoCop" 188 | helperText={ 189 |
190 |

191 | A process in the background periodically fetches products from the Supreme mobile api, if you use this option Autocop will try 192 | to find products based on the products from this API. 193 |
194 |
195 | This is required for countries where product's names aren't displayed on the Supreme website like Japan. 196 |

197 |
198 | } 199 | /> 200 |
201 | 202 |
203 | 210 | } 211 | title="Disable images loading" 212 | helperText={ 213 |
214 |

215 | If enabled, images will not be loaded on the Supreme website, this can improve speed. 216 |

217 |
218 | } 219 | /> 220 |
221 |
222 | 229 | } 230 | title="Enable AutoCop timer" 231 | helperText={ 232 |
233 |

234 | AutoCop is a feature that will automatically try to buy your desired products at a specific time. 235 |

236 | Once AutoCop is triggered at the end of the timer, a tab with the Supreme shop will be opened for every products you have defined in the "Autocop" page and automatically add them to 237 | the cart if they are found. 238 |

239 |
240 | } 241 | /> 242 |
243 | 244 | { 245 | atcEnabled && 246 |
247 | 255 | 264 |
265 | } 266 |
267 | 276 | 277 | 278 | 279 |
280 |
281 | 289 |
290 | 291 |
292 | 300 |
301 | 302 |
303 | 311 |
312 | 313 |
314 | 322 |
323 | 324 |
325 | 330 |
331 |
332 |
333 | ); 334 | }; 335 | 336 | const OptionsForm = reduxForm({ 337 | form: 'options', 338 | })(Options); 339 | 340 | const selector = formValueSelector('options'); 341 | 342 | function mapStateToProps(state, ownProps) { 343 | const currentProfile = state.profiles.currentProfile; 344 | const settings = state.profiles.profiles.filter(x => x.name === currentProfile)[0].settings; 345 | const initialValues = Object.assign({}, defaultValues, (settings[ownProps.shop] || {})[menus.MENU_OPTIONS] || {}); 346 | if (initialValues['atcStartDate'] && initialValues['atcStartDate'] !== 'Invalid Date') { 347 | initialValues['atcStartDate'] = new Date(initialValues['atcStartDate']); 348 | } 349 | return { 350 | initialValues, 351 | atcEnabled: selector(state, 'atcEnabled'), 352 | }; 353 | } 354 | 355 | Options.propTypes = { 356 | shop: PropTypes.string.isRequired, 357 | }; 358 | 359 | 360 | export default connect(mapStateToProps)(OptionsForm); 361 | -------------------------------------------------------------------------------- /bot/src/app/components/shops/supreme/AtcCreateForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { reduxForm, Field } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import ChipInput from 'material-ui-chip-input'; 5 | import MenuItem from 'material-ui/MenuItem'; 6 | import { 7 | TextField, 8 | SelectField, 9 | Toggle, 10 | } from 'redux-form-material-ui'; 11 | import RaisedButton from 'material-ui/RaisedButton'; 12 | import FlatButton from 'material-ui/FlatButton'; 13 | import IconButton from 'material-ui/IconButton'; 14 | import ActionDelete from 'material-ui/svg-icons/action/delete'; 15 | import { orange400, blue400, green400, red400 } from 'material-ui/styles/colors'; 16 | import * as Validators from '../../../utils/FormValidators'; 17 | import Styles from '../../../constants/Styles'; 18 | import * as Utils from '../../../constants/Utils'; 19 | import ProductsService from '../../../../services/supreme/ProductsService'; 20 | import KeywordsService from '../../../../services/KeywordsService'; 21 | import DropsService from '../../../../services/supreme/DropsService'; 22 | import FuzzyStringMatcher from '../../../utils/FuzzyStringMatcher'; 23 | 24 | 25 | function getSizeForCategory(category) { 26 | switch (category) { 27 | case 'accessories': 28 | case 't-shirts': 29 | case 'sweatshirts': 30 | case 'tops-sweaters': 31 | case 'shirts': 32 | case 'jackets': 33 | return Utils.sizes; 34 | case 'pants': 35 | case 'shorts': 36 | return Utils.sizesPants; 37 | case 'shoes': 38 | return Utils.shoeSizes; 39 | case 'skate': 40 | return Utils.skateSizes; 41 | case 'hats': 42 | return Utils.hatsSizes; 43 | default: 44 | return []; 45 | } 46 | } 47 | 48 | class AtcCreateForm extends Component { 49 | constructor(props) { 50 | super(props); 51 | this.state = { 52 | keywords: props.initialValues.keywords || [], 53 | category: props.initialValues.category, 54 | matchedProducts: [], 55 | matchedDropProducts: [], 56 | }; 57 | if (props.initialValues && props.initialValues.category) { 58 | this.onKeywordChange(props.initialValues.keywords); 59 | } 60 | } 61 | 62 | async onKeywordChange(keywords) { 63 | if (!this.state.category || !this.state.keywords.length) { 64 | return; 65 | } 66 | const monitoredProducts = await ProductsService.fetchProducts(); 67 | const matches = KeywordsService.findMatches(monitoredProducts, keywords, this.state.category) || []; 68 | this.setState({ 69 | matchedProducts: matches.map(x => x.name), 70 | }); 71 | const dropProducts = (await DropsService.fetchLatestDropProducts()).filter(x => x.category === this.state.category); 72 | const matcher = new FuzzyStringMatcher(dropProducts, { key: 'name' }); 73 | const dropMatches = matcher.search(keywords.join(' ')); 74 | this.setState({ 75 | matchedDropProducts: dropMatches.map(x => x.name), 76 | }); 77 | } 78 | 79 | addIgnoreKeyword(productName) { 80 | const productKeywords = productName.split(' ').map(x => x.toLowerCase()); 81 | const keywords = this.state.keywords.map(x => x.toLowerCase()); 82 | const ignoringKeyword = productKeywords.find(x => keywords.indexOf(x) === -1); 83 | if (ignoringKeyword) { 84 | const newKeywords = [`!${ignoringKeyword}`, ...this.state.keywords]; 85 | this.props.change('keywords', newKeywords); 86 | this.setState({ 87 | keywords: newKeywords, 88 | }); 89 | this.onKeywordChange(newKeywords); 90 | } 91 | } 92 | 93 | render() { 94 | const { handleSubmit, pristine, submitting, onRequestClose, atcProducts, initialValues, editing } = this.props; 95 | const renderChip = ({input, hintText, floatingLabelText, meta: {touched, error} }) => ( 96 | { 102 | let values = input.value || []; 103 | values = values.slice(); 104 | values.push(addedChip); 105 | input.onChange(values); 106 | }} 107 | onRequestDelete={(deletedChip) => { 108 | let values = input.value || []; 109 | values = values.filter(v => v !== deletedChip); 110 | input.onChange(values); 111 | }} 112 | onBlur={() => input.onBlur()} 113 | hintText={hintText} 114 | floatingLabelText={floatingLabelText} 115 | errorText={(touched && error) ? error : ''} 116 | /> 117 | ); 118 | const buttonStyle = { 119 | margin: 6, 120 | float: 'right', 121 | }; 122 | 123 | const initialAtcName = initialValues.name; 124 | const formValidators = [Validators.required]; 125 | if (!editing) { 126 | formValidators.push(Validators.unique(atcProducts.map(x => x.product.name))); 127 | } else { 128 | formValidators.push(Validators.unique(atcProducts.filter(x => x.product.name !== initialAtcName).map(x => x.product.name))); 129 | } 130 | const deleteIconStyle = { 131 | width: 16, 132 | height: 16, 133 | color: red400, 134 | }; 135 | 136 | const deleteStyle = { 137 | width: 32, 138 | height: 32, 139 | }; 140 | return ( 141 |
142 |

ATC Product description is only used to differentiate different ATC products, it doesn't have any effect on the Autocop process.

143 |

Keywords is the most important information to find a product for Autocop, make sure to add detailed keywords. For example for a Box Logo add the following keywords: box, logo, hoodie.

144 |

You can also add negative keywords by prepending a "!" to a keyword, for example the keywords "box logo !longsleeve tee" will match a product like "Box Logo Tee" but not "Box Logo Longsleeve tee"

145 |

If you do not select a size, AutoCop will choose the size you selected in the "Sizings" tab.

146 | {this.state.matchedProducts.length > 0 && (

147 | Warning! Your keywords already matches with the following products from the store, click on the bin to ignore unwanted products: 148 |

    149 | {this.state.matchedProducts.map(x => { 150 | return ( 151 |
  • {x} 152 | this.addIgnoreKeyword(x)} 156 | tooltip="Add negative keywords to ignore" 157 | > 158 | 159 | 160 |
  • 161 | ); 162 | })} 163 |
164 |

)} 165 | {this.state.matchedDropProducts.length > 1 && (

166 | Warning! Your keywords matches with multiple products from the incoming drop, click on the bin to ignore unwanted products: 167 |

    168 | {this.state.matchedDropProducts.map(x => { 169 | return ( 170 |
  • {x} 171 | this.addIgnoreKeyword(x)} 175 | tooltip="Add negative keywords to ignore" 176 | > 177 | 178 | 179 |
  • 180 | ); 181 | })} 182 |
183 |

)} 184 | {this.state.matchedDropProducts.length === 1 && ( 185 |
186 |

The keywords will match the following product from the incoming drop:

187 |
    188 |
  • {this.state.matchedDropProducts[0]}
  • 189 |
190 |
191 | 192 | )} 193 |
194 |
195 | 203 |
204 | 205 |
206 | { 215 | const keywords = Object.values(v).filter(x => typeof x === 'string'); 216 | this.setState({ keywords }, () => { 217 | this.onKeywordChange(keywords); 218 | }); 219 | }} 220 | /> 221 |
222 | 223 |
224 | 231 |
232 | 233 |
234 | { 241 | this.setState({ category: v }, () => { 242 | this.onKeywordChange(this.state.keywords); 243 | }); 244 | }} 245 | > 246 | { 247 | Utils.categories.map((x) => { 248 | return ( 249 | 250 | ); 251 | }) 252 | } 253 | 254 |
255 | 256 |
257 | 263 | { 264 | [, ...getSizeForCategory(this.state.category || initialValues.category).map((x) => { 265 | return ( 266 | 267 | ); 268 | })] 269 | } 270 | 271 |
272 | 273 |
274 | 282 | 283 | 284 | { 285 | Array.apply(null, new Array(5)).map((x, i) => { 286 | return ; 287 | }) 288 | } 289 | 290 |
291 | 292 |
293 | 301 | 302 | 303 | { 304 | Array.apply(null, new Array(5)).map((x, i) => { 305 | return ; 306 | }) 307 | } 308 | 309 |
310 | 311 |
312 |
313 | 319 |
320 | 321 |
322 | 328 | { 332 | if (onRequestClose) { 333 | onRequestClose(); 334 | } 335 | }} 336 | /> 337 |
338 |
339 |
340 | ); 341 | } 342 | } 343 | 344 | AtcCreateForm.propTypes = { 345 | onRequestClose: PropTypes.function, 346 | }; 347 | 348 | const Form = reduxForm({ 349 | form: 'atc-form', 350 | })(AtcCreateForm); 351 | 352 | function mapStateToProps(state, ownProps) { 353 | return { 354 | atcProducts: state.atc.atcProducts, 355 | initialValues: Object.assign({ 356 | enabled: true, 357 | retryCount: 'inf', 358 | soldOutAction: 'skip', 359 | }, ownProps.initialValues), 360 | }; 361 | } 362 | 363 | export default connect(mapStateToProps)(Form); 364 | --------------------------------------------------------------------------------