Repository: walmat/nebula Branch: main Commit: 176ae287cbf8 Files: 634 Total size: 10.6 MB Directory structure: gitextract_l8lb8z_d/ ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierrc ├── .stylelintrc ├── app/ │ ├── 3ds.html │ ├── Harvester.html │ ├── Question.html │ ├── __mocks__/ │ │ ├── dns.tsx │ │ ├── electron-ga.tsx │ │ ├── electron.tsx │ │ ├── fileMock.tsx │ │ ├── request-promise.tsx │ │ └── styleMock.tsx │ ├── api/ │ │ └── sys/ │ │ ├── fileOps.ts │ │ └── index.ts │ ├── app.html │ ├── auth.html │ ├── classes/ │ │ ├── AppUpdate.ts │ │ ├── Boot.ts │ │ ├── Notification.ts │ │ ├── ProxyTester.ts │ │ └── Storage.ts │ ├── components/ │ │ ├── Analytics/ │ │ │ ├── Loadable.tsx │ │ │ ├── __tests__/ │ │ │ │ └── Analytics.spec.tsx │ │ │ ├── actions/ │ │ │ │ ├── checkouts.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── news.tsx │ │ │ ├── components/ │ │ │ │ ├── checkouts.tsx │ │ │ │ ├── expenses.tsx │ │ │ │ ├── news.tsx │ │ │ │ ├── orders.tsx │ │ │ │ ├── stats.tsx │ │ │ │ └── welcome.tsx │ │ │ ├── index.tsx │ │ │ ├── reducers/ │ │ │ │ ├── checkouts.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── news.tsx │ │ │ ├── styles/ │ │ │ │ ├── checkouts.tsx │ │ │ │ ├── expenses.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── news.tsx │ │ │ │ ├── orders.tsx │ │ │ │ ├── shipments.tsx │ │ │ │ ├── stats.tsx │ │ │ │ └── welcome.tsx │ │ │ └── types.tsx │ │ ├── App/ │ │ │ ├── App.tsx │ │ │ ├── Providers.tsx │ │ │ ├── Root.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── App.spec.tsx │ │ │ │ └── Root.spec.tsx │ │ │ ├── actions.tsx │ │ │ ├── components/ │ │ │ │ ├── titlebar/ │ │ │ │ │ └── Titlebar.tsx │ │ │ │ └── toolbar/ │ │ │ │ ├── area.tsx │ │ │ │ ├── body.tsx │ │ │ │ ├── menu.tsx │ │ │ │ └── profile.tsx │ │ │ ├── reducers.tsx │ │ │ ├── selectors.tsx │ │ │ └── styles/ │ │ │ ├── Titlebar.tsx │ │ │ ├── ToolbarAreaPane.tsx │ │ │ └── index.tsx │ │ ├── Calendar/ │ │ │ ├── Calendar.tsx │ │ │ ├── CalendarMonth.tsx │ │ │ ├── CalendarYear.tsx │ │ │ ├── Container.tsx │ │ │ ├── Month.tsx │ │ │ └── ViewSelect.tsx │ │ ├── Captchas/ │ │ │ ├── Harvesters.tsx │ │ │ ├── Loadable.tsx │ │ │ ├── __tests__/ │ │ │ │ └── Harvesters.spec.tsx │ │ │ ├── actions/ │ │ │ │ ├── captchas.tsx │ │ │ │ └── index.tsx │ │ │ ├── components/ │ │ │ │ ├── actionBar/ │ │ │ │ │ └── ActionBar.tsx │ │ │ │ ├── card/ │ │ │ │ │ └── index.tsx │ │ │ │ └── grid/ │ │ │ │ └── index.tsx │ │ │ ├── reducers/ │ │ │ │ ├── captchas.tsx │ │ │ │ └── index.tsx │ │ │ ├── selectors.tsx │ │ │ └── styles/ │ │ │ ├── actionBar.tsx │ │ │ ├── card.tsx │ │ │ ├── createDialog.tsx │ │ │ └── index.tsx │ │ ├── DebouncedInput/ │ │ │ └── DebouncedInput.tsx │ │ ├── ErrorBoundary/ │ │ │ ├── components/ │ │ │ │ ├── GenerateErrorReport.tsx │ │ │ │ └── GenerateErrorReportBody.tsx │ │ │ ├── index.tsx │ │ │ └── styles/ │ │ │ ├── GenerateErrorReport.tsx │ │ │ └── index.tsx │ │ ├── ImportExport/ │ │ │ ├── components/ │ │ │ │ └── dialog.tsx │ │ │ ├── index.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── Legal/ │ │ │ ├── PrivacyPolicy/ │ │ │ │ ├── Loadable.tsx │ │ │ │ ├── __tests__/ │ │ │ │ │ └── PrivacyPolicy.spec.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles/ │ │ │ │ └── index.tsx │ │ │ └── TermsOfService/ │ │ │ ├── Loadable.tsx │ │ │ ├── __tests__/ │ │ │ │ └── TermsOfService.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── LoadingIndicator/ │ │ │ ├── index.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── NoChildrenComponent/ │ │ │ ├── index.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── Profiles/ │ │ │ ├── Loadable.tsx │ │ │ ├── Profiles.tsx │ │ │ ├── __tests__/ │ │ │ │ └── Profiles.spec.tsx │ │ │ ├── actions/ │ │ │ │ ├── index.tsx │ │ │ │ └── profiles.tsx │ │ │ ├── components/ │ │ │ │ ├── actionBar/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── card/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── create/ │ │ │ │ │ ├── ProfileCreateDialog.tsx │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── ProfileCreateDialog.spec.tsx │ │ │ │ │ ├── billing.tsx │ │ │ │ │ ├── payment.tsx │ │ │ │ │ └── shipping.tsx │ │ │ │ └── grid/ │ │ │ │ └── index.tsx │ │ │ ├── reducers/ │ │ │ │ ├── current.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── location.tsx │ │ │ │ ├── payment.tsx │ │ │ │ └── profiles.tsx │ │ │ ├── selectors.tsx │ │ │ └── styles/ │ │ │ ├── actionBar.tsx │ │ │ ├── card.tsx │ │ │ ├── createDialog.tsx │ │ │ └── index.tsx │ │ ├── Progressbar/ │ │ │ ├── Loadable.tsx │ │ │ ├── index.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── Proxies/ │ │ │ ├── Loadable.tsx │ │ │ ├── Proxies.tsx │ │ │ ├── __tests__/ │ │ │ │ └── Proxies.spec.tsx │ │ │ ├── actions/ │ │ │ │ └── index.tsx │ │ │ ├── components/ │ │ │ │ ├── ProxyActionBar.tsx │ │ │ │ ├── ProxyCreateDialog.tsx │ │ │ │ ├── Table/ │ │ │ │ │ ├── ProxyTable.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── tableHead.tsx │ │ │ │ │ │ ├── tableRow.tsx │ │ │ │ │ │ └── tableToolbar.tsx │ │ │ │ │ └── styles/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── tableToolbar.tsx │ │ │ │ └── __tests__/ │ │ │ │ └── ProxyCreateDialog.spec.tsx │ │ │ ├── reducers/ │ │ │ │ ├── current.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── proxies.tsx │ │ │ ├── selectors.tsx │ │ │ └── styles/ │ │ │ ├── actionBar.tsx │ │ │ ├── createDialog.tsx │ │ │ └── index.tsx │ │ ├── ReportBugs/ │ │ │ ├── Loadable.tsx │ │ │ ├── __tests__/ │ │ │ │ └── ReportBugs.spec.tsx │ │ │ ├── index.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── Settings/ │ │ │ ├── actions.tsx │ │ │ ├── components/ │ │ │ │ └── dialog/ │ │ │ │ ├── ProductField.tsx │ │ │ │ ├── ProfileField.tsx │ │ │ │ ├── StoreField.tsx │ │ │ │ ├── accounts.tsx │ │ │ │ ├── defaults.tsx │ │ │ │ ├── generics.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── rates.tsx │ │ │ │ └── webhooks.tsx │ │ │ ├── index.tsx │ │ │ ├── reducers/ │ │ │ │ ├── accounts.tsx │ │ │ │ ├── currentAccount.tsx │ │ │ │ ├── currentWebhook.tsx │ │ │ │ ├── defaults.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── rates.ts │ │ │ │ ├── settings.tsx │ │ │ │ └── webhooks.tsx │ │ │ ├── selectors.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── Sidebar/ │ │ │ ├── Sidebar.tsx │ │ │ ├── __tests__/ │ │ │ │ └── Sidebar.spec.tsx │ │ │ ├── components/ │ │ │ │ ├── AnimatedLogo.tsx │ │ │ │ ├── icons.tsx │ │ │ │ └── menuItems.tsx │ │ │ └── styles/ │ │ │ └── index.tsx │ │ ├── Tasks/ │ │ │ ├── Loadable.tsx │ │ │ ├── Tasks.tsx │ │ │ ├── __tests__/ │ │ │ │ └── Tasks.spec.tsx │ │ │ ├── actions/ │ │ │ │ └── index.tsx │ │ │ ├── components/ │ │ │ │ ├── Table/ │ │ │ │ │ ├── TableData.tsx │ │ │ │ │ ├── TableWrapper.tsx │ │ │ │ │ ├── TaskList.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── TableBodyWrapper.tsx │ │ │ │ │ │ ├── TableHeader.tsx │ │ │ │ │ │ ├── cells/ │ │ │ │ │ │ │ ├── checkbox.tsx │ │ │ │ │ │ │ ├── product.tsx │ │ │ │ │ │ │ ├── profile.tsx │ │ │ │ │ │ │ ├── proxies.tsx │ │ │ │ │ │ │ ├── sizes.tsx │ │ │ │ │ │ │ ├── status.tsx │ │ │ │ │ │ │ ├── store.tsx │ │ │ │ │ │ │ └── taskId.tsx │ │ │ │ │ │ ├── header/ │ │ │ │ │ │ │ ├── checkbox.tsx │ │ │ │ │ │ │ ├── product.tsx │ │ │ │ │ │ │ ├── profile.tsx │ │ │ │ │ │ │ ├── proxies.tsx │ │ │ │ │ │ │ ├── sizes.tsx │ │ │ │ │ │ │ ├── status.tsx │ │ │ │ │ │ │ ├── store.tsx │ │ │ │ │ │ │ └── taskId.tsx │ │ │ │ │ │ ├── icons.tsx │ │ │ │ │ │ ├── tableRow.tsx │ │ │ │ │ │ ├── tableToolbar.tsx │ │ │ │ │ │ └── toolbar/ │ │ │ │ │ │ ├── clock.tsx │ │ │ │ │ │ ├── filter.tsx │ │ │ │ │ │ ├── groups.tsx │ │ │ │ │ │ ├── monitor.tsx │ │ │ │ │ │ ├── retry.tsx │ │ │ │ │ │ └── stagger.tsx │ │ │ │ │ └── styles/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── tableToolbar.tsx │ │ │ │ ├── actionBar/ │ │ │ │ │ ├── actionBar.tsx │ │ │ │ │ ├── copyBtn.tsx │ │ │ │ │ ├── createBtn.tsx │ │ │ │ │ ├── deleteBtn.tsx │ │ │ │ │ ├── editBtn.tsx │ │ │ │ │ ├── startBtn.tsx │ │ │ │ │ └── stopBtn.tsx │ │ │ │ ├── create/ │ │ │ │ │ ├── AccountField.tsx │ │ │ │ │ ├── CaptchaField.tsx │ │ │ │ │ ├── CategoryField.tsx │ │ │ │ │ ├── CheckoutDelayField.tsx │ │ │ │ │ ├── DateField.tsx │ │ │ │ │ ├── DiscountField.tsx │ │ │ │ │ ├── FootsiteForm.tsx │ │ │ │ │ ├── MaxPriceField.tsx │ │ │ │ │ ├── MinPriceField.tsx │ │ │ │ │ ├── MockToggle.tsx │ │ │ │ │ ├── NumberOfTasksField.tsx │ │ │ │ │ ├── OneCheckout.tsx │ │ │ │ │ ├── PasswordField.tsx │ │ │ │ │ ├── PayPalField.tsx │ │ │ │ │ ├── PokemonForm.tsx │ │ │ │ │ ├── ProductField.tsx │ │ │ │ │ ├── ProfileField.tsx │ │ │ │ │ ├── ProxiesField.tsx │ │ │ │ │ ├── QuantityField.tsx │ │ │ │ │ ├── RatesField.tsx │ │ │ │ │ ├── RestockMode.tsx │ │ │ │ │ ├── SecureBypassField.tsx │ │ │ │ │ ├── ShopifyForm.tsx │ │ │ │ │ ├── SizesField.tsx │ │ │ │ │ ├── StoreField.tsx │ │ │ │ │ ├── StyleIdField.tsx │ │ │ │ │ ├── SupremeForm.tsx │ │ │ │ │ ├── TaskCreateDialog.tsx │ │ │ │ │ ├── TaskForm.tsx │ │ │ │ │ ├── TaskModeField.tsx │ │ │ │ │ ├── UseRotateProxies.tsx │ │ │ │ │ ├── VariationField.tsx │ │ │ │ │ ├── YeezySupplyForm.tsx │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── TaskCreateDialog.spec.tsx │ │ │ │ │ ├── clearBtn.tsx │ │ │ │ │ ├── closeBtn.tsx │ │ │ │ │ └── createBtn.tsx │ │ │ │ └── edits/ │ │ │ │ └── TaskEditDialog.tsx │ │ │ ├── reducers/ │ │ │ │ ├── current.tsx │ │ │ │ ├── delays.tsx │ │ │ │ ├── edits.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── tasks.tsx │ │ │ ├── selectors.tsx │ │ │ ├── styles/ │ │ │ │ ├── actionBar.tsx │ │ │ │ ├── createDialog.tsx │ │ │ │ └── index.tsx │ │ │ └── useTaskKeyPress.tsx │ │ ├── featureFlag/ │ │ │ ├── FeatureFlagContext.tsx │ │ │ └── NebulaFeatureFlags.ts │ │ └── ui/ │ │ ├── Select.tsx │ │ └── table/ │ │ ├── IndeterminateCheckbox.tsx │ │ ├── RenderRow.tsx │ │ ├── RowTable.tsx │ │ ├── Table.tsx │ │ ├── TableCell.tsx │ │ └── TableVirtualized.tsx │ ├── constants/ │ │ ├── countries.json │ │ ├── env.js │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── meta.js │ │ ├── regexes.js │ │ └── sizes.js │ ├── hooks/ │ │ ├── useAnalyticsFile.tsx │ │ ├── useAutoSolveLifecycle.tsx │ │ ├── useAutoUpdateLifecycle.tsx │ │ ├── useEscape.tsx │ │ ├── useInterval.ts │ │ ├── useNotificationLifecycle.tsx │ │ ├── useProxiesStatus.tsx │ │ ├── useQuickTaskLifecycle.tsx │ │ ├── useTaskLifecycle.tsx │ │ ├── useTaskStatus.tsx │ │ ├── useTraceUpdate.ts │ │ ├── useUpdateProfiles.tsx │ │ ├── useUpdateProxies.tsx │ │ ├── useUpdateStagger.tsx │ │ ├── useUpdateWebhooks.tsx │ │ └── useWhyDidYouUpdate.ts │ ├── index.tsx │ ├── main.dev.ts │ ├── mainWindow/ │ │ └── windows.ts │ ├── menu.ts │ ├── routing/ │ │ ├── ClientRouter.ts │ │ └── routes.ts │ ├── store/ │ │ ├── configureStore/ │ │ │ ├── dev.ts │ │ │ ├── index.ts │ │ │ └── prod.ts │ │ ├── migrations.ts │ │ └── reducers.ts │ ├── styles/ │ │ ├── js/ │ │ │ ├── index.tsx │ │ │ ├── mixins.tsx │ │ │ ├── themes.ts │ │ │ └── variables.tsx │ │ ├── scss/ │ │ │ ├── app.global.scss │ │ │ ├── base/ │ │ │ │ ├── _base.scss │ │ │ │ ├── _extends.scss │ │ │ │ ├── _mixins.scss │ │ │ │ ├── _variables.scss │ │ │ │ └── mixins/ │ │ │ │ ├── _align-items.scss │ │ │ │ ├── _animate-link.scss │ │ │ │ ├── _animations.scss │ │ │ │ ├── _backface-visibility.scss │ │ │ │ ├── _background-cover.scss │ │ │ │ ├── _border.scss │ │ │ │ ├── _box-model.scss │ │ │ │ ├── _box-shadow.scss │ │ │ │ ├── _breakpoint.scss │ │ │ │ ├── _clearfix.scss │ │ │ │ ├── _display.scss │ │ │ │ ├── _display_flex.scss │ │ │ │ ├── _flex.scss │ │ │ │ ├── _hide-text.scss │ │ │ │ ├── _horz-vert-center.scss │ │ │ │ ├── _hover-focus.scss │ │ │ │ ├── _inline-block.scss │ │ │ │ ├── _inner-shadow.scss │ │ │ │ ├── _keyframes.scss │ │ │ │ ├── _linear-gradient-angle.scss │ │ │ │ ├── _linear-gradient.scss │ │ │ │ ├── _margin-auto.scss │ │ │ │ ├── _mediumFont.scss │ │ │ │ ├── _min-breakpoint.scss │ │ │ │ ├── _opacity.scss │ │ │ │ ├── _placeholder.scss │ │ │ │ ├── _rem.scss │ │ │ │ ├── _replace-text.scss │ │ │ │ ├── _retina.scss │ │ │ │ ├── _rounded-corners.scss │ │ │ │ ├── _single-transform.scss │ │ │ │ ├── _text-shadow.scss │ │ │ │ ├── _transform.scss │ │ │ │ ├── _transitions.scss │ │ │ │ ├── _translate.scss │ │ │ │ └── _triangles.scss │ │ │ └── themes/ │ │ │ ├── fonts.scss │ │ │ └── reset.scss │ │ └── select.tsx │ ├── tasks/ │ │ ├── common/ │ │ │ ├── classes/ │ │ │ │ ├── index.ts │ │ │ │ ├── monitor.ts │ │ │ │ └── task.ts │ │ │ ├── constants/ │ │ │ │ └── index.ts │ │ │ ├── contexts/ │ │ │ │ ├── footsite.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pokemon.ts │ │ │ │ ├── shopify.ts │ │ │ │ └── yeezysupply.ts │ │ │ ├── index.ts │ │ │ └── utils/ │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── queues.ts │ │ │ ├── request.ts │ │ │ ├── rfrl.ts │ │ │ └── timer.ts │ │ ├── footsites/ │ │ │ ├── classes/ │ │ │ │ ├── functions/ │ │ │ │ │ ├── billing.ts │ │ │ │ │ ├── captcha.ts │ │ │ │ │ ├── cart.ts │ │ │ │ │ ├── checkout.ts │ │ │ │ │ ├── email.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── information.ts │ │ │ │ │ ├── queue.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ ├── shipping.ts │ │ │ │ │ └── stock.ts │ │ │ │ ├── tasks/ │ │ │ │ │ ├── base.ts │ │ │ │ │ └── index.ts │ │ │ │ └── types/ │ │ │ │ ├── datadome.d.ts │ │ │ │ ├── geetest.d.ts │ │ │ │ ├── index.ts │ │ │ │ ├── stock.d.ts │ │ │ │ └── variant.d.ts │ │ │ ├── constants/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── mocks/ │ │ │ │ ├── addToCart.ts │ │ │ │ ├── cookies/ │ │ │ │ │ ├── champs.js │ │ │ │ │ ├── cookies.ts │ │ │ │ │ ├── eb.js │ │ │ │ │ ├── fa.js │ │ │ │ │ ├── ftl-ca.js │ │ │ │ │ ├── ftl-kids.js │ │ │ │ │ └── ftl-us.js │ │ │ │ ├── getProductInfo.ts │ │ │ │ ├── getProductPage.ts │ │ │ │ ├── getSession.ts │ │ │ │ ├── getStock.ts │ │ │ │ ├── submitCheckout.ts │ │ │ │ └── submitInformation.ts │ │ │ └── utils/ │ │ │ ├── __tests__/ │ │ │ │ └── pickVariant.test.ts │ │ │ ├── cleanseHeaderData.ts │ │ │ ├── dfValues.ts │ │ │ ├── forms.ts │ │ │ ├── index.ts │ │ │ └── pickVariant.ts │ │ ├── index.ts │ │ ├── managers/ │ │ │ ├── analytics.ts │ │ │ ├── browser/ │ │ │ │ └── index.ts │ │ │ ├── cache/ │ │ │ │ ├── cache.ts │ │ │ │ └── windows.ts │ │ │ ├── captcha/ │ │ │ │ ├── autoSolve.ts │ │ │ │ ├── captcha.ts │ │ │ │ ├── windows.ts │ │ │ │ └── youtube.ts │ │ │ ├── checkout.ts │ │ │ ├── checkpoint/ │ │ │ │ └── index.tsx │ │ │ ├── geetest/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── interception/ │ │ │ │ ├── index.ts │ │ │ │ └── window.ts │ │ │ ├── notification.ts │ │ │ ├── profile/ │ │ │ │ ├── profile.ts │ │ │ │ └── typings.d.ts │ │ │ ├── proxy.ts │ │ │ ├── queue/ │ │ │ │ └── index.ts │ │ │ ├── restart.ts │ │ │ ├── tasks/ │ │ │ │ ├── choose.ts │ │ │ │ └── index.ts │ │ │ ├── typings.d.ts │ │ │ ├── utils.ts │ │ │ └── webhook/ │ │ │ ├── aycd.ts │ │ │ ├── discord.ts │ │ │ ├── index.ts │ │ │ └── slack.ts │ │ ├── pokemon/ │ │ │ ├── classes/ │ │ │ │ ├── functions/ │ │ │ │ │ ├── captcha.ts │ │ │ │ │ ├── cart.ts │ │ │ │ │ ├── checkout.ts │ │ │ │ │ ├── datadome.ts │ │ │ │ │ ├── email.ts │ │ │ │ │ ├── encrypt.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── information.ts │ │ │ │ │ ├── payment.ts │ │ │ │ │ ├── product.ts │ │ │ │ │ └── session.ts │ │ │ │ └── tasks/ │ │ │ │ ├── base.ts │ │ │ │ └── index.ts │ │ │ ├── constants/ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── types/ │ │ │ │ ├── cart.d.ts │ │ │ │ ├── checkout.d.ts │ │ │ │ ├── datadome.d.ts │ │ │ │ ├── geetest.d.ts │ │ │ │ ├── index.d.ts │ │ │ │ ├── information.d.ts │ │ │ │ ├── key.d.ts │ │ │ │ ├── order.d.ts │ │ │ │ ├── payment.d.ts │ │ │ │ ├── product.d.ts │ │ │ │ ├── products.d.ts │ │ │ │ ├── success.d.ts │ │ │ │ └── token.d.ts │ │ │ └── utils/ │ │ │ ├── cards.ts │ │ │ ├── decode.ts │ │ │ ├── encrypt.ts │ │ │ ├── forms.ts │ │ │ ├── index.ts │ │ │ └── pickVariant.ts │ │ ├── shopify/ │ │ │ ├── classes/ │ │ │ │ ├── functions/ │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── cart.ts │ │ │ │ │ ├── challenge.ts │ │ │ │ │ ├── checkout.ts │ │ │ │ │ ├── checkpoint.ts │ │ │ │ │ ├── config.ts │ │ │ │ │ ├── customer.ts │ │ │ │ │ ├── discount.ts │ │ │ │ │ ├── homepage.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── order.ts │ │ │ │ │ ├── password.ts │ │ │ │ │ ├── payment.ts │ │ │ │ │ ├── paypal.ts │ │ │ │ │ ├── product.ts │ │ │ │ │ ├── queue.ts │ │ │ │ │ ├── review.ts │ │ │ │ │ ├── session.ts │ │ │ │ │ └── shipping.ts │ │ │ │ ├── monitor.ts │ │ │ │ ├── rates.ts │ │ │ │ └── tasks/ │ │ │ │ ├── base.ts │ │ │ │ ├── fast.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pfutile.ts │ │ │ │ ├── preload.ts │ │ │ │ └── safe.ts │ │ │ ├── constants/ │ │ │ │ ├── gateways.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── mocks/ │ │ │ │ ├── __cookies__.ts │ │ │ │ ├── apiCheckouts.tsx │ │ │ │ ├── checkout.ts │ │ │ │ ├── deadstockQuestion.ts │ │ │ │ ├── kithMock.tsx │ │ │ │ ├── paymentsConfig.ts │ │ │ │ ├── responseTypes.ts │ │ │ │ └── sessions.ts │ │ │ ├── types/ │ │ │ │ ├── cart.d.ts │ │ │ │ ├── index.d.ts │ │ │ │ ├── product.d.ts │ │ │ │ └── rates.d.ts │ │ │ └── utils/ │ │ │ ├── __tests__/ │ │ │ │ └── pickVariant.test.ts │ │ │ ├── forms.ts │ │ │ ├── index.ts │ │ │ ├── mocks/ │ │ │ │ └── kithNewBalance.ts │ │ │ ├── parse.ts │ │ │ ├── pickVariant.ts │ │ │ └── protection.ts │ │ └── yeezysupply/ │ │ ├── classes/ │ │ │ ├── functions/ │ │ │ │ ├── 3ds.ts │ │ │ │ ├── akamai.ts │ │ │ │ ├── bloom.ts │ │ │ │ ├── cart.ts │ │ │ │ ├── checkout.ts │ │ │ │ ├── homepage.ts │ │ │ │ ├── index.ts │ │ │ │ ├── information.ts │ │ │ │ ├── mpulse.ts │ │ │ │ ├── pixel.ts │ │ │ │ ├── product.ts │ │ │ │ ├── splash.ts │ │ │ │ ├── stock.ts │ │ │ │ └── waiting.ts │ │ │ ├── tasks/ │ │ │ │ ├── base.ts │ │ │ │ └── index.ts │ │ │ └── types/ │ │ │ └── index.ts │ │ ├── constants/ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── mocks/ │ │ │ ├── addToCart.ts │ │ │ ├── cookies.ts │ │ │ ├── getAvailability.ts │ │ │ ├── getProductInfo.ts │ │ │ ├── getProductPage.ts │ │ │ ├── submitCheckout.ts │ │ │ └── submitInformation.ts │ │ └── utils/ │ │ ├── __tests__/ │ │ │ └── pickVariant.test.ts │ │ ├── dfValues.ts │ │ ├── forms.ts │ │ ├── index.ts │ │ └── pickVariant.ts │ └── utils/ │ ├── assert.ts │ ├── bootHelper.ts │ ├── captchaTypes.ts │ ├── cardFormatter.ts │ ├── comparators.ts │ ├── convertObjectToArray.ts │ ├── createWindows.ts │ ├── date.js │ ├── eventHandling.ts │ ├── funcs.js │ ├── getPlatform.ts │ ├── gzip.ts │ ├── imgForCardType.ts │ ├── imgsrc.ts │ ├── isEncoded.ts │ ├── isOnline.ts │ ├── isPackaged.js │ ├── loadFile.ts │ ├── loadUrls.ts │ ├── log.ts │ ├── paths.js │ ├── pkginfo.js │ ├── proxy.ts │ ├── randInt.ts │ ├── reducerPrefixer.ts │ ├── saveFile.ts │ ├── sleep.ts │ ├── storageHelper.ts │ ├── styleResets.ts │ ├── testProxies.ts │ ├── titlebarDoubleClick.ts │ ├── trimFat.ts │ ├── url.ts │ └── validateTasks.ts ├── autosolve-client.d.ts ├── babel.config.js ├── babelx.js ├── chrome-paths.d.ts ├── config/ │ └── env/ │ ├── env.dev.js │ ├── env.prod.js │ └── index.js ├── electron-builder.yml ├── internals/ │ └── scripts/ │ ├── AfterPack.js │ ├── CheckBuiltsExist.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ ├── CheckYarn.js │ └── Notarize.js ├── jest.config.js ├── jest.setup.js ├── jsdom-extra.d.ts ├── package.json ├── preloads/ │ ├── 3ds.js │ ├── auth.js │ └── harvester.js ├── test/ │ ├── babel-transformer.js │ ├── babelRegisterTs.js │ ├── debug.ts │ ├── mockOffsetSize.tsx │ ├── polyfill.js │ └── testUtils.tsx ├── tsconfig.json └── webpack/ ├── config.base.js ├── config.eslint.js ├── config.main.prod.babel.js ├── config.renderer.dev.babel.js ├── config.renderer.dev.dll.babel.js └── config.renderer.prod.babel.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules # OSX .DS_Store # flow-typed flow-typed/npm/* !flow-typed/npm/module_vx.x.x.js # App packaged release app/*.prod.js app/style.css app/style.css.map dist dll main.js main.js.map .idea npm-debug.log.* __snapshots__ # Package.json package.json .travis.yml .idea vendors build docs .vscode .github app/dll .prettierrc .stylelintrc .eslintrc.json ================================================ FILE: .eslintrc.js ================================================ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', allowImportExportEverywhere: true, ecmaVersion: 10, ecmaFeatures: { modules: true, }, }, env: { browser: true, node: true, jest: true, es6: true, }, plugins: ['import', 'promise', 'compat', 'react', '@typescript-eslint'], extends: ['airbnb', 'plugin:prettier/recommended', 'prettier/react'], settings: { 'import/resolver': { webpack: { config: 'webpack/config.eslint.js', }, node: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, 'eslint-import-resolver-typescript': true, }, }, rules: { 'linebreak-style': 0, 'arrow-parens': 'off', 'compat/compat': 'error', 'consistent-return': 'off', 'comma-dangle': 'off', 'generator-star-spacing': 'off', 'import/no-unresolved': 'error', 'import/extensions': [ 'error', 'always', { ts: 'never', tsx: 'never', js: 'never', jsx: 'never', }, ], 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 'jsx-a11y/anchor-is-valid': 'off', 'jsx-a11y/label-has-for': 'off', 'jsx-a11y/no-noninteractive-element-interactions': 'off', 'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/no-static-element-interactions': 'off', 'jsx-a11y/click-events-have-key-events': 'off', 'no-console': [ 'error', { allow: ['info', 'error', 'warn'], }, ], 'no-use-before-define': 'off', 'no-multi-assign': 'off', 'prettier/prettier': ['error', { singleQuote: true }], 'promise/param-names': 'error', 'promise/always-return': 'error', 'promise/catch-or-return': 'error', 'promise/no-native': 'off', 'react/sort-comp': [ 'error', { order: ['type-annotations', 'static-methods', 'lifecycle', 'everything-else', 'render'], }, ], 'react/jsx-no-bind': 'off', 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], 'react/prefer-stateless-function': 'off', strict: 'off', 'import/prefer-default-export': 'off', 'arrow-body-style': 'off', 'no-underscore-dangle': 'off', 'class-methods-use-this': 'off', 'no-shadow': 'off', 'react/prop-types': 'off', 'import/no-dynamic-require': 'off', 'no-unused-vars': 'off', //use typescript version. 'no-restricted-syntax': 1, '@typescript-eslint/no-unused-vars': [ 'error', // setting this to warn for now... // TODO fix these when we finish typescript migration. { args: 'after-used', argsIgnorePattern: '^(args|theme|props|state|ownProps|dispatch|getState)|_', varsIgnorePattern: '^(args|variables|mixins|args|log)', }, ], }, globals: { fetchMock: true, }, }; ================================================ FILE: .gitattributes ================================================ * text eol=lf *.png binary *.jpg binary *.jpeg binary *.ico binary *.icns binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Expected behavior** If you're describing a bug, tell us what should happen. If you're suggesting a change/improvement, tell us how it should work. **Current Behavior** If describing a bug, tell us what happens instead of the expected behavior. If suggesting a change/improvement, explain the difference between current behavior. **Possible Solution** Not obligatory, but suggest a fix/reason for the bug or ideas how to implement the addition or change. **Steps to Reproduce** Provide a link to a live example or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant 1. 2. 3. 4. **Environment** Include as many relevant details about the environment you experienced the bug in - Node version : - Version or Branch used : - Operating System and version [e.g. macOS 10.14 Mojave]: - App Version [e.g. v2.0.0]: **Screenshots / Recordings** If applicable, add screenshots to help explain your problem. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: You want something added to the app or code. title: '' labels: enhancement assignees: '' --- ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release .eslintcache # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules/ app/node_modules # OSX .DS_Store # flow-typed flow-typed/npm/* !flow-typed/npm/module_vx.x.x.js # App packaged release app/build app/*.prod.js app/renderer.prod.js app/style.css dist dll main.js main.js.map .idea npm-debug.log.* package-lock.json app/certs/*.pem app/certs/* certs/* todo.txt *yarn-error.log eslint-common-rules.txt build/entitlements.mas.plist *embedded.provisionprofile junit.xml .VSCodeCounter electron ================================================ FILE: .prettierrc ================================================ { "overrides": [ { "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"], "options": { "parser": "json" } } ], "singleQuote": true, "trailingComma": "none", "arrowParens": "avoid", "endOfLine": "auto" } ================================================ FILE: .stylelintrc ================================================ { "extends": ["stylelint-config-standard", "stylelint-config-prettier"], "rules": { "at-rule-no-unknown": null, "no-descending-specificity": null } } ================================================ FILE: app/3ds.html ================================================
================================================ FILE: app/Harvester.html ================================================ Captcha Harvester
Minimize
Close

Default

Captcha Harvester

================================================ FILE: app/Question.html ================================================ Nebula Omega
================================================ FILE: app/__mocks__/dns.tsx ================================================ const dns = { lookup: (_, cb) => cb(null) }; export default dns; ================================================ FILE: app/__mocks__/electron-ga.tsx ================================================ class Analytics { send = jest.fn(); } export default Analytics; ================================================ FILE: app/__mocks__/electron.tsx ================================================ import createIPCMock from 'electron-mock-ipc'; const mocked = createIPCMock(); export const { ipcMain, ipcRenderer } = mocked; export const app = { getPath: jest.fn(name => { return name; }), getName: jest.fn(), getVersion: jest.fn() }; export const require = jest.fn(); export const match = jest.fn(); export const remote = { app }; export const dialog = jest.fn(); ================================================ FILE: app/__mocks__/fileMock.tsx ================================================ module.exports = {}; ================================================ FILE: app/__mocks__/request-promise.tsx ================================================ // import { CoreOptions, Request } from 'request'; // import { debugConsole } from '../../test/debug'; // const requestPromise = jest.requireActual('request-promise'); // const mockRequest = async ( // uri: string, // options: CoreOptions // ): Promise => { // // eslint-disable-next-line // debugConsole({ // mockRequest: true, // uri, // method: options.method, // headers: options.headers, // json: options.json, // body: options.body // }); // return requestPromise(uri, options); // }; // const mockJar = () => { // return { // setCookie: jest.fn(), // getCookiesString: jest.fn(() => 'cookies'), // getCookies: jest.fn(() => []) // }; // }; // const request = jest.fn(mockRequest); // request.jar = jest.fn(mockJar); // const mockResponseOnce = fn => request.mockImplementationOnce(fn); // request.mockResponseOnce = mockResponseOnce; // request.jar.mockJarOnce = fn => request.jar.mockImplementation(fn); // export default request; ================================================ FILE: app/__mocks__/styleMock.tsx ================================================ module.exports = {}; ================================================ FILE: app/api/sys/fileOps.ts ================================================ import { existsSync as _existsSync, writeFile as _writeFileAsync, appendFile as _appendFileAsync, readFileSync as _readFileSync, writeFileSync as _writeFileSync } from 'fs'; import { EOL } from 'os'; import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; export const writeFileAsync = (filePath: string, text: string) => { const options = { mode: 0o755 }; _writeFileAsync(filePath, text, options, err => { if (err) { console.error(err, `writeFileAsync`); } }); }; export const writeFileSync = (filePath: string, text: string) => { const options = { mode: 0o755 }; try { _writeFileSync(filePath, text, options); } catch (err) { console.error(err, `writeFileSync`); } }; export const appendFileAsync = (filePath: string, text: string) => { const options = { mode: 0o755 }; _appendFileAsync(filePath, text + EOL, options, err => { if (err) { console.error(err, `appendFileAsync`); } }); }; export const readFileSync = (filePath: string) => { const options = { encoding: 'utf8' }; return _readFileSync(filePath, options); }; export const fileExistsSync = (filePath: string) => _existsSync(filePath); export const createDirSync = (newFolderPath: string) => { mkdirp.sync(newFolderPath); }; export const deleteFilesSync = (filePath: string) => { rimraf.sync(filePath); }; ================================================ FILE: app/api/sys/index.ts ================================================ /* eslint no-await-in-loop: off */ import { readdir as fsReaddir, rename as fsRename, existsSync, statSync, lstatSync } from 'fs'; import Promise from 'bluebird'; import junk from 'junk'; import rimraf from 'rimraf'; import mkdirp from 'mkdirp'; import path from 'path'; import moment from 'moment'; import { exec } from 'child_process'; import findLodash from 'lodash/find'; import { log } from '../../utils/log'; import { isArray } from '../../utils/funcs'; const readdir = Promise.promisify(fsReaddir); const execPromise = Promise.promisify(exec); export const promisifiedExec = (command: string) => { try { return execPromise(command); } catch (e) { log.error(e); return null; } }; export const promisifiedExecNoCatch = (command: string) => execPromise(command); export const checkFileExists = async (filePath: string) => { try { if (typeof filePath === 'undefined' || filePath === null) { return false; } let _isArray = false; if (isArray(filePath)) { _isArray = true; } let fullPath = null; if (_isArray) { for (let i = 0; i < filePath.length; i += 1) { const item = filePath[i]; fullPath = path.resolve(item); if (existsSync(fullPath)) { return true; } } return false; } fullPath = path.resolve(filePath); return existsSync(fullPath); } catch (e) { log.error(e); return false; } }; /** Local device -> */ export const asyncReadLocalDir = async ({ filePath, ignoreHidden = true }: { filePath: string; ignoreHidden?: boolean; }) => { try { const response = []; const { error, data } = await readdir(filePath) .then((res: any) => { return { data: res, error: null }; }) .catch(e => { return { data: null, error: e }; }); if (error) { log.error(error, `asyncReadLocalDir`); return { error: true, data: null }; } let files = data; files = data.filter(junk.not); if (ignoreHidden) { files = data.filter((item: string) => !/(^|\/)\.[^\/\.]/g.test(item)); // eslint-disable-line no-useless-escape } for (let i = 0; i < files.length; i += 1) { const file = files[i]; const fullPath = path.resolve(filePath, file); if (!existsSync(fullPath)) { continue; // eslint-disable-line no-continue } const stat = statSync(fullPath); const isFolder = lstatSync(fullPath).isDirectory(); const extension = path.extname(fullPath); const { size, atime: dateTime } = stat; if (findLodash(response, { path: fullPath })) { continue; // eslint-disable-line no-continue } response.push({ name: file, path: fullPath, extension, size, isFolder, dateAdded: moment(dateTime).format('YYYY-MM-DD HH:mm:ss') }); } return { error, data: response }; } catch (e) { log.error(e); return { error: true, data: null }; } }; export const promisifiedRimraf = (item: any) => { try { return new Promise(resolve => { rimraf(item, {}, error => { resolve({ data: null, stderr: error, error }); }); }); } catch (e) { log.error(e); return null; } }; export const delLocalFiles = async ({ fileList }: { fileList: any[] }) => { try { if (!fileList || fileList.length < 1) { return { error: `No files selected.`, stderr: null, data: null }; } for (let i = 0; i < fileList.length; i += 1) { const item = fileList[i]; const { error } = await promisifiedRimraf(item); if (error) { log.error(`${error}`, `delLocalFiles -> rm error`); return { error, stderr: null, data: false }; } } return { error: null, stderr: null, data: true }; } catch (e) { log.error(e); return { error: e, stderr: null, data: false }; } }; const promisifiedRename = ({ oldFilePath, newFilePath }: { oldFilePath: string; newFilePath: string; }) => { try { return new Promise(resolve => { fsRename(oldFilePath, newFilePath, error => { resolve({ data: null, stderr: error, error }); }); }); } catch (e) { log.error(e); return null; } }; export const renameLocalFiles = async ({ oldFilePath, newFilePath }: { oldFilePath: string; newFilePath: string; }) => { try { if ( typeof oldFilePath === 'undefined' || oldFilePath === null || typeof newFilePath === 'undefined' || newFilePath === null ) { return { error: `No files selected.`, stderr: null, data: null }; } const { error } = await promisifiedRename({ oldFilePath, newFilePath }); if (error) { log.error(`${error}`, `renameLocalFiles -> mv error`); return { error, stderr: null, data: false }; } return { error: null, stderr: null, data: true }; } catch (e) { log.error(e); return { error: e, stderr: null, data: false }; } }; const promisifiedMkdir = ({ newFolderPath }: { newFolderPath: string }) => { try { return new Promise(resolve => { mkdirp(newFolderPath, error => { resolve({ data: null, stderr: error, error }); }); }); } catch (e) { log.error(e); return null; } }; export const newLocalFolder = async ({ newFolderPath }: { newFolderPath: string; }) => { try { if (typeof newFolderPath === 'undefined' || newFolderPath === null) { return { error: `Invalid path.`, stderr: null, data: null }; } const { error } = await promisifiedMkdir({ newFolderPath }); if (error) { log.error(`${error}`, `newLocalFolder -> mkdir error`); return { error, stderr: null, data: false }; } return { error: null, stderr: null, data: true }; } catch (e) { log.error(e); return { error: e, stderr: null, data: false }; } }; ================================================ FILE: app/app.html ================================================ Nebula Omega
================================================ FILE: app/auth.html ================================================ Nebula Omega

Welcome Back!

Please enter your license key below

================================================ FILE: app/classes/AppUpdate.ts ================================================ /* eslint-disable import/first */ import { app, dialog, BrowserWindow } from 'electron'; import { autoUpdater, CancellationToken } from 'electron-updater'; import { version } from '../../package.json'; app.getVersion = () => version; import { isConnected } from '../utils/isOnline'; import { log } from '../utils/log'; import { isPackaged } from '../utils/isPackaged'; import { PATHS } from '../utils/paths'; import { ENABLE_BACKGROUND_AUTO_UPDATE } from '../constants'; import { unixTimestampNow } from '../utils/date'; import { getMainWindow } from '../mainWindow/windows'; import { undefinedOrNull } from '../utils/funcs'; let progressbarWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow; const createChildWindow = () => { try { return new BrowserWindow({ parent: mainWindow, modal: true, show: false, height: 150, width: 600, title: 'Update downloading...', resizable: false, minimizable: false, maximizable: false, fullscreenable: false, movable: false, webPreferences: { nodeIntegration: true } }); } catch (e) { log.error(e, `AppUpdate -> createChildWindow`); return null; } }; const fireProgressbar = () => { try { if (progressbarWindow) { progressbarWindow.show(); progressbarWindow.focus(); return; } progressbarWindow = createChildWindow(); if (progressbarWindow) { progressbarWindow.loadURL(`${PATHS.loadUrlPath}#progressbarPage`); progressbarWindow.webContents.on('did-finish-load', () => { if (progressbarWindow) { progressbarWindow.show(); progressbarWindow.focus(); } }); progressbarWindow.on('closed', () => { progressbarWindow = null; }); progressbarWindow.on('unresponsive', (error: any) => { log.error(error, `AppUpdate -> progressbarWindow -> onerror`); }); } } catch (e) { log.error(e, `AppUpdate -> fireProgressbar`); } }; export default class AppUpdate { autoUpdater: any; domReadyFlag: boolean; updateInitFlag: boolean; updateForceCheckFlag: boolean; _errorDialog: { timeGenerated: number; title: string | null; message: string | null; }; cancellationToken: any; updateIsDownloading: any; updateIsActive: number; disableAutoUpdateCheck: boolean; constructor() { this.autoUpdater = autoUpdater; if (!isPackaged) { this.autoUpdater.updateConfigPath = PATHS.appUpdateFile; } // Just in case we end up attaching more than 10 listeners this.autoUpdater.setMaxListeners(Infinity); this.autoUpdater.autoDownload = ENABLE_BACKGROUND_AUTO_UPDATE; this.domReadyFlag = false; this.updateInitFlag = false; this.updateForceCheckFlag = false; this._errorDialog = { timeGenerated: 0, title: null, message: null }; this.cancellationToken = new CancellationToken(); this.updateIsDownloading = false; this.updateIsActive = 0; // 0 = no, 1 = update check in progress, -1 = update in progress this.disableAutoUpdateCheck = false; } init() { try { if (this.updateInitFlag) { return; } this.autoUpdater.on('error', (error: any) => { const errorMsg = error == null ? 'unknown' : (error.stack || error).toString(); if (progressbarWindow) { progressbarWindow.close(); } this.closeActiveUpdates(); if (this.isNetworkError(error)) { this.spitMessageDialog( 'Update Error', 'Oops.. A network error occured. Try again!', 'error' ); log.doLog(error); this.updateForceCheckFlag = false; this.disableAutoUpdateCheck = false; this.updateIsActive = 0; return; } if (/Cannot find channel/i.test(error)) { this.spitMessageDialog( 'No Updates Found', 'You have the latest version installed.', 'info' ); log.doLog(error); this.updateForceCheckFlag = false; this.disableAutoUpdateCheck = false; this.updateIsActive = 0; return; } this.spitMessageDialog( 'Update Error', 'Oops.. Some error occured while updating the app. Try again!', 'error' ); this.updateForceCheckFlag = false; this.disableAutoUpdateCheck = false; this.updateIsActive = 0; log.error(errorMsg, `AppUpdate -> onerror`); }); this.autoUpdater.on('update-available', async () => { if (progressbarWindow && this.updateIsActive !== -1) { progressbarWindow.close(); } const { response } = await dialog.showMessageBox({ type: 'info', title: 'Update Available', message: 'New version available. Download now?', buttons: ['Yes', 'No'] }); if (response === 0) { if (progressbarWindow) { progressbarWindow.close(); } this.closeActiveUpdates(-1); this.initDownloadUpdatesProgress(); this.autoUpdater.downloadUpdate(this.cancellationToken); } this.closeActiveUpdates(); this.updateForceCheckFlag = false; this.disableAutoUpdateCheck = false; this.updateIsActive = 0; }); this.autoUpdater.on('download-progress', (progress: any) => { if (!progressbarWindow) { return; } this.setUpdateProgressWindow({ value: progress.percent || 0 }); }); this.autoUpdater.on('update-downloaded', async () => { this.closeActiveUpdates(); if (progressbarWindow) { progressbarWindow.close(); } await dialog.showMessageBox({ type: 'info', title: 'Update Installed', message: 'Downloaded! Application will relaunch now.', buttons: ['Continue'] }); this.autoUpdater.quitAndInstall(); }); this.updateInitFlag = true; } catch (e) { log.error(e, `AppUpdate -> init`); } } pollForUpdates() { try { this.setMainWindow(); if (!mainWindow) { return; } isConnected() .then((connected: any) => { if (!connected) { return null; } if (this.updateIsActive === 1 || this.disableAutoUpdateCheck) { return null; } this.autoUpdater.on('update-not-available', () => { this.updateForceCheckFlag = false; this.disableAutoUpdateCheck = false; this.updateIsActive = 0; }); if (this.updateIsDownloading) { this.cancellationToken.cancel(); } this.autoUpdater.checkForUpdates(); this.updateIsActive = 1; return true; }) .catch(() => {}); } catch (e) { log.error(e, `AppUpdate -> checkForUpdates`); } } checkForUpdates() { try { this.setMainWindow(); if (!mainWindow) { return; } isConnected() .then((connected: any) => { if (!connected) { return null; } if (this.updateIsActive === 1 || this.disableAutoUpdateCheck) { return null; } this.autoUpdater.on('checking-for-update', () => { this.setCheckUpdatesProgress(); }); this.autoUpdater.on('update-not-available', () => { this.closeActiveUpdates(); if (progressbarWindow) { progressbarWindow.close(); } this.updateForceCheckFlag = false; this.disableAutoUpdateCheck = false; this.updateIsActive = 0; }); if (this.updateIsDownloading) { this.cancellationToken.cancel(); } this.autoUpdater.checkForUpdates(); this.updateIsActive = 1; return true; }) .catch(() => {}); } catch (e) { log.error(e, `AppUpdate -> checkForUpdates`); } } async forceCheck() { try { this.setMainWindow(); if (!mainWindow) { return; } if (!this.updateForceCheckFlag && this.updateIsActive !== 1) { this.autoUpdater.once('update-not-available', async () => { this.closeActiveUpdates(); if (progressbarWindow) { progressbarWindow.close(); } await dialog.showMessageBox({ title: 'No update found!', message: 'You have the latest version installed.', buttons: ['Close'] }); this.updateForceCheckFlag = false; this.disableAutoUpdateCheck = false; this.updateIsActive = 0; }); } if (this.updateIsActive === 1) { return; } if (this.updateIsActive === -1) { const { response } = await dialog.showMessageBox({ title: 'Update in progress', message: 'Another update is in progess. Are you sure want to restart the update?', buttons: ['Yes', 'No'] }); if (response === 0) { if (this.updateIsDownloading) { this.cancellationToken.cancel(); } this.autoUpdater.checkForUpdates(); this.updateIsActive = -1; } return; } if (this.updateIsDownloading) { this.cancellationToken.cancel(); } this.autoUpdater.checkForUpdates(); this.updateForceCheckFlag = true; this.disableAutoUpdateCheck = true; this.updateIsActive = 1; } catch (e) { log.error(e, `AppUpdate -> forceCheck`); } } setCheckUpdatesProgress() { try { isConnected() .then((connected: any) => { if (!connected) { this.spitMessageDialog( 'Checking For Updates', 'Internet connection is unavailable.' ); return null; } fireProgressbar(); this.setTaskBarProgressBar(2); if (progressbarWindow) { progressbarWindow.webContents.on('dom-ready', () => { if (progressbarWindow) { progressbarWindow.webContents.send( 'progressBarDataCommunication', { progressTitle: `Checking For Updates`, progressBodyText: `Please wait...`, value: 0, variant: `indeterminate` } ); } }); } return true; }) .catch(() => {}); } catch (e) { log.error(e, `AppUpdate -> setCheckUpdatesProgress`); } } initDownloadUpdatesProgress() { try { isConnected() .then((connected: any) => { if (!connected) { this.spitMessageDialog( 'Downloading Updates', 'Internet connection is unavailable.' ); return null; } fireProgressbar(); this.updateIsDownloading = true; this.domReadyFlag = false; this.setUpdateProgressWindow({ value: 0 }); return true; }) .catch(() => {}); } catch (e) { log.error(e, `AppUpdate -> initDownloadUpdatesProgress`); } } setUpdateProgressWindow({ value = 0 }) { try { const data = { progressTitle: `Downloading Updates`, progressBodyText: `Please wait...`, value, variant: `determinate` }; this.setTaskBarProgressBar(value / 100); if (this.domReadyFlag && progressbarWindow) { progressbarWindow.webContents.send( 'progressBarDataCommunication', data ); return; } if (progressbarWindow) { progressbarWindow.webContents.on('dom-ready', () => { if (progressbarWindow) { progressbarWindow.webContents.send( 'progressBarDataCommunication', data ); this.domReadyFlag = true; } }); } } catch (e) { log.error(e, `AppUpdate -> setUpdateProgressWindow`); } } setMainWindow() { const _mainWindow = getMainWindow(); if (!_mainWindow || undefinedOrNull(_mainWindow)) { return; } mainWindow = _mainWindow; } setTaskBarProgressBar(value: any) { try { if (mainWindow) { mainWindow.setProgressBar(value); } } catch (e) { log.error(e, `AppUpdate -> setTaskBarProgressBar`); } } isNetworkError(errorObj: any) { return ( errorObj.message === 'net::ERR_INTERNET_DISCONNECTED' || errorObj.message === 'net::ERR_PROXY_CONNECTION_FAILED' || errorObj.message === 'net::ERR_CONNECTION_RESET' || errorObj.message === 'net::ERR_CONNECTION_CLOSE' || errorObj.message === 'net::ERR_NAME_NOT_RESOLVED' || errorObj.message === 'net::ERR_CONNECTION_TIMED_OUT' ); } async spitMessageDialog(title: string, message: string, type = 'message') { const { timeGenerated: _timeGenerated } = this._errorDialog; const delayTime = 1000; if ( _timeGenerated !== 0 && _timeGenerated - unixTimestampNow() < delayTime ) { return; } this._errorDialog = { timeGenerated: unixTimestampNow(), title, message }; switch (type) { default: case 'message': await dialog.showMessageBox({ title, message, buttons: ['Close'] }); break; case 'error': dialog.showErrorBox(title, message); break; } } closeActiveUpdates(updateIsActive = 0) { this.setTaskBarProgressBar(-1); this.updateIsActive = updateIsActive; } } ================================================ FILE: app/classes/Boot.ts ================================================ /* eslint no-await-in-loop: off */ /** * Boot * Note: Don't import log helper file from utils here */ import { readdirSync } from 'fs'; import { baseName, PATHS } from '../utils/paths'; import { fileExistsSync, writeFileAsync, createDirSync, deleteFilesSync } from '../api/sys/fileOps'; import { daysDiff, yearMonthNow } from '../utils/date'; import { LOG_FILE_ROTATION_CLEANUP_THRESHOLD } from '../constants'; const { logFile, settingsFile, authFile, logDir } = PATHS; const logFileRotationCleanUpThreshold = LOG_FILE_ROTATION_CLEANUP_THRESHOLD; export default class Boot { verifyDirList: string[]; verifyFileList: string[]; settingsFile: string; authFile: string; constructor() { this.verifyDirList = [logDir]; this.verifyFileList = [logFile, settingsFile, authFile]; this.settingsFile = settingsFile; this.authFile = authFile; } async init() { try { for (let i = 0; i < this.verifyDirList.length; i += 1) { const item = this.verifyDirList[i]; if (!(await this.verifyDir(item))) { await this.createDir(item); } } if (!this.verifyFile(this.authFile)) { await this.createFile(this.authFile); } if (!this.verifyFile(this.settingsFile)) { await this.createFile(this.settingsFile); } for (let i = 0; i < this.verifyFileList.length; i += 1) { const item = this.verifyFileList[i]; if (!this.verifyFile(item)) { await this.createFile(item); } } return; } catch (e) { console.error(e); } } async verify() { try { for (let i = 0; i < this.verifyFileList.length; i += 1) { const item = this.verifyDirList[i]; if (!(await this.verifyDir(item))) { return; } } for (let i = 0; i < this.verifyFileList.length; i += 1) { const item = this.verifyFileList[i]; if (!this.verifyFile(item)) { return; } } return; } catch (e) { console.error(e); } } quickVerify() { try { for (let i = 0; i < this.verifyFileList.length; i += 1) { const item = this.verifyFileList[i]; if (!this.verifyFile(item)) { return false; } } return true; } catch (e) { console.error(e); return false; } } async verifyDir(filePath: string) { try { return fileExistsSync(filePath); } catch (e) { console.error(e); return null; } } async createDir(newFolderPath: string) { try { createDirSync(newFolderPath); } catch (e) { console.error(e); } } verifyFile(filePath: string) { try { return fileExistsSync(filePath); } catch (e) { console.error(e); return null; } } createFile(filePath: string) { try { writeFileAsync(filePath, ``); } catch (e) { console.error(e); } } cleanRotationFiles() { try { const dirFileList = readdirSync(logDir); const pattern = `^\\${baseName(logFile)}`; const _regex = new RegExp(pattern, 'gi'); const filesList = dirFileList.filter(elm => { return !elm.match(_regex); }); if (filesList === null || filesList.length < 1) { return null; } filesList.map(async a => { const dateMatch = a.match(/\d{4}-\d{2}/g); if ( dateMatch === null || dateMatch.length < 1 || typeof dateMatch[0] === 'undefined' || dateMatch[0] === null ) { return; } const _diff = daysDiff(yearMonthNow({}), dateMatch[0]); if (_diff >= logFileRotationCleanUpThreshold) { deleteFilesSync(`${logDir}/${a}`); } }); return null; } catch (e) { console.error(e); return null; } } } ================================================ FILE: app/classes/Notification.ts ================================================ import { PATHS } from '../utils/paths'; import { IS_DEV } from '../constants/env'; /* Cache of Audio elements, for instant playback */ const cache: any = {}; const VOLUME = 0.55; // these get packaged into renderer.prod.js which resides in the same filepath as the app.html const sounds: any = { DONE: { url: IS_DEV ? PATHS.successSoundPath : './success.mp3', volume: VOLUME }, HEADS_UP: { url: IS_DEV ? PATHS.notifySoundPath : './notify.mp3', volume: VOLUME } }; function preload() { // eslint-disable-next-line no-restricted-syntax for (const name in sounds) { if (!cache[name]) { const sound = sounds[name]; const audio = (cache[name] = new window.Audio()); audio.volume = sound.volume; audio.src = sound.url; } } } function play(name: string) { let audio = cache[name]; if (!audio) { const sound = sounds[name]; if (!sound) { throw new Error('Invalid sound name'); } audio = cache[name] = new window.Audio(); audio.volume = sound.volume || VOLUME; audio.src = sound.url; } audio.currentTime = 0; audio.play(); } export { preload, play }; ================================================ FILE: app/classes/ProxyTester.ts ================================================ import { isEmpty } from 'lodash'; import { ipcMain, IpcMainInvokeEvent, BrowserWindow } from 'electron'; import { IPCKeys } from '../constants/ipc'; import { Proxy } from '../components/Proxies/reducers/current'; import { testProxy } from '../utils/testProxies'; import { StaggeredQueue } from '../tasks/common/utils'; export default class ProxyTester { mainWindow: BrowserWindow | null; hasLogged: boolean; queue: StaggeredQueue; results: { [group: string]: { [ip: string]: { speed: number | 'failed'; }; }; }; interval: { [id: string]: any; }; constructor(mainWindow: BrowserWindow | null) { this.queue = new StaggeredQueue(false, 20); this.hasLogged = false; this.mainWindow = mainWindow; this.results = {}; this.interval = {}; ipcMain.on(IPCKeys.RequestTestProxy, this.insert); } process = async ({ group, id, url }: any) => { if (this.mainWindow) { const time = await testProxy(url, id); if (!this.results[group]) { this.results[group] = {}; } this.results[group][id] = time; } }; start = (group: string) => { if (!this.interval[group]) { this.interval[group] = setInterval(() => { if (isEmpty(this.results[group])) { return; } if (this.mainWindow) { this.mainWindow.webContents.send( IPCKeys.ResponseTestProxy, group, this.results[group] ); this.results[group] = {}; } }, 1000); } }; stop = (group: string) => { if (this.interval[group]) { clearInterval(this.interval[group]); this.interval[group] = null; } }; clear = () => { this.queue.clear(); return Object.keys(this.results).map((id: string) => this.stop(id)); }; remove = (group: string, ip: string) => { this.queue.removeJob(group, ip); }; insert = ( _: IpcMainInvokeEvent, id: string, url: string, proxies: Proxy[] ) => { const promises = proxies.map(({ ip }) => this.queue.add(id, ip, this.process, { url }) ); this.start(id); Promise.all(promises).catch(() => {}); }; } ================================================ FILE: app/classes/Storage.ts ================================================ import { log } from '../utils/log'; import { readFileSync, writeFileSync, deleteFilesSync } from '../api/sys/fileOps'; export default class Storage { filePath: string; constructor(filePath: string) { this.filePath = filePath; } getAll() { try { const _stream = readFileSync(this.filePath); if ( typeof _stream === 'undefined' || _stream === null || Object.keys(_stream).length < 1 ) { return {}; } return JSON.parse(_stream); } catch (e) { log.error(e, `Storage -> getAll`); } } getItem(key: string) { try { if (typeof key === 'undefined' || key === null) { return null; } // get all items const allItems = this.getAll(); if (allItems[key]) { return allItems[key]; } return null; } catch (e) { log.error(e, 'Storage -> getItem'); } } getItems(keys: any) { try { if (typeof keys === 'undefined' || keys === null || keys.length < 0) { return {}; } const allItem = this.getAll(); const _return: any = {}; // eslint-disable-next-line array-callback-return keys.map((a: any) => { if (typeof allItem[a] === 'undefined' || allItem[a] === null) { return; } _return[a] = allItem[a]; }); return _return; } catch (e) { log.error(e, `Storage -> getAll`); return null; } } setAll({ ...data }) { try { writeFileSync(this.filePath, JSON.stringify({ ...data })); } catch (e) { log.error(e, `Storage -> setAll`); } } set(key: string, value: any) { try { // get all items const allItems = this.getAll(); // update / set the item allItems[key] = value; // write back to file this.setAll({ ...allItems }); } catch (e) { log.error(e, 'Storage -> set'); } } delete() { try { deleteFilesSync(this.filePath); } catch (e) { log.error(e, 'Storage -> delete'); } } } ================================================ FILE: app/components/Analytics/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../LoadingIndicator'; /* eslint import/no-cycle: [2, { maxDepth: 1 }] */ export default Loadable(() => import('./index'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Analytics/__tests__/Analytics.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import Analytics from '../index'; import { withProviders } from '../../../../test/testUtils'; const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('should render Analytics', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); const Root = withProviders({ Component: Analytics }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Welcome Back,')).toBeDefined(); }); ================================================ FILE: app/components/Analytics/actions/checkouts.tsx ================================================ import prefixer from '../../../utils/reducerPrefixer'; const prefix = '@@Checkouts'; const checkoutTypesList = ['ADD']; export const checkoutActions = checkoutTypesList.map(t => `${prefix}/${t}`); export const checkoutActionTypes = prefixer(prefix, checkoutTypesList); export const addCheckout = checkout => { return { type: checkoutActionTypes.ADD, payload: checkout }; }; ================================================ FILE: app/components/Analytics/actions/index.tsx ================================================ import { checkoutActionTypes, checkoutActions } from './checkouts'; import { newsActionTypes, newsActions } from './news'; export { checkoutActionTypes, newsActionTypes, checkoutActions, newsActions }; ================================================ FILE: app/components/Analytics/actions/news.tsx ================================================ import prefixer from '../../../utils/reducerPrefixer'; const prefix = '@@News'; const newsTypesList = ['ADD']; export const newsActions = newsTypesList.map(t => `${prefix}/${t}`); export const newsActionTypes = prefixer(prefix, newsTypesList); export const addNews = news => { return { type: newsActionTypes.ADD, payload: news }; }; ================================================ FILE: app/components/Analytics/components/checkouts.tsx ================================================ import React from 'react'; import classnames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import { FixedSizeList as List } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import { Typography, Grid, Tooltip, Fade } from '@material-ui/core'; import { orderBy, isPlainObject } from 'lodash'; import { useConfirm } from 'material-ui-confirm'; import FileCopyIcon from '@material-ui/icons/FileCopy'; import DoneIcon from '@material-ui/icons/Done'; import { makeStyles } from '@material-ui/styles'; import NoChildrenComponent from '../../NoChildrenComponent'; import { imageForCard } from '../../../utils/imgForCardType'; import { styles } from '../styles/checkouts'; import { setAnalyticsFile } from '../../Settings/actions'; import { loadCSVFile } from '../../../utils/loadFile'; import { RootState } from '../../../store/reducers'; const useStyles = makeStyles(styles); const CheckoutComponent = ({ checkout, style }: { checkout: any; style: any; }) => { const styles = useStyles(); let storeValue = checkout.store; if (isPlainObject(storeValue)) { storeValue = checkout.store.name; } let productValue = checkout.product; if (isPlainObject(productValue)) { productValue = checkout.product.name; } let cardValue = checkout.card; if (!cardValue && checkout.profile) { cardValue = checkout.profile.type; } return ( *': { margin: '0 16px 0 0', overflow: 'hidden' } }} component="div" variant="body1" > {storeValue} {productValue} {cardValue ? ( ) : null} ); }; type RowProps = { data: any[]; index: number; style: any; }; const Row = ({ data, index, style }: RowProps) => { const checkout = data[index]; return ( ); }; const CheckoutsLoader = ({ checkouts }: { checkouts: any[] }) => ( {({ height, width }) => ( {Row} )} ); const CheckoutsGrid = ({ checkouts }: { checkouts: any[] }) => { if (!checkouts.length) { return ; } return ; }; const CheckoutsComponent = () => { const styles = useStyles(); const dispatch = useDispatch(); const confirm = useConfirm(); const file = useSelector((state: RootState) => state.Settings.analyticsFile); const checkouts = useSelector((state: RootState) => orderBy( state.Checkouts.filter((checkout: any) => checkout.success), checkout => Number(checkout.date), 'desc' ) ); const attachFile = async () => { if (file) { try { await confirm({ title: `Are you sure you want to detach the analytics file?`, description: `File location: ${file}`, confirmationText: 'Yes', cancellationText: 'No', dialogProps: { classes: { paper: styles.paperRoot } }, confirmationButtonProps: { classes: { root: styles.confirmBtn }, style: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' } }, cancellationButtonProps: { classes: { root: styles.cancelBtn }, style: { width: 105, height: 35 } } }); return dispatch(setAnalyticsFile('')); } catch (e) { // noop... } return null; } const path = await loadCSVFile(); if (!path) { return; } return dispatch(setAnalyticsFile(path)); }; return (
{file ? ( ) : ( )} {checkouts.length} Total{' '} {!checkouts.length || checkouts.length > 1 ? 'Checkouts' : 'Checkout'}
); }; export default CheckoutsComponent; ================================================ FILE: app/components/Analytics/components/expenses.tsx ================================================ /* eslint-disable no-console */ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import moment from 'moment'; import classNames from 'classnames'; import { groupBy } from 'lodash'; import ReactApexChart from 'react-apexcharts'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Typography, Grid, Select, MenuItem } from '@material-ui/core'; import { makeStyles } from '@material-ui/styles'; import { RootState } from '../../../store/reducers'; import { EXPENSES_OPTIONS_LIGHT, EXPENSES_OPTIONS_DARK, VIEWS } from '../../../constants'; import { makeExpensesView } from '../../Settings/selectors'; import { setField, SETTINGS_FIELDS } from '../../Settings/actions'; import { styles } from '../styles/expenses'; const useStyles = makeStyles(styles); const group = (data: any) => groupBy(data, ({ date }) => moment(Number(date)).day()); const strip = (currency: string | number) => Number(currency?.toString().replace(/[^0-9.-]+/g, '')); const buildData = (success: any[]) => { const successData = group(success); const data = []; // loop over each day of the week... for (let i = 0; i < 7; i += 1) { if (successData[i]) { data.push( Number( successData[i].reduce( (prev, curr) => prev + strip(curr.amount || curr.product?.price), 0 ) ).toFixed(2) ); } else { data.push(0); } } return data; }; const ExpensesComponent = () => { const styles = useStyles(); const dispatch = useDispatch(); const expensesView = useSelector(makeExpensesView); const theme = useSelector((state: RootState) => state.Theme); const checkouts = useSelector((state: RootState) => state.Checkouts.filter((checkout: any) => checkout.success).filter( (checkout: any) => moment(Number(checkout.date)).unix() >= moment().subtract(expensesView, 'days').unix() ) ); const total = checkouts.reduce( (prev: any, curr: any) => prev + strip(curr?.amount || curr?.product?.price), 0 ); const seriesOptions = theme === 0 ? EXPENSES_OPTIONS_LIGHT : EXPENSES_OPTIONS_DARK; const series = [{ name: '', data: buildData(checkouts) }]; return (
Money Spent in USD ${new Intl.NumberFormat('en-US').format(total)} Total
); }; export default ExpensesComponent; ================================================ FILE: app/components/Analytics/components/news.tsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import { sortBy } from 'lodash'; import moment from 'moment'; import { Typography, Grid } from '@material-ui/core'; import { makeStyles } from '@material-ui/styles'; import NoChildrenComponent from '../../NoChildrenComponent'; import { styles } from '../styles/news'; const typeColors: any = { UPDATE: '#58D67D', DROP: '#9389F9' }; const messages: any = { UPDATE: ' - Update - ', DROP: ' - Drop - ' }; const useStyles = makeStyles(styles); const NewsRow = ({ news: { date, type, message }, styles }: { news: any; styles: any; }) => { return (
{moment(Number(date)).format('MM/DD')} {messages[type]} {message} ); }; const NewsGrid = () => { const styles = useStyles(); const news = useSelector(({ News }: { News: any[] }) => News); if (!news.length) { return ; } return ( <> {sortBy(news, n => Number(n.date)) .reverse() .map(n => ( ))} ); }; const NewsComponent = () => { const styles = useStyles(); return (
News
); }; export default NewsComponent; ================================================ FILE: app/components/Analytics/components/orders.tsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import { Typography, Grid } from '@material-ui/core'; import LocalShippingIcon from '@material-ui/icons/LocalShipping'; import { makeStyles } from '@material-ui/styles'; import NoChildrenComponent from '../../NoChildrenComponent'; import { imageForCard } from '../../../utils/imgForCardType'; import { styles } from '../styles/orders'; import { RootState } from '../../../store/reducers'; const useStyles = makeStyles(styles); const OrderComponent = ({ order: { store, product, card }, styles }: { order: any; styles: any; }) => ( {store} {product} ); const OrdersGrid = ({ orders, styles }: { orders: any[]; styles: any }) => { if (!orders.length) { return ; } return (
{orders.map(order => ( ))}
); }; const OrdersComponent = () => { const styles = useStyles(); const orders = useSelector((state: RootState) => state.Checkouts.filter((checkout: any) => checkout.delivered) ); return (
{orders.length} {!orders.length || orders.length > 1 ? 'Orders' : 'Order'}{' '} Received
); }; export default OrdersComponent; ================================================ FILE: app/components/Analytics/components/stats.tsx ================================================ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-console */ import React, { useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import moment from 'moment'; import { groupBy } from 'lodash'; import ReactApexChart from 'react-apexcharts'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Typography, Grid, Select, MenuItem } from '@material-ui/core'; import { createSelector } from 'reselect'; import { makeStyles } from '@material-ui/styles'; import { RootState } from '../../../store/reducers'; import { STATS_OPTIONS_LIGHT, STATS_OPTIONS_DARK, VIEWS } from '../../../constants'; import { setField, SETTINGS_FIELDS } from '../../Settings/actions'; import { styles } from '../styles/stats'; const group = (data: any) => groupBy(data, ({ date }) => moment(Number(date)).startOf('day').unix()); const useStyles = makeStyles(styles); const getCheckouts = (state: any) => state.Checkouts; const getStatsView = (state: any) => state.Settings.statsView; const getCheckoutsForStatsView = createSelector( [getCheckouts, getStatsView], (checkouts, statsView) => { return checkouts.filter( (checkout: any) => moment(Number(checkout.date)).unix() >= moment().subtract(Number(statsView), 'days').unix() ); } ); const extractSuccessData = (success: any[], statsView: string) => { const successData = Array.from(Array(Number(statsView)), () => 0); for (const [time, checkouts] of Object.entries(group(success))) { if (checkouts?.length) { successData[daysFrom(now, Number(time)) - 1] = checkouts.length; } } return successData.reverse(); }; const extractFailedData = (failed: any[], statsView: string) => { const failedData = Array.from(Array(Number(statsView)), () => 0); for (const [time, checkouts] of Object.entries(group(failed))) { if (checkouts?.length) { failedData[daysFrom(now, Number(time)) - 1] = checkouts.length; } } return failedData.reverse(); }; const daysFrom = (start: number, end: number) => Math.ceil( moment.duration(moment.unix(start).diff(moment.unix(end))).asDays() ); const now = moment().unix(); const StatsComponent = () => { const styles = useStyles(); const dispatch = useDispatch(); const theme = useSelector((state: RootState) => state.Theme); const statsView = useSelector((state: RootState) => state.Settings.statsView); const checkouts = useSelector(getCheckoutsForStatsView); const failed = checkouts.filter((checkout: any) => !checkout.success); const success = checkouts.filter((checkout: any) => checkout.success); const successData = useCallback(extractSuccessData(success, statsView), [ success.length, statsView ]); const failedData = useCallback(extractFailedData(failed, statsView), [ failed.length, statsView ]); const seriesOptions = theme === 0 ? STATS_OPTIONS_LIGHT : STATS_OPTIONS_DARK; const series = [ { name: 'Success', data: successData }, { name: 'Failed', data: failedData } ]; const _setStatsView = (option: string) => { dispatch(setField(SETTINGS_FIELDS.STATS_VIEW, option)); }; return (
Checkout Statistics
); }; export default StatsComponent; ================================================ FILE: app/components/Analytics/components/welcome.tsx ================================================ import React from 'react'; import { PURGE } from 'redux-persist'; import { useSelector, useDispatch } from 'react-redux'; import { useSpring, animated } from 'react-spring'; import { Typography, Button, Grid } from '@material-ui/core'; import { useConfirm } from 'material-ui-confirm'; import { makeStyles } from '@material-ui/styles'; import { log } from '../../../utils/log'; import { quit } from '../../../utils/createWindows'; import { makeUser } from '../../App/selectors'; import { styles } from '../styles/welcome'; const useStyles = makeStyles(styles); const AnimatedWelcomeText = () => { const styles = useStyles(); const { name } = useSelector(makeUser); const props = useSpring({ delay: 500, opacity: 1, from: { opacity: 0 }, config: { duration: 850 } }); return ( Welcome Back, {name} ); }; const AnimatedActionButtons = () => { const props = useSpring({ opacity: 1, from: { opacity: 0 }, config: { duration: 850 } }); const styles = useStyles(); const dispatch = useDispatch(); const confirm = useConfirm(); const deactivateHandler = async (e: any) => { e.stopPropagation(); try { await confirm({ title: `Are you sure you want to deactivate?`, description: 'Your data will persist, but you will need to authenticate on next launch.', confirmationText: 'Yes', cancellationText: 'No', dialogProps: { classes: { paper: styles.paperRoot } }, confirmationButtonProps: { classes: { root: styles.confirmBtn }, style: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' } }, cancellationButtonProps: { classes: { root: styles.cancelBtn }, style: { width: 105, height: 35 } } }); } catch (e) { if (!e) { return; } log.error(e, 'Welcome -> Deactivate'); } }; const handleReset = async () => { try { await confirm({ title: `Are you sure you want to reset application data?`, description: 'This action cannot be undone.', confirmationText: 'Yes', cancellationText: 'No', dialogProps: { classes: { paper: styles.paperRoot } }, confirmationButtonProps: { classes: { root: styles.confirmBtn }, style: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' } }, cancellationButtonProps: { classes: { root: styles.cancelBtn }, style: { width: 105, height: 35 } } }); dispatch({ type: PURGE, key: 'root', result: () => null }); setTimeout(() => quit(), 2500); } catch (e) { if (!e) { return; } log.error(e, 'Tasks -> Reset All Data'); } }; return ( ); }; const WelcomeComponent = () => { const styles = useStyles(); return (
); }; export default WelcomeComponent; ================================================ FILE: app/components/Analytics/index.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { Grid } from '@material-ui/core'; import { styles } from './styles'; // first row import WelcomeComponent from './components/welcome'; import CheckoutsComponent from './components/checkouts'; import OrrdersComponent from './components/orders'; // second row import ExpensesComponent from './components/expenses'; const useStyles = makeStyles(styles); const Analytics = () => { const styles = useStyles(); return (
); }; export default Analytics; ================================================ FILE: app/components/Analytics/reducers/checkouts.tsx ================================================ import { PURGE } from 'redux-persist'; import { checkoutActionTypes } from '../actions/checkouts'; type Action = { type: string; payload?: any; }; export type Checkouts = []; export const initialState: Checkouts = []; export function Checkouts(state = initialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...initialState]; case checkoutActionTypes.ADD: { if (!payload) { return state; } return payload; } default: return state; } } ================================================ FILE: app/components/Analytics/reducers/index.tsx ================================================ import { Checkouts } from './checkouts'; import { News } from './news'; export { Checkouts, News }; ================================================ FILE: app/components/Analytics/reducers/news.tsx ================================================ import { PURGE } from 'redux-persist'; import { newsActionTypes } from '../actions'; type Action = { type: string; payload?: any; }; export type NewsObject = {}; export type News = NewsObject[]; export const newsInitialState: News = []; export function News(state = newsInitialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...newsInitialState]; case newsActionTypes.ADD: { if (!payload) { return state; } return payload; } default: return state; } } ================================================ FILE: app/components/Analytics/styles/checkouts.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { container: { maxHeight: '100%' }, cardType: { filter: `contrast(${theme.palette.primary.contrast})` }, root: { backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, height: '100%', margin: '0 10px', flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'column', borderRadius: 8, transition: theme.transitions.create(['background-color'], { duration: 300 }) }, row: { flexDirection: 'row' }, table: { display: 'table', tableLayout: 'fixed', width: '100%' }, tableRow: { display: 'table-row' }, margin: { width: '100%', height: 24, display: 'flex' }, gridTop: { marginBottom: 8 }, gridBottom: { height: '100%', overflowY: 'scroll', marginBottom: 8, '&::-webkit-scrollbar': { display: 'none !important' } }, paperRoot: { backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, confirmBtn: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' }, cancelBtn: { width: 105, height: 35, background: theme.palette.primary.secondary, color: theme.palette.primary.color, '&:hover': { opacity: 0.5, background: theme.palette.primary.secondary, color: theme.palette.primary.color } }, activeIcon: { color: '#4BB543 !important', transition: theme.transitions.create(['color'], { duration: 300 }), '&:hover': { opacity: 0.5, transition: theme.transitions.create(['color'], { duration: 300 }), color: '#B33A3A !important' } }, grid: { width: `100%` }, store: { display: 'table-cell', whiteSpace: 'nowrap', alignSelf: 'center', fontSize: 10, margin: 'auto 8px auto 16px', width: '20%', overflow: 'hidden' }, product: { display: 'table-cell', whiteSpace: 'nowrap', alignSelf: 'center', fontSize: 10, margin: 'auto 8px', width: '100%', overflow: 'hidden' }, card: { display: 'table-cell', alignSelf: 'center', fontSize: 10, margin: 'auto 8px' }, icon: { background: '#EEEDFC', width: 40, height: 40, borderRadius: '50%', margin: '16px 8px 0 8px' }, iconSvg: { display: 'block', margin: 'auto', marginTop: 8, color: '#8377F4', cursor: 'pointer' }, bold: { fontWeight: 700, whiteSpace: 'pre' }, welcome: { color: theme.palette.primary.color, display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px' } }; }; ================================================ FILE: app/components/Analytics/styles/expenses.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { gridItem: { height: '100%' }, root: { backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, height: '100%', marginRight: 10, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'column', borderRadius: 8, transition: theme.transitions.create(['background-color'], { duration: 300 }) }, dropdownIcon: { color: theme.palette.primary.color, fontSize: 16, marginTop: 4 }, menuList: { fontSize: 10, padding: 0, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, chartContainer: { width: '100%', height: '200px', position: 'relative' }, row: { flexDirection: 'row' }, grid: { width: `100%` }, noPadding: { padding: 0 }, bold: { fontWeight: 700, whiteSpace: 'pre' }, colContainer: { justifyContent: 'center', textAlign: 'center' }, firstCol: { display: 'inline-flex', flex: 1 }, secondCol: { display: 'inline-flex', justifyContent: 'center', textAlign: 'center', flex: 1 }, thirdCol: { display: 'inline-flex', textAlign: 'right', alignSelf: 'flex-start', flex: 1 }, header: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px', marginLeft: '8px', fontWeight: 500, fontSize: '16px' }, menuItem: { padding: '4px 8px', fontSize: 10, textAlign: 'center' }, subtext: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginLeft: '8px', fontWeight: 400, fontSize: '12px' }, totalText: { display: 'flex', margin: '16px auto', fontWeight: 700, fontSize: '12px', textAlign: 'center' }, selectPeriod: { minWidth: 83, fontSize: 12, padding: 0, color: theme.palette.primary.color }, subselect: { display: 'flex', color: theme.palette.primary.color, margin: '5px 8px 0 auto', fontWeight: 700, fontSize: '12px', textAlign: 'right' } }; }; ================================================ FILE: app/components/Analytics/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { margin: 35, marginTop: 75, display: 'flex', flexDirection: 'column', width: '100%', height: 'calc(100% - 100px)' }, row: { height: '100%' }, grid: { margin: '10px 10px 10px 0', height: '30%' } }; }; ================================================ FILE: app/components/Analytics/styles/news.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { gridItem: { height: '100%' }, root: { backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, height: '100%', marginLeft: 10, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'column', borderRadius: 8, transition: theme.transitions.create(['background-color'], { duration: 300 }) }, row: { flexDirection: 'row' }, date: { fontWeight: 700, fontSize: 12, display: 'inline-flex' }, messageBuffer: { fontWeight: 500, fontSize: 12, display: 'inline', fontStyle: 'italic' }, message: { fontWeight: 500, fontSize: 12, display: 'inline', wordWrap: 'break-word', overflowWrap: 'break-word' }, flexFirst: { // margin: 'auto 0' }, newsItem: { margin: '4px 16px', width: 'calc(100% - 32px)', height: 'auto', display: 'flex' }, grid: { width: `100%` }, gridBottom: { overflowY: 'scroll', flexWrap: 'nowrap', height: '100%', display: 'flex', margin: '8px 0', flexDirection: 'column', '&::-webkit-scrollbar': { display: 'none !important' } }, status: { height: 8, width: 8, margin: '0 8px 0 0', display: 'inline-flex', borderRadius: '50%' }, bold: { fontWeight: 700, whiteSpace: 'pre' }, colContainer: { justifyContent: 'center', textAlign: 'center' }, firstCol: { display: 'inline-flex', flex: 1 }, header: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px', marginLeft: '8px', fontWeight: 500, fontSize: '16px' }, welcome: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px' } }; }; ================================================ FILE: app/components/Analytics/styles/orders.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { container: { maxHeight: '100%' }, root: { backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, height: '100%', marginLeft: 10, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'column', borderRadius: 8, transition: theme.transitions.create(['background-color'], { duration: 300 }) }, row: { flexDirection: 'row' }, table: { display: 'table', tableLayout: 'fixed', width: '100%' }, tableRow: { display: 'table-row' }, margin: { width: '100%', height: 24, display: 'flex' }, gridTop: { marginBottom: 8 }, gridBottom: { overflowY: 'scroll', height: '100%', marginBottom: 8, '&::-webkit-scrollbar': { display: 'none !important' } }, grid: { width: `100%` }, store: { display: 'table-cell', whiteSpace: 'nowrap', alignSelf: 'center', fontSize: 10, margin: 'auto 8px auto 16px', width: '20%', overflow: 'hidden' }, product: { display: 'table-cell', whiteSpace: 'nowrap', alignSelf: 'center', fontSize: 10, margin: 'auto 8px', width: '100%', overflow: 'hidden' }, card: { display: 'table-cell', alignSelf: 'center', fontSize: 10, margin: 'auto 8px' }, icon: { background: '#FFEDDC', width: 40, height: 40, borderRadius: '50%', margin: '16px 8px 0 8px' }, iconSvg: { display: 'block', margin: 'auto', marginTop: 8, color: '#FFB15E' }, bold: { fontWeight: 700, whiteSpace: 'pre' }, welcome: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px' } }; }; ================================================ FILE: app/components/Analytics/styles/shipments.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { loadingSpinner: { alignItems: 'center', justifyContent: 'center', marginLeft: '50%', marginTop: '10%' }, gridItem: { height: '100%' }, root: { backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, height: '100%', marginLeft: 10, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'column', borderRadius: 8, transition: theme.transitions.create(['background-color'], { duration: 300 }) }, hide: { display: 'none' }, row: { flexDirection: 'row' }, text: { fontSize: 10, textAlign: 'center', color: theme.palette.primary.color, margin: 'auto 0', '& > *': { textAlign: 'center' } }, addBtn: { height: 21, width: 21, backgroundColor: '#8E83F4', borderRadius: '50%', color: '#fff', fontSize: 10, '&:hover': { backgroundColor: 'rgba(142 ,131, 244, 0.555)' } }, searchRoot: { border: `1px solid ${theme.palette.primary.border}`, backgroundColor: theme.palette.primary.background, boxShadow: 'none' }, grid: { width: `100%` }, statusWaiting: { backgroundColor: '#FFB15E', borderRadius: '50%', height: 8, width: 8, margin: '0 8px 0 0' }, statusDelivered: { backgroundColor: '#58d67d', borderRadius: '50%', height: 8, width: 8, margin: '0 8px 0 0' }, statusPill: { backgroundColor: '#FFB15E', borderRadius: 11, height: 17, width: 90, margin: '-5px 0 6px auto', display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'center' }, store: { fontWeight: 700, fontSize: 10, margin: '0 0 4px 0' }, ordered: { lineHeight: 1.43, fontWeight: 700, fontSize: 8, color: '#fff' }, storeSubtext: { textTransform: 'capitalize', fontSize: 10 }, locationIcon: { margin: 'auto', height: 10, width: 10, marginRight: 4 }, locationText: { fontSize: 10 }, gridBottom: { overflowY: 'scroll', height: '100%', marginBottom: 8, '&::-webkit-scrollbar': { display: 'none !important' } }, searchContainer: { display: 'flex', flexDirection: 'column', width: 'calc(100% - 32px)', height: '100%' }, shipmentItem: { margin: '4px 16px', width: '100%', height: 45, display: 'flex' }, flexBottom: { display: 'flex', flexWrap: 'wrap' }, firstCol: { display: 'inline-flex', flex: 1 }, flexFirst: { margin: 'auto 0' }, adjustML: { marginLeft: '-15px !important' }, flexEnd: { margin: 'auto auto 5px auto', display: 'flex', flexWrap: 'wrap' }, bold: { fontWeight: 700, whiteSpace: 'pre' }, colContainer: { justifyContent: 'center', textAlign: 'center' }, header: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px', marginLeft: '8px', fontWeight: 500, fontSize: '16px' }, welcome: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px' } }; }; ================================================ FILE: app/components/Analytics/styles/stats.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { gridItem: { height: '100%' }, root: { backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, height: '100%', marginRight: 10, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', flexDirection: 'column', borderRadius: 8, transition: theme.transitions.create(['background-color'], { duration: 300 }) }, dropdownIcon: { color: theme.palette.primary.color, fontSize: 16, marginTop: 4 }, menuList: { fontSize: 10, padding: 0, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, row: { flexDirection: 'row' }, grid: { width: `100%` }, menuItem: { padding: '4px 8px', fontSize: 10, textAlign: 'center' }, bold: { fontWeight: 700, whiteSpace: 'pre' }, colContainer: { width: '100%', justifyContent: 'center', textAlign: 'center' }, firstCol: { display: 'inline-flex', flex: 1 }, secondCol: { display: 'inline-flex', justifyContent: 'center', textAlign: 'center', flex: 1 }, thirdCol: { display: 'inline-flex', justifyContent: 'center', alignSelf: 'flex-start', textAlign: 'right', flex: 1 }, header: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginTop: '8px', marginLeft: '8px', fontWeight: 500, fontSize: '16px' }, subtext: { display: 'flex', justifyContent: 'flex-start', alignSelf: 'flex-start', marginLeft: '8px', fontWeight: 400, fontSize: '12px' }, totalText: { display: 'flex', margin: '0 auto', fontWeight: 700, fontSize: '12px', textAlign: 'center' }, selectPeriod: { minWidth: 83, fontSize: 12, padding: 0, color: theme.palette.primary.color }, subselect: { display: 'flex', margin: '5px 8px 0 auto', fontWeight: 700, fontSize: '12px', textAlign: 'right' } }; }; ================================================ FILE: app/components/Analytics/styles/welcome.tsx ================================================ import { mixins } from '../../../styles/js'; export const styles = theme => { return { gridItem: { height: '100%' }, root: { background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', height: '100%', marginRight: 10, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', borderRadius: 8, transition: theme.transitions.create(['background-color'], { duration: 300 }) }, row: { width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }, bold: { fontWeight: 700, whiteSpace: 'pre' }, welcome: { color: '#fff', display: 'flex', justifyContent: 'center', alignSelf: 'center', margin: '0 0 16px 0', fontWeight: 300 }, paperRoot: { backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, confirmBtn: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' }, cancelBtn: { width: 105, height: 35, background: theme.palette.primary.secondary, color: theme.palette.primary.color, '&:hover': { opacity: 0.5, background: theme.palette.primary.secondary, color: theme.palette.primary.color } }, unbindBtn: { fontSize: '12px', justifyContent: 'center', alignSelf: 'center', fontWeight: 400, marginRight: '12px', padding: '6px 16px', transition: theme.transitions.create(['opacity'], { duration: 300 }), ...mixins().btnNegative }, joinDiscordBtn: { opacity: 0.5, fontSize: '12px', justifyContent: 'center', alignSelf: 'center', fontWeight: 400, padding: '6px 16px', transition: theme.transitions.create(['opacity'], { duration: 300 }), ...mixins().btnNegative } }; }; ================================================ FILE: app/components/Analytics/types.tsx ================================================ /* eslint-disable camelcase */ // Generated by https://quicktype.io export interface PackageAPIResponse { id: string; object: string; mode: string; tracking_code: string; status: string; created_at: string; updated_at: string; signed_by: null; weight: null; est_delivery_date: string; shipment_id: null; carrier: string; public_url: string; tracking_details: TrackingDetail[]; carrier_detail: null; fees: Fee[]; } export interface Fee { object: string; type: string; amount: string; charged: boolean; refunded: boolean; } export interface TrackingDetail { object: string; message: string; status: string; datetime: string; source: string; tracking_location: TrackingLocation; } export interface TrackingLocation { object: string; city: string; state: string; country: null; zip: string; } ================================================ FILE: app/components/App/App.tsx ================================================ import React, { useEffect, useCallback } from 'react'; import { ipcRenderer } from 'electron'; import { CssBaseline } from '@material-ui/core'; import { ConfirmProvider } from 'material-ui-confirm'; import { makeStyles } from '@material-ui/styles'; import { log } from '../../utils/log'; import { styles } from './styles'; import ToolbarAreaPane from './components/toolbar/area'; import Titlebar from './components/titlebar/Titlebar'; import Sidebar from '../Sidebar/Sidebar'; import ErrorBoundary from '../ErrorBoundary'; import ClientRouter from '../../routing/ClientRouter'; import { bootLoader } from '../../utils/bootHelper'; import { IPCKeys } from '../../constants/ipc'; import { useProxiesStatus } from '../../hooks/useProxiesStatus'; import { useTaskStatus } from '../../hooks/useTaskStatus'; import { useAnalyticsFile } from '../../hooks/useAnalyticsFile'; import { useQuickTaskLifecycle } from '../../hooks/useQuickTaskLifecycle'; import { useTaskLifecycle } from '../../hooks/useTaskLifecycle'; import { useUpdateProxies } from '../../hooks/useUpdateProxies'; import { useUpdateStagger } from '../../hooks/useUpdateStagger'; import { useUpdateWebhooks } from '../../hooks/useUpdateWebhooks'; import { useUpdateProfiles } from '../../hooks/useUpdateProfiles'; import { useAutoSolveLifecycle } from '../../hooks/useAutoSolveLifecycle'; import { useAutoUpdaterLifecycle } from '../../hooks/useAutoUpdateLifecycle'; import { useNotificationLifecycle } from '../../hooks/useNotificationLifecycle'; const useStyles = makeStyles(styles); const App = () => { const styles = useStyles(); useProxiesStatus(); useTaskStatus(); useAnalyticsFile(); useQuickTaskLifecycle(); useTaskLifecycle(); useAutoSolveLifecycle(); useAutoUpdaterLifecycle(); useNotificationLifecycle(); useUpdateProxies(); useUpdateWebhooks(); useUpdateProfiles(); useUpdateStagger(); const setup = useCallback(() => { try { window.addEventListener('beforeunload', _cleanup); ipcRenderer.setMaxListeners(Infinity); bootLoader.cleanRotationFiles(); } catch (e) { log.error(e, `App -> setup`); } }, []); const _cleanup = useCallback(() => { [...Object.values(IPCKeys)].map(key => ipcRenderer.removeAllListeners(key)); window.removeEventListener('beforeunload', _cleanup); }, []); useEffect(() => { setup(); return () => { _cleanup(); }; }, []); return (
); }; export default App; ================================================ FILE: app/components/App/Providers.tsx ================================================ /* eslint-disable react/no-children-prop */ import React, { useMemo } from 'react'; import { Provider, useSelector } from 'react-redux'; import { StylesProvider, ThemeProvider as MuiThemeProvider } from '@material-ui/styles'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; import NotificationsIcon from '@material-ui/icons/Notifications'; import ErrorIcon from '@material-ui/icons/Error'; import HelpIcon from '@material-ui/icons/Help'; import CloseIcon from '@material-ui/icons/Close'; import { IconButton } from '@material-ui/core'; import { MuiPickersUtilsProvider } from '@material-ui/pickers'; import { ThemeProvider } from 'styled-components'; import { SnackbarProvider } from 'notistack'; import { createTheme } from '@material-ui/core/styles'; import { Store } from 'redux'; import MomentUtils from '@date-io/moment'; import { getNebulaFeatureFlags } from '../featureFlag/NebulaFeatureFlags'; import { FeatureFlagContext } from '../featureFlag/FeatureFlagContext'; import { light, dark } from './styles'; import { RootState } from '../../store/reducers'; const lightTheme = createTheme(light); const darkTheme = createTheme(dark); const getTheme = (type: number) => { switch (type) { case 0: return lightTheme; case 1: return darkTheme; default: return lightTheme; } }; // this has access to redux slices const ThemeInjector = ({ children }: { children: React.ReactNode }) => { const theme = useSelector((state: RootState) => state.Theme); const chosen = useMemo(() => getTheme(theme), [theme]); const notistackRef = React.createRef(); const onClickDismiss = (key: string | number) => () => { notistackRef.current.closeSnackbar(key); }; return ( , error: , warning: , info: }} disableWindowBlurListener preventDuplicate autoHideDuration={2500} maxSnack={3} ref={notistackRef} action={key => ( )} > {children} ); }; type Props = { store: Store; children: React.ReactNode; }; const Providers = ({ store, children }: Props) => { const features = useMemo(() => { return getNebulaFeatureFlags(); }, []); return ( ); }; export default Providers; ================================================ FILE: app/components/App/Root.tsx ================================================ import React from 'react'; import { PersistGate } from 'redux-persist/integration/react'; import { HashRouter } from 'react-router-dom'; import Providers from './Providers'; import App from './App'; type Props = { store: any; persistor: any; }; const Root = ({ store, persistor }: Props) => { return ( ); }; export default Root; ================================================ FILE: app/components/App/__tests__/App.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import App from '../App'; import { withProviders } from '../../../../test/testUtils'; const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('render a checkbox and modify its values', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); const Root = withProviders({ Component: App }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Dashboard')).toBeDefined(); }); ================================================ FILE: app/components/App/__tests__/Root.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import Root from '../Root'; import { configureStore, history } from '../../../store/configureStore'; const { store, persistor } = configureStore(); const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('render a checkbox and modify its values', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Dashboard')).toBeDefined(); }); ================================================ FILE: app/components/App/actions.tsx ================================================ import prefixer from '../../utils/reducerPrefixer'; const appPrefix = '@@App'; const appActionTypesList = [ 'REQ_LOAD', 'RES_LOAD', 'FAIL_LOAD', 'LOGOUT', 'SET_THEME', 'IMPORT_STATE' ]; export const globalActions = appActionTypesList.map(a => `${appPrefix}/${a}`); export const globalActionTypes = prefixer(appPrefix, appActionTypesList); export function setTheme(theme: number) { return { type: globalActionTypes.SET_THEME, payload: theme }; } export function importAll(state: any) { return { type: globalActionTypes.IMPORT_STATE, payload: state }; } export function reqLoadApp() { return { type: globalActionTypes.REQ_LOAD }; } export function resLoadApp() { return { type: globalActionTypes.RES_LOAD }; } export function failLoadApp(e: any) { return { type: globalActionTypes.FAIL_LOAD, payload: { error: e } }; } const userPrefix = '@@User'; const userActionTypesList = ['SET_USER']; export const userActions = userActionTypesList.map(a => `${userPrefix}/${a}`); export const userActionTypes = prefixer(userPrefix, userActionTypesList); export function setUser(user: any) { return (dispatch: any) => { dispatch({ type: userActionTypes.SET_USER, payload: user }); }; } const storesPrefix = '@@Stores'; const storesActionTypesList = ['ADD_STORE', 'SET_STORES']; export const storesActions = storesActionTypesList.map( a => `${storesPrefix}/${a}` ); export const storesActionTypes = prefixer(storesPrefix, storesActionTypesList); export function addStore(newStore: any) { return { type: storesActionTypes.ADD_STORE, payload: newStore }; } export function setStores(stores: any[]) { return { type: storesActionTypes.SET_STORES, payload: stores }; } ================================================ FILE: app/components/App/components/titlebar/Titlebar.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { useLocation } from 'react-router-dom'; import { styles } from '../../styles/Titlebar'; const useStyles = makeStyles(styles); const Titlebar = () => { const styles = useStyles(); const { pathname } = useLocation(); return /progressbar/i.test(pathname) ? null :
; }; export default Titlebar; ================================================ FILE: app/components/App/components/toolbar/area.tsx ================================================ import React from 'react'; import ToolbarBody from './body'; const ToolbarAreaPane = ({ ...parentProps }: any) => { return ; }; export default ToolbarAreaPane; ================================================ FILE: app/components/App/components/toolbar/body.tsx ================================================ import React, { useState, useCallback } from 'react'; import { makeStyles } from '@material-ui/styles'; import { useLocation } from 'react-router-dom'; import { ClickAwayListener, AppBar, Toolbar, List } from '@material-ui/core'; import { styles } from '../../styles/ToolbarAreaPane'; import SettingsDialog from '../../../Settings'; import StateDialog from '../../../ImportExport'; import Profile from './profile'; import MenuItems from './menu'; const useStyles = makeStyles(styles); const ToolbarAreaPane = () => { const styles = useStyles(); const [anchorEl, setAnchorEl] = useState(null); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isStateOpen, setIsStateOpen] = useState(false); const { pathname } = useLocation(); const handleClick = useCallback( (event: any) => { if (anchorEl) { setAnchorEl(null); return; } setAnchorEl(event.currentTarget); }, [anchorEl] ); const handleClose = useCallback(() => { setAnchorEl(null); }, []); const _handleToggleSettings = useCallback(() => { setIsSettingsOpen(!isSettingsOpen); handleClose(); }, [isSettingsOpen]); const _handleToggleState = useCallback(() => { setIsStateOpen(!isStateOpen); handleClose(); }, [isStateOpen]); // don't render the toolbar on the progress window or task group windows... if (/progress|taskGroup/i.test(pathname)) { return null; } return (
); }; export default ToolbarAreaPane; ================================================ FILE: app/components/App/components/toolbar/menu.tsx ================================================ import React, { MouseEvent } from 'react'; import { Fade, Menu, MenuItem } from '@material-ui/core'; import { makeStyles } from '@material-ui/styles'; import { close, minimize, quit } from '../../../../utils/createWindows'; import { styles } from '../../styles/ToolbarAreaPane'; const useStyles = makeStyles(styles); const _renderMenuItem = ({ className, onClick, label }: { className: string; onClick: (event: MouseEvent) => void; label: string; }) => ( {label} ); const extractMenuItems = ( pathname: string, toggleState: any, toggleSettings: any, styles: any ) => { const action = /privacy|terms|bugs/i.test(pathname) ? close : quit; if (/privacy|terms|bugs/i.test(pathname)) { return ( Close ); } return [ { className: styles.condensedMenuItem, onClick: toggleState, label: 'State' }, { className: styles.condensedMenuItem, onClick: toggleSettings, label: 'Settings' }, { className: styles.condensedMenuItem, onClick: minimize, label: 'Minimize' }, { className: styles.condensedMenuItem, onClick: action, label: 'Close' } ].map(_renderMenuItem); }; const ToolbarAreaPane = ({ anchorEl, handleClose, handleToggleState, handleToggleSettings, pathname }: { anchorEl: any; handleClose: any; handleToggleState: any; handleToggleSettings: any; pathname: string; }) => { const styles = useStyles(); const menuItems = extractMenuItems( pathname, handleToggleState, handleToggleSettings, styles ); return ( {menuItems} ); }; export default ToolbarAreaPane; ================================================ FILE: app/components/App/components/toolbar/profile.tsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { Typography, Badge, ListItem, ListItemText, Avatar, Tooltip } from '@material-ui/core'; import { makeUser } from '../../selectors'; import { styles } from '../../styles/ToolbarAreaPane'; const useStyles = makeStyles(styles); const ToolbarAreaPane = ({ handleClick }: { handleClick: any }) => { const styles = useStyles(); const user = useSelector(makeUser); let badgeStyle = styles.badge; let displayName = ''; let subtext = ''; let avatarUrl = ''; let badgeTitle = ''; if (user) { const { id, avatar, name, username, type } = user; badgeTitle = type; displayName = name; subtext = username; if (avatar) { if (avatar.startsWith('a_')) { avatarUrl = `https://cdn.discordapp.com/avatars/${id}/${avatar}.gif`; } else { avatarUrl = `https://cdn.discordapp.com/avatars/${id}/${avatar}.png`; } } } if (badgeTitle === 'Staff') { badgeStyle = styles.badgeStaff; } if (badgeTitle === 'Renewal') { badgeStyle = styles.badgeMember; } if (badgeTitle === 'Lifetime' || badgeTitle === 'F&F') { badgeStyle = styles.badgeLifetime; } return ( {displayName} } secondary={ {subtext} } primaryTypographyProps={{ style: { lineHeight: 1 } }} secondaryTypographyProps={{ style: { lineHeight: 1 } }} /> ); }; export default ToolbarAreaPane; ================================================ FILE: app/components/App/reducers.tsx ================================================ import { startCase } from 'lodash'; import { PURGE } from 'redux-persist'; import { globalActionTypes, userActionTypes, storesActionTypes } from './actions'; type Action = { type: string; payload?: any; }; export type User = { id: string; username: string; avatar: string; name: string; email: string; status: string; type: string; createdAt: string; updatedAt: string; }; export const initialState: User = { id: '', username: '', avatar: '', name: '', email: '', status: '', type: '', createdAt: '', updatedAt: '' }; export type Theme = number; export const initialThemeState: Theme = 0; export function Theme(state = initialThemeState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return initialThemeState; case globalActionTypes.SET_THEME: return payload; default: return state; } } export function User(state = initialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return { ...initialState }; case userActionTypes.SET_USER: return { ...payload }; default: return state; } } type Store = { label: string; supported: boolean; value: string; }; type Platform = { options: Store[]; _id: string; index: number; label: string; }; export type Stores = Platform[]; export const initialStoresState: Stores | [] = []; export function Stores(state = initialStoresState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...initialStoresState]; case storesActionTypes.ADD_STORE: { if (!payload) { return state; } const { name, url } = payload; if (!name || !url) { return state; } const index = state.findIndex(({ label }) => label === 'Shopify'); if (index === -1) { return state; } return [...state].map((platform, idx) => { if (idx !== index) { return platform; } if (platform.options.some(({ value }) => value === url)) { return platform; } const newOptions = [ ...platform.options, { label: startCase(name), supported: true, value: url } ].sort((a, b) => { if (b.label < a.label) { return 1; } if (b.label > a.label) { return -1; } return 0; }); return { ...platform, options: [...newOptions] }; }); } case storesActionTypes.SET_STORES: { if (payload) { return [...payload]; } return state; } default: return state; } } ================================================ FILE: app/components/App/selectors.tsx ================================================ import { createSelector } from 'reselect'; import { RootState } from '../../store/reducers'; import { initialState, initialStoresState } from './reducers'; const selectUser = (state: RootState) => state.User; const selectStores = (state: RootState) => state.Stores; export const makeUser = createSelector( selectUser, state => state || initialState ); export const makeStores = createSelector( selectStores, state => state || initialStoresState ); ================================================ FILE: app/components/App/styles/Titlebar.tsx ================================================ import { mixins } from '../../../styles/js'; export const styles = () => { return { root: { width: `calc(100% - 110px)`, height: 32, position: `fixed`, zIndex: 999, ...mixins().appDragEnable } }; }; ================================================ FILE: app/components/App/styles/ToolbarAreaPane.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { backgroundColor: `transparent !important`, color: theme.palette.primary.color, position: 'absolute', width: 'auto', marginRight: 20, top: 0, right: 0 }, dropdownIcon: { color: theme.palette.primary.color, fontSize: 16, marginTop: 4 }, menuList: { fontSize: 10, padding: 0, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, badge: { top: 0, right: 0, height: 8, width: 8, color: 'white', backgroundColor: '#D8D8D8' }, badgeStaff: { top: 0, right: 0, height: 8, width: 8, color: 'white', backgroundColor: '#ff86ab' }, badgeMember: { top: 0, right: 0, height: 8, width: 8, color: 'white', backgroundColor: '#a097fd' }, badgeLifetime: { top: 0, right: 0, height: 8, width: 8, color: 'white', backgroundColor: '#29cf8a' }, inline: { display: 'flex', color: theme.palette.primary.color, flexDirection: 'column', textAlign: 'right', lineHeight: 1.25 }, bold: { fontWeight: 500 }, grow: { flexGrow: 1 }, subtext: { fontSize: 10, color: theme.palette.primary.subtext, fontWeight: 300 }, toolbarInnerWrapper: { display: 'flex', margin: '0 0 0 auto', padding: 0 }, toolbar: { width: `auto`, height: variables().sizes.toolbarHeight }, appBar: { backgroundColor: `transparent !important`, margin: '16px 0 0 0' }, navBtns: { margin: 15 }, noAppDrag: { ...mixins().appDragDisable, ...mixins().noDrag }, navBtnImgs: { ...mixins().noDrag, ...mixins().noselect, height: 25, width: `auto`, '&:hover': { backgroundColor: 'transparent !important' } }, disabledNavBtns: {}, toolbarMenu: { padding: 0 }, condenseRight: { paddingRight: 6.5, height: '100%', margin: 'auto 0' }, avatar: { cursor: 'pointer' }, condensedMenuItem: { display: 'flex !important', justifyContent: 'center', padding: '8px 4px !important', fontSize: 12, fontWeight: 400, lineHeight: `0.5 !important` } }; }; ================================================ FILE: app/components/App/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const dark = { props: { MuiButtonBase: { disableRipple: true } }, palette: { primary: { ...variables().styles.darkPalette }, secondary: { ...variables().styles.secondaryColor } }, typography: { useNextVariants: true, fontSize: variables().styles.regularFontSize, fontFamily: [ 'Roboto', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', '"Helvetica Neue"', 'Arial', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"' ].join(',') }, overrides: { MuiPaper: { root: { backgroundColor: variables().styles.darkPalette.background } }, MuiTypography: { root: { color: variables().styles.darkPalette.color } }, MuiInputBase: { root: { color: variables().styles.darkPalette.color }, inputMultiline: { height: '100%' } }, MuiMobileStepper: { root: { background: variables().styles.darkPalette.background } }, MuiCheckbox: { root: { color: variables().styles.darkPalette.checkbox } }, MuiSwitch: { track: { backgroundColor: variables().styles.darkPalette.subtext } }, MuiDialog: { paper: { color: variables().styles.darkPalette.color, backgroundColor: variables().styles.darkPalette.background } }, MuiDialogTitle: { root: { color: variables().styles.darkPalette.color } }, MuiDialogContentText: { root: { color: variables().styles.darkPalette.subtext } } } }; export const light = { props: { MuiButtonBase: { disableRipple: true } }, palette: { primary: { ...variables().styles.lightPalette }, secondary: { ...variables().styles.secondaryColor } }, typography: { useNextVariants: true, fontSize: variables().styles.regularFontSize, fontFamily: [ 'Roboto', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', '"Helvetica Neue"', 'Arial', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"' ].join(',') }, overrides: { MuiPaper: { root: { backgroundColor: variables().styles.lightPalette.background } }, MuiTypography: { root: { color: variables().styles.lightPalette.color } }, MuiInputBase: { root: { color: variables().styles.lightPalette.color }, inputMultiline: { height: '100%' } }, MuiMobileStepper: { root: { background: variables().styles.lightPalette.background } }, MuiCheckbox: { root: { color: variables().styles.lightPalette.checkbox } }, MuiSwitch: { track: { backgroundColor: variables().styles.lightPalette.subtext } }, MuiDialog: { paper: { color: variables().styles.lightPalette.color, backgroundColor: variables().styles.lightPalette.background } }, MuiDialogTitle: { root: { color: variables().styles.lightPalette.color } }, MuiDialogContentText: { root: { color: variables().styles.lightPalette.subtext } } } }; export const styles = theme => { return { root: { display: `flex`, flexDirection: 'column', height: `100%`, background: theme.palette.primary.background }, noAppDrag: { ...mixins().appDragDisable }, navBtns: { paddingLeft: 5 }, navBtnImgs: { height: 25, width: `auto`, ...mixins().noDrag, ...mixins().noselect }, row: { display: `flex`, flexDirection: 'row', height: `100%` }, col: { display: `flex`, flexDirection: 'column', small: { flexGrow: 1 }, extend: { flexGrow: 5 } }, noProfileError: { textAlign: `center`, ...mixins().center, ...mixins().absoluteCenter }, props: { MuiTypography: { display: 'block' } } }; }; ================================================ FILE: app/components/Calendar/Calendar.tsx ================================================ import React, { useState } from 'react'; import { Text, Flex } from 'rebass'; import { Container } from './Container'; import ViewSelect, { CALENDAR_VIEW } from './ViewSelect'; import CalendarYear from './CalendarYear'; import CalendarMonth from './CalendarMonth'; const getCalendarView = view => { const mapping = { [CALENDAR_VIEW.YEAR]: CalendarYear, [CALENDAR_VIEW.MONTH]: CalendarMonth }; return view in mapping ? mapping[view] : CalendarYear; }; const Calendar = () => { const [view, setView] = useState({ label: 'month', value: CALENDAR_VIEW.MONTH }); const CalendarComponent = getCalendarView(view.value); return ( Calendar setView(value)} maxWidth="200px" /> ); }; export default Calendar; ================================================ FILE: app/components/Calendar/CalendarMonth.tsx ================================================ import React from 'react'; import styled from 'styled-components'; import moment from 'moment'; import LeftIcon from '@material-ui/icons/ChevronLeft'; import RightIcon from '@material-ui/icons/ChevronRight'; import { Card } from '@material-ui/core'; import { space } from 'styled-system'; import { Text, Flex } from 'rebass'; import { getWeekday } from './Month'; const WeekdayGrid = styled.div` font-weight: 500; display: grid; grid-template-columns: repeat(7, 1fr); grid-gap: 10px 5px; ${space} `; const DayGrid = styled.div` display: grid; grid-template-columns: repeat(7, 1fr); grid-gap: 10px 5px; height: 100%; & :first-child { grid-column: ${props => props.firstWeekday}; } ${space} `; const DayCard = styled(Card)` border-radius: 10px; background-color: #fff; `; const MonthText = styled(Text)` font-size: 24px; font-weight: 700; text-align: center; color: #000; `; const YearText = styled(Text)` font-size: 12px; font-weight: 400; text-align: center; color: #616161; `; const today = moment(); const CalendarMonth = () => { const month = moment().format('MMMM'); const year = moment().format('YYYY'); const weekdays = moment.weekdaysMin(); const firstWeekday = getWeekday(today); const daysInMonth = today.daysInMonth(); const days = Array.from(Array(daysInMonth).keys()).map(i => i + 1); return ( <> {month} {year} {weekdays.map(wk => ( {wk.slice(0, 1)} ))} {days.map(d => ( {d} ))} ); }; export default CalendarMonth; ================================================ FILE: app/components/Calendar/CalendarYear.tsx ================================================ import React from 'react'; import styled from 'styled-components'; import moment from 'moment'; import Month from './Month'; const AnnualGrid = styled.div` display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); grid-gap: 25px; `; const CalendarYear = () => { const months = moment.months(); return ( {months.map((month, i) => ( ))} ); }; export default CalendarYear; ================================================ FILE: app/components/Calendar/Container.tsx ================================================ import React from 'react'; import styled from 'styled-components'; import { layout } from 'styled-system'; export const Root = styled.div` width: 100%; margin: 35px; margin-top: 55px; display: flex; flex-direction: column; `; export const Grid = styled.div` height: 100%; width: 100%; `; export const TableContainer = styled.div` height: 100%; &::-webkit-scrollbar: { display: none; } ${layout} overflow-y: hidden; `; type Props = { children: React.ReactNode; }; export const Container = ({ children, ...rest }: Props) => { return ( {children} ); }; ================================================ FILE: app/components/Calendar/Month.tsx ================================================ import React from 'react'; import { Flex, Text } from 'rebass'; import { space } from 'styled-system'; import moment, { Moment } from 'moment'; import { Card as _Card } from '@material-ui/core'; import styled from 'styled-components'; const Card = styled(_Card)` border-radius: 10px; background-color: #fff; line-height: 1.16; `; const DayGrid = styled.div` display: grid; grid-template-columns: repeat(7, 1fr); grid-gap: 10px 5px; & :first-child { grid-column: ${props => props.firstWeekday}; } ${space} `; export const getWeekday = (date: Moment) => { const weekday = date.startOf('month').day() % 6; if (weekday === 0) { return 7; } return weekday; }; type Props = { month: string; }; const Month = ({ month }: Props) => { const year = moment().get('year'); const months = moment.months(); const monthIndex = months.findIndex(m => m === month); const weekdays = moment.weekdaysMin(); const date = moment({ year, month: monthIndex, day: 1 }); const firstWeekday = getWeekday(date); const daysInMonth = date.daysInMonth(); const days = Array.from(Array(daysInMonth).keys()).map(i => i + 1); return ( {month} {year} {weekdays.map(wk => ( {wk.slice(0, 1)} ))} {days.map(d => ( {d} ))} ); }; export default Month; ================================================ FILE: app/components/Calendar/ViewSelect.tsx ================================================ import React from 'react'; import WindowedSelect from 'react-windowed-select'; import styled from 'styled-components'; import { FormGroup as _FormGroup } from '@material-ui/core'; import { colorStyles, IndicatorSeparator } from '../../styles/select'; const FormGroup = styled(_FormGroup)` flex: 1; display: flex; `; export const CALENDAR_VIEW = { YEAR: 'YEAR', MONTH: 'MONTH' }; type Option = { label: string; value: string; }; type Props = { value: Option; onChange: (option: Option) => void; }; const ViewSelect = ({ value, onChange, maxWidth }: Props) => { const options = [ { label: 'year', value: CALENDAR_VIEW.YEAR }, { label: 'month', value: CALENDAR_VIEW.MONTH } ]; const style = { ...colorStyles(null), input: styles => ({ ...styles, width: 'auto', maxWidth, color: '#000' }), control: styles => ({ ...styles, width: 'auto', maxWidth, border: '1px solid #979797', height: 29, fontSize: 12, minHeight: 29, borderRadius: 5, outline: 'none', cursor: 'pointer', boxShadow: 'none', ':hover': { border: '1px solid #979797', cursor: 'pointer' } }) }; return ( ); }; export default ViewSelect; ================================================ FILE: app/components/Captchas/Harvesters.tsx ================================================ import React from 'react'; import { useDispatch } from 'react-redux'; import { makeStyles } from '@material-ui/styles'; import { Grid } from '@material-ui/core'; import ActionBar from './components/actionBar/ActionBar'; import { createCaptcha } from './actions'; import HarvesterGrid from './components/grid'; import { styles } from './styles'; const useStyles = makeStyles(styles); const CaptchasPrimitive = () => { const styles = useStyles(); const dispatch = useDispatch(); const handleCreate = () => dispatch(createCaptcha()); return ( ); }; export default CaptchasPrimitive; ================================================ FILE: app/components/Captchas/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../LoadingIndicator'; /* eslint import/no-cycle: [2, { maxDepth: 1 }] */ export default Loadable(() => import('./Harvesters'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Captchas/__tests__/Harvesters.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import Harvesters from '../Harvesters'; import { withProviders } from '../../../../test/testUtils'; it('should render Harvesters', async () => { const Root = withProviders({ Component: Harvesters }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('No Harvesters')).toBeDefined(); }); ================================================ FILE: app/components/Captchas/actions/captchas.tsx ================================================ import prefixer from '../../../utils/reducerPrefixer'; const prefix = '@@Captcha'; export const captchasActionList = ['DELETE', 'EDIT', 'CREATE', 'THEME']; export const captchasActions = captchasActionList.map(a => `${prefix}/${a}`); export const captchasActionTypes = prefixer(prefix, captchasActionList); export const deleteCaptcha = captcha => { return dispatch => { dispatch({ type: captchasActionTypes.DELETE, payload: captcha }); }; }; export const createCaptcha = () => { return dispatch => { dispatch({ type: captchasActionTypes.CREATE }); }; }; export const editCaptcha = ({ id, type, field, value }) => ({ type: captchasActionTypes.EDIT, payload: { id, type, field, value } }); export const changeTheme = (theme = 1) => { return dispatch => { dispatch({ type: captchasActionTypes.THEME, payload: theme }); }; }; ================================================ FILE: app/components/Captchas/actions/index.tsx ================================================ import { createCaptcha, editCaptcha, deleteCaptcha, captchasActionTypes, captchasActionList, captchasActions, changeTheme } from './captchas'; export const HARVESTER_TYPES = { EDIT: 'EDIT' }; export const HARVESTER_FIELDS = { NAME: 'name', STORE: 'store', PROXY: 'proxy', TOKEN: 'token', TYPE: 'type' }; export { createCaptcha, editCaptcha, deleteCaptcha, captchasActionList, captchasActionTypes, captchasActions, changeTheme }; ================================================ FILE: app/components/Captchas/components/actionBar/ActionBar.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { Grid } from '@material-ui/core'; import Create from '@material-ui/icons/Add'; import { styles } from '../../styles/actionBar'; type Props = { onCreate: () => void; }; const useStyles = makeStyles(styles); const ActionBarComponent = ({ onCreate }: Props) => { const styles = useStyles(); return ( ); }; export default ActionBarComponent; ================================================ FILE: app/components/Captchas/components/card/index.tsx ================================================ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useConfirm } from 'material-ui-confirm'; import { ipcRenderer } from 'electron'; import { Grid, IconButton, Fade, FormGroup, Input, Select as MuiSelect, Tooltip, MenuItem } from '@material-ui/core'; import { makeStyles } from '@material-ui/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import LaunchIcon from '@material-ui/icons/Launch'; import DeleteIcon from '@material-ui/icons/Delete'; import YouTubeIcon from '@material-ui/icons/YouTube'; import LoadingIcon from '@material-ui/icons/Cancel'; import { log } from '../../../../utils/log'; import { styles } from '../../styles/card'; import { editCaptcha, deleteCaptcha, HARVESTER_FIELDS, HARVESTER_TYPES } from '../../actions'; import { IPCKeys } from '../../../../constants/ipc'; import { HarvesterOptions, HarvesterTypes } from '../../../../constants'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); const buildMenuItems = () => { const styles = useStyles(); const options = [...Object.keys(HarvesterOptions)]; return options.map(option => ( {option} )); }; const CardComponent = ({ captcha }: { captcha: any }) => { const styles = useStyles(); const dispatch = useDispatch(); const confirm = useConfirm(); const theme = useSelector((state: RootState) => state.Theme); const [isLoadingYouTube, setLoadingYoutube] = useState(false); const [isLoadingHarvester, setLoadingHarvester] = useState(false); const editHandler = async (e: any, field: string) => { dispatch( editCaptcha({ id: captcha.id, type: HARVESTER_TYPES.EDIT, field, value: e.target.value }) ); }; const closeHandler = async (e: any) => { e.stopPropagation(); try { await confirm({ title: `Remove Harvester "${captcha.name}"?`, description: 'This action cannot be undone.', confirmationText: 'Yes', cancellationText: 'No', dialogProps: { classes: { paper: styles.paperRoot } }, confirmationButtonProps: { classes: { root: styles.confirmBtn }, style: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' } }, cancellationButtonProps: { classes: { root: styles.cancelBtn }, style: { width: 105, height: 35 } } }); ipcRenderer .invoke(IPCKeys.CloseHarvesterWindows, { ...captcha }) .then(() => dispatch(deleteCaptcha(captcha))) .catch(() => { // TODO: Error handling }); } catch (e) { log.error(e, 'Harvesters -> Remove Harvester Cancelled'); } }; const launchHarvesterHandler = () => { if (!isLoadingHarvester && !isLoadingYouTube) { setLoadingHarvester(true); ipcRenderer .invoke(IPCKeys.LaunchHarvester, { ...captcha, theme }) .then(() => setLoadingHarvester(false)) .catch(() => setLoadingHarvester(false)); } }; const cancelLaunchHarvester = () => { ipcRenderer .invoke(IPCKeys.CancelLaunchHarvester, { ...captcha }) .then(() => setLoadingHarvester(false)) .catch(() => setLoadingHarvester(false)); }; const launchYouTubeHandler = () => { if (!isLoadingYouTube && !isLoadingHarvester) { setLoadingYoutube(true); ipcRenderer .invoke(IPCKeys.LaunchYoutube, { ...captcha }) .then(() => setLoadingYoutube(false)) .catch(() => setLoadingYoutube(false)); } }; const cancelLaunchYouTube = () => { ipcRenderer .invoke(IPCKeys.CancelLaunchYouTube, { ...captcha }) .then(() => setLoadingYoutube(false)) .catch(() => setLoadingYoutube(false)); }; return (
editHandler(e, HARVESTER_FIELDS.NAME)} onBlur={() => ipcRenderer.send(IPCKeys.UpdateHarvester, { ...captcha, theme }) } disableUnderline value={captcha.name} className={styles.cardHolder} /> editHandler(e, HARVESTER_FIELDS.STORE)} defaultValue={HarvesterOptions.Shopify} SelectDisplayProps={{ style: { paddingTop: 7, fontWeight: 400 } }} inputProps={{ classes: { icon: styles.dropdownIcon } }} IconComponent={ExpandMoreIcon} MenuProps={{ MenuListProps: { classes: { root: styles.menuList } }, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, getContentAnchorEl: null }} > {buildMenuItems()} editHandler(e, HARVESTER_FIELDS.TYPE)} defaultValue={HarvesterTypes.Checkout} SelectDisplayProps={{ style: { paddingTop: 7, fontWeight: 400 } }} inputProps={{ classes: { icon: styles.dropdownIcon } }} IconComponent={ExpandMoreIcon} MenuProps={{ MenuListProps: { classes: { root: styles.menuList } }, anchorOrigin: { vertical: 'bottom', horizontal: 'left' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, getContentAnchorEl: null }} > {captcha.store === HarvesterOptions.Shopify ? ( [ HarvesterTypes.Login, HarvesterTypes.Checkout, HarvesterTypes.Checkpoint ].map(option => ( {option} )) ) : ( Checkout )} editHandler(e, HARVESTER_FIELDS.PROXY)} onBlur={() => ipcRenderer.send(IPCKeys.UpdateHarvester, { ...captcha, theme }) } /> {isLoadingYouTube ? ( ) : ( )} {isLoadingHarvester ? ( ) : ( )}
); }; export default CardComponent; ================================================ FILE: app/components/Captchas/components/grid/index.tsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import NoChildrenComponent from '../../../NoChildrenComponent'; import { makeHarvesters } from '../../selectors'; import CardComponent from '../card'; const HarvestersGrid = () => { const captchas = useSelector(makeHarvesters); if (!captchas.length) { return ; } return captchas.map((captcha: any) => ( )); }; export default HarvestersGrid; ================================================ FILE: app/components/Captchas/reducers/captchas.tsx ================================================ import { ipcRenderer } from 'electron'; import uuidv4 from 'uuidv4'; import { PURGE } from 'redux-persist'; import { captchasActionTypes, HARVESTER_FIELDS } from '../actions'; import { HarvesterOptions, HarvesterTypes, Platforms, platformForStore } from '../../../constants'; import { IPCKeys } from '../../../constants/ipc'; export const initialState: any[] = []; type Action = { type: string; payload?: any; }; export type Captchas = []; export function Captchas(state = initialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...initialState]; case captchasActionTypes.CREATE: { let id: string; const checker = (c: any) => c.id === id; do { id = uuidv4(); } while (state.some(checker)); const count = state.length + 1; const harvester = { id, name: `Harvester ${count}`, store: HarvesterOptions.Shopify, platform: Platforms.Shopify, type: HarvesterTypes.Checkout, proxy: '' }; return [...state, harvester]; } case captchasActionTypes.EDIT: { if (!payload || (payload && !payload.id)) { return state; } const { id, field, value } = payload; return state.map(captcha => { if (captcha.id === id) { if (field === HARVESTER_FIELDS.STORE) { const platform = platformForStore(value); if (platform !== Platforms.Shopify) { return { ...captcha, platform, type: HarvesterTypes.Checkout, [field]: value }; } return { ...captcha, platform, [field]: value }; } return { ...captcha, [field]: value }; } return captcha; }); } case captchasActionTypes.THEME: { return state.map(harvester => { ipcRenderer.send(IPCKeys.UpdateTheme, { theme: payload }); return harvester; }); } case captchasActionTypes.DELETE: if (payload) { return state.filter(p => p.id !== payload.id); } return state; default: return state; } } ================================================ FILE: app/components/Captchas/reducers/index.tsx ================================================ import { Captchas } from './captchas'; export { Captchas }; ================================================ FILE: app/components/Captchas/selectors.tsx ================================================ import { createSelector } from 'reselect'; import { Captchas } from './reducers'; import { RootState } from '../../store/reducers'; const selectCaptchas = (state: RootState) => state.Captchas; export const makeHarvesters = createSelector( selectCaptchas, state => state || Captchas ); ================================================ FILE: app/components/Captchas/styles/actionBar.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { justifyContent: 'center', zIndex: 999 }, background: { height: 40, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', boxShadow: '0px 4px 8px 0px rgba(0,0,0,0.25)', borderRadius: '50%', flexDirection: 'row', display: 'flex', position: 'absolute', bottom: 24, width: 40 }, alignCenter: { alignSelf: 'center', height: '100%' }, center: { display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: '100%' }, table: { height: `calc(100% - 125px)` }, actionIcon: { cursor: 'pointer', color: '#fff', height: 24, width: 24, '&:hover': { opacity: 0.5 } }, paper: { height: '100%', display: 'flex', flexDirection: 'column' }, toolbar: { paddingLeft: '64px', paddingRight: '32px' }, title: { flex: '0 0 auto' }, spacer: { flex: '1 1 100%' } }; }; ================================================ FILE: app/components/Captchas/styles/card.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { margin: 10, backgroundColor: theme.palette.primary.card, height: 250, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', borderRadius: 12, transition: theme.transitions.create(['background-color'], { duration: 300 }), '&:hover': { transition: theme.transitions.create(['background-color'], { duration: 300 }), backgroundColor: 'rgba(131,119,244,1)' } }, paperRoot: { backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, input: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, fontWeight: 400, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color, border: `1px solid ${theme.palette.primary.border}`, width: '100%' }, inputShortOne: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, marginRight: 8, fontWeight: 400, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color, border: `1px solid ${theme.palette.primary.border}`, width: '45%' }, inputShortTwo: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, marginLeft: 8, fontWeight: 400, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color, border: `1px solid ${theme.palette.primary.border}`, width: '45%' }, dropdownIcon: { color: theme.palette.primary.color, fontSize: 16, marginTop: 4, marginRight: 4 }, confirmBtn: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' }, menuList: { fontSize: 10, padding: 0, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, menuItem: { padding: '4px 8px', fontSize: 10, textAlign: 'center' }, cancelBtn: { width: 105, height: 35, background: theme.palette.primary.secondary, color: theme.palette.primary.color, '&:hover': { opacity: 0.5, background: theme.palette.primary.secondary, color: theme.palette.primary.color } }, flexFillOne: { flex: 1, marginRight: 8 }, flexFillTwo: { flex: 1, marginLeft: 8 }, proxyInput: { background: theme.palette.primary.background, padding: '0 8px', borderRadius: 4, color: theme.palette.primary.color }, background: { height: '100%', width: '100%' }, gridContainer: { margin: 24, width: 'calc(100% - 48px)' }, gridContainerMid: { margin: '24px 24px 0 24px', width: 'calc(100% - 48px)' }, gridContainerEnd: { margin: '0 24px 24px 24px', width: 'calc(100% - 48px)', justifyContent: 'space-around' }, cardHolder: { display: 'flex', flex: 1, color: '#fff', fontSize: '1.5em', fontWeight: 500, '&:hover': { opacity: 0.5, cursor: 'pointer' } }, cardName: { display: 'flex', flex: 1, color: '#fff', fontSize: 14, fontWeight: 400 }, textHolder: { margin: '0 auto 0 0', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }, imgHolder: { width: '20%', margin: 'auto 0' }, dots: { display: 'flex', color: '#fff', fontSize: 16, fontWeight: 400, marginRight: 8 }, cardNumber: { display: 'flex', flex: 1, color: '#fff', fontSize: 16, fontWeight: 400 }, actionIconWrapper: { display: 'flex', padding: 0, alignSelf: 'flex-start', '&:hover': { background: 'transparent', backgroundColor: 'transparent' } }, youtubeIconWrapper: { display: 'flex', padding: 0, alignSelf: 'flex-start', '&:hover': { background: 'transparent', backgroundColor: 'transparent' } }, cardTypeImg: { height: 50, width: 50, objectFit: 'scale-down' }, actionIcon: { color: '#fff', '&:hover': { opacity: 0.5 } }, youtubeIcon: { color: '#fff', '&:hover': { opacity: 0.5 }, width: '1.5em', height: '1.5em' } }; }; ================================================ FILE: app/components/Captchas/styles/createDialog.tsx ================================================ import { variables } from '../../../styles/js'; export const styles = theme => ({ margin: {}, rootSm: { width: 350, height: 415, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, fieldset: { width: `45%`, margin: '0 12px', border: 0, display: 'inline-flex', padding: 0, position: 'relative', minWidth: 0, flexDirection: 'column', verticalAlign: 'top' }, fieldsetFull: { width: '100%' }, dialogContent: { margin: '16px 48px', padding: 0 }, subtitle: { fontSize: 12, fontWeight: 500 }, fmSettingsStylesFix: { marginTop: 10 }, formGroupOne: { margin: '0 4px 16px 0', flexWrap: 'nowrap', display: 'inline-flex' }, formGroupTwo: { margin: '0 0 16px 4px', flexWrap: 'nowrap', display: 'inline-flex' }, formGroup: { margin: '0 0 16px 0' }, formGroupCenter: { margin: '8px 0 16px 0' }, subheading: { marginBottom: 5 }, title: { color: theme.palette.primary.heading, display: 'flex', justifyContent: 'center', margin: '24px 24px 16px 24px' }, inputWrapper: { margin: 0 }, input: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, fontWeight: 400, backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, border: `1px solid ${theme.palette.primary.border}`, width: '100%' }, flexOne: { flex: 2 }, flexNone: { flex: 1 }, flex: { display: 'flex' }, block: {}, onBoardingPaper: { position: `relative`, padding: 10, marginTop: 4, backgroundColor: variables().styles.secondaryColor.background }, onBoardingPaperArrow: { fontWeight: `bold`, content: ' ', borderBottom: `11px solid ${variables().styles.secondaryColor.background}`, borderLeft: '8px solid transparent', borderRight: '8px solid transparent', position: 'absolute', top: -10, left: 2 }, onBoardingPaperBody: { color: variables().styles.primaryColor.background }, a: { fontWeight: `bold` }, menuItem: { padding: '4px 8px', fontSize: 10, textAlign: 'center' }, stepper: { maxWidth: 400, flexGrow: 1 }, bar: { backgroundColor: '#fff' }, progressBar: { backgroundColor: 'rgba(164,155,255, 0.333)', color: '#d8d8d8' }, stepperRoot: { position: 'absolute', bottom: 0, width: '100%' }, dropdownIcon: { color: theme.palette.primary.color, fontSize: 16, marginTop: 4 }, menuList: { fontSize: 10, padding: 0, backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, btnStart: { width: 105, height: 35, color: '#fff', borderRadius: 4, transition: theme.transitions.create(['opacity'], { duration: 300 }), background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', '&:hover': { background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) }, '&:active': { background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) } }, btnEnd: { width: 105, height: 35, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, borderRadius: 4, border: `1px solid ${theme.palette.primary.border}`, transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) }, '&:active': { opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) } } }); ================================================ FILE: app/components/Captchas/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { margin: 25, marginTop: 75, display: 'flex', flexDirection: 'row', alignContent: 'flex-start', width: '100%', height: 'calc(100% - 100px)', overflow: 'scroll', '&::-webkit-scrollbar': { display: 'none' } } }; }; ================================================ FILE: app/components/DebouncedInput/DebouncedInput.tsx ================================================ import React, { useState } from 'react'; type Props = { InputComponent: any; isNumerical?: boolean; properties: object; text?: string; updateText: (text: string) => void; }; const DebouncedInput = (props: Props) => { const { InputComponent, isNumerical, properties, updateText, text: inputText } = props; const [typingTimeout, setTypingTimeout] = useState(null); const [text, setText] = useState(inputText || ''); if (!inputText && text !== '' && !typingTimeout) { setText(''); } const handleTextChange = (e: any) => { const { value } = e.target; if (isNumerical) { const re = /^[0-9\b]+$/; if (value === '' || re.test(value.trim())) { setText(value.trim()); clearTimeout(typingTimeout); setTypingTimeout( setTimeout(() => { updateText(value.trim()); setTypingTimeout(null); }, 500) ); return; } } setText(value); clearTimeout(typingTimeout); setTypingTimeout( setTimeout(() => { updateText(value.trim()); setTypingTimeout(null); }, 500) ); }; return ( ); }; DebouncedInput.defaultProps = { isNumerical: false, text: '' }; export default DebouncedInput; ================================================ FILE: app/components/ErrorBoundary/components/GenerateErrorReport.tsx ================================================ import React, { Component } from 'react'; import { shell, ipcRenderer } from 'electron'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { withStyles } from '@material-ui/styles'; import { log } from '../../../utils/log'; import { styles } from '../styles/GenerateErrorReport'; import { promisifiedRimraf } from '../../../api/sys'; import { fileExistsSync } from '../../../api/sys/fileOps'; import { mailTo } from '../../../constants'; import { compressFile } from '../../../utils/gzip'; import GenerateErrorReportBody from './GenerateErrorReportBody'; import { IPCKeys } from '../../../constants/ipc'; class GenerateErrorReport extends Component { compressLog = async () => { try { const { logFile, logFileZippedPath } = await ipcRenderer.invoke( IPCKeys.GetLogPath ); compressFile(logFile, logFileZippedPath); } catch (e) { log.error(e, `GenerateErrorReport -> compressLog`); } }; handleGenerateErrorLogs = async () => { try { const { mailToInstructions, logFileZippedPath } = await ipcRenderer.invoke(IPCKeys.GetLogPath); const { error } = await promisifiedRimraf(logFileZippedPath); if (error) { return null; } this.compressLog(); if (!fileExistsSync(logFileZippedPath)) { return null; } if (window) { window.location.href = `${mailTo} ${mailToInstructions}`; } shell.showItemInFolder(logFileZippedPath); return true; } catch (e) { log.error(e, `GenerateErrorReport -> generateErrorLogs`); return null; } }; render() { const { classes: styles } = this.props; return ( ); } } const mapDispatchToProps = (dispatch, ownProps) => bindActionCreators({}, dispatch); const mapStateToProps = (state, props) => { return {}; }; export default connect( mapStateToProps, mapDispatchToProps )(withStyles(styles)(GenerateErrorReport)); ================================================ FILE: app/components/ErrorBoundary/components/GenerateErrorReportBody.tsx ================================================ import React, { PureComponent } from 'react'; import { Button } from '@material-ui/core'; export default class GenerateErrorReportBody extends PureComponent { render() { const { styles, onGenerateErrorLogs } = this.props as any; return ( ); } } ================================================ FILE: app/components/ErrorBoundary/index.tsx ================================================ import React, { PureComponent } from 'react'; import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/styles'; import { Typography, Button } from '@material-ui/core'; import { ipcRenderer } from 'electron'; import { EOL } from 'os'; import { log } from '../../utils/log'; import { styles } from './styles'; import { imgsrc } from '../../utils/imgsrc'; import GenerateErrorReport from './components/GenerateErrorReport'; import { IPCKeys } from '../../constants/ipc'; class ErrorBoundary extends PureComponent { state = { errorInfo: null }; componentDidCatch(error, errorInfo) { this.setState({ errorInfo }); const _errorInfo = JSON.stringify(errorInfo); log.doLog( `Error boundary log capture:${EOL}${error.toString()}${EOL}${_errorInfo}`, true, error ); } handleReload = async () => { try { const { history } = this.props; history.push('/'); ipcRenderer.invoke(IPCKeys.GetCurrentWindow, 'reload'); } catch (e) { log.error(e, `ErrorBoundary -> handleReload`); } }; render() { const { classes: styles, children } = this.props; const { errorInfo } = this.state; if (errorInfo) { return (
Some Error Occured! Uh oh! Looks like we ran into an issue. Please send us the generated error log so that we can address this.
); } return children; } } export default withRouter(withStyles(styles)(ErrorBoundary)); ================================================ FILE: app/components/ErrorBoundary/styles/GenerateErrorReport.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ subHeading: { ...mixins().noDrag, ...mixins().noselect, marginTop: 15 }, instructions: { listStyle: `none`, color: variables().styles.textLightColor, lineHeight: '24px', marginTop: 15, paddingLeft: 0, marginBottom: 15 }, generateLogsBtn: { marginTop: 15 }, emailIdWrapper: { color: variables().styles.textLightColor, marginTop: 15 }, emailId: { fontWeight: `bold` }, btnStart: { marginTop: 15, height: 35, color: '#fff', borderRadius: 4, transition: theme.transitions.create(['opacity'], { duration: 300 }), background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', '&:hover': { background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) }, '&:active': { background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) } } }); ================================================ FILE: app/components/ErrorBoundary/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ root: { textAlign: `center`, ...mixins().center, ...mixins().absoluteCenter }, bugImg: { ...mixins().noDrag, height: `auto`, borderRadius: '50%', width: 150 }, headings: { ...mixins().noDrag, ...mixins().noselect, marginTop: 15 }, subHeading: { ...mixins().noDrag, ...mixins().noselect, marginTop: 15 }, goBackBtn: { marginTop: 15, marginLeft: 15 }, btnEnd: { height: 35, marginTop: 15, marginLeft: 15, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, borderRadius: 4, border: `1px solid ${theme.palette.primary.border}`, transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, border: `1px solid ${theme.palette.primary.border}`, opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) }, '&:active': { color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, border: `1px solid ${theme.palette.primary.border}`, opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) } } }); ================================================ FILE: app/components/ImportExport/components/dialog.tsx ================================================ /* eslint-disable @typescript-eslint/no-unused-vars */ import React from 'react'; import { useDispatch, useStore } from 'react-redux'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; import { makeStyles } from '@material-ui/styles'; import { Typography, DialogContent, FormControl, FormGroup, Fade, Button, Tooltip } from '@material-ui/core'; import loadFile from '../../../utils/loadFile'; import saveFile from '../../../utils/saveFile'; import { importTasks } from '../../Tasks/actions'; import { importProfiles } from '../../Profiles/actions'; import { importAccounts } from '../../Settings/actions'; import { importAll } from '../../App/actions'; import { styles } from '../styles'; const onExport = ({ state }: { state: any }) => { if (!state) { return; } return saveFile(state); }; const useStyles = makeStyles(styles); const ImportExportDialogContent = () => { const styles = useStyles(); const dispatch = useDispatch(); const store = useStore(); const state = store.getState(); const { Tasks, Profiles, Accounts } = state; const { News, Checkouts, Stores, User, ...rest } = state; const importHandler = async (type: string) => { const { success, data } = await loadFile(type); if (success) { switch (type) { case 'accounts': return dispatch(importAccounts(data)); case 'profiles': return dispatch(importProfiles(data)); case 'tasks': return dispatch(importTasks(data)); case 'all': return dispatch(importAll(data)); default: break; } } return null; }; return (
Profiles
{Profiles.length ? ( ) : ( )}
Accounts
{Accounts.length ? ( ) : ( )}
Tasks
{!isEmpty(Tasks) ? ( ) : ( )}
All Data
Please double check the information you load into Omega. We cover most inconsistencies, but we cannot guarantee data that isn't exported from us directly to be sanitized and correct. Also, please be aware that your current state will not be overwitten during the process of importing data.
); }; export default ImportExportDialogContent; ================================================ FILE: app/components/ImportExport/index.tsx ================================================ import React, { useMemo } from 'react'; import { makeStyles } from '@material-ui/styles'; import classNames from 'classnames'; import { Typography, Button } from '@material-ui/core'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from './components/dialog'; import { styles } from './styles'; const useStyles = makeStyles(styles); const ImportExportDialog = ({ show, toggleState }: { show: boolean; toggleState: any; }) => { const styles = useStyles(); return useMemo( () => (
Application State
), [show] ); }; export default ImportExportDialog; ================================================ FILE: app/components/ImportExport/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ margin: {}, root: {}, dialog: { padding: '16px 24px 24px 24px' }, dialogSizes: { padding: '16px 24px 24px 24px', minHeight: 215 }, topRow: { display: 'flex' }, pointer: { cursor: 'pointer' }, pushRight: { marginRight: 'auto !important' }, flexRow: { display: 'flex', flex: 1, margin: '0' }, flexCol: { display: 'flex', flexDirection: 'column' }, input: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, fontWeight: 400, border: '1px solid #979797', width: '100%' }, createBtn: { fontSize: '12px', justifyContent: 'center', alignSelf: 'flex-start', fontWeight: 400, lineHeight: 1, marginRight: '12px', padding: '6px 16px', borderColor: '#8E83F4', backgroundColor: '#8E83F4', color: '#fff', width: '100%', height: 27, maxHeight: 27, transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { opacity: 0.5, borderColor: '#8E83F4', backgroundColor: '#8E83F4', color: '#fff' } }, deleteBtn: { opacity: 0.5, fontSize: '12px', justifyContent: 'center', alignSelf: 'flex-start', fontWeight: 400, lineHeight: 1, margin: '0 0 0 14px', padding: '6px 16px', borderColor: '#8E83F4', border: '1px solid', backgroundColor: '#fff', color: '#8E83F4', width: '100%', transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { opacity: 0.5, border: '1px solid', backgroundColor: '#fff', color: '#8E83F4' } }, fieldset: { width: '100%' }, fieldSetFirst: { maxWidth: '47.5%', width: '47.5%', margin: '0 8px 0 0' }, fieldSetSecond: { maxWidth: '47.5%', width: '47.5%', margin: '0 0 0 8px' }, accountFieldOne: { width: `30%`, margin: '0 11px 0 0' }, accountFieldTwo: { width: `30%`, margin: '0 11px' }, accountFieldThree: { width: `30%`, margin: '0 0 0 11px' }, fieldSetHalfOne: { width: '47.5%', maxWidth: '47.5%', marginRight: 8, overflow: 'hidden' }, fieldSetHalfTwo: { width: '47.5%', maxWidth: '47.5%', marginLeft: 8, overflow: 'hidden' }, previousIcon: { width: 16, height: 16, display: 'flex', flexDirection: 'column', margin: 'auto 8px' }, nextIcon: { width: 16, height: 16, display: 'flex', flexDirection: 'column', margin: 'auto 8px', color: '#616161' }, subtitle: { marginBottom: 8 }, fmSettingsStylesFix: { marginTop: 10 }, formGroup: { paddingTop: 0 }, subheading: { margin: 'auto 24px', display: 'inline-flex', flexDirection: 'row', justifyContent: 'center', color: '#616161', transition: theme.transitions.create(['color'], { duration: 300 }), '&:hover': { cursor: 'pointer', color: '#8E83F4', transition: theme.transitions.create(['color'], { duration: 300 }), '& > *': { cursor: 'pointer', color: '#8E83F4', transition: theme.transitions.create(['color'], { duration: 300 }) } } }, title: { margin: 24, display: 'flex', flexDirection: 'row', justifyContent: 'center' }, switch: { height: 30 }, block: { marginBottom: 20 }, onBoardingPaper: { position: `relative`, padding: 10, margin: '0 0 8px 0', color: '#fff', backgroundColor: variables().styles.primaryColor.main }, onBoardingPaperArrow: { fontWeight: `bold`, content: ' ', borderBottom: `11px solid ${variables().styles.primaryColor.main}`, borderLeft: '8px solid transparent', borderRight: '8px solid transparent', position: 'absolute', top: -10, left: 2 }, onBoardingPaperBody: { color: variables().styles.primaryColor.secondary }, a: { fontWeight: `bold`, margin: '0 8px 0 4px' }, aLight: { fontWeight: 400, margin: '0 8px' }, btnPositive: { display: 'flex', ...mixins().btnPositive }, btnNegative: { display: 'flex', ...mixins().btnNegative }, btnWarning: { display: 'flex', backgroundColor: 'rgb(199, 193, 255)', color: '#fff', '&:hover': { opacity: 0.5, color: '#fff', backgroundColor: 'rgb(199, 193, 255)', borderColor: 'rgb(199, 193, 255)' }, '&:active': { opacity: 0.5, color: '#fff', backgroundColor: 'rgb(199, 193, 255)', borderColor: 'rgb(199, 193, 255)' } }, bottomRow: { flex: '0 0 auto', margin: '8px 4px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }, textCol: { flex: 1, margin: '8px 16px auto 16px' } }); ================================================ FILE: app/components/Legal/PrivacyPolicy/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../../LoadingIndicator'; /* eslint import/no-cycle: [2, { maxDepth: 1 }] */ export default Loadable(() => import('./index'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Legal/PrivacyPolicy/__tests__/PrivacyPolicy.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import PrivacyPolicy from '../index'; import { withProviders } from '../../../../../test/testUtils'; const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('should render PrivacyPolicy', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); const Root = withProviders({ Component: PrivacyPolicy }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Cookies')).toBeDefined(); }); ================================================ FILE: app/components/Legal/PrivacyPolicy/index.tsx ================================================ import React, { Component } from 'react'; import { withStyles } from '@material-ui/styles'; import { Typography } from '@material-ui/core'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Helmet } from 'react-helmet'; import { APP_WEBSITE, APP_IDENTIFIER, APP_NAME, APP_TITLE, AUTHOR_EMAIL, AUTHOR_NAME } from '../../../constants/meta'; import { openExternalUrl } from '../../../utils/url'; import { resetOverFlowY } from '../../../utils/styleResets'; import { PRIVACY_POLICY_PAGE_TITLE, EFFECTIVE_DATE } from '../../../constants'; import { styles } from './styles'; class PrivacyPolicyPage extends Component { componentWillMount() { resetOverFlowY(); } render() { const { classes: styles } = this.props; return (
{PRIVACY_POLICY_PAGE_TITLE} Privacy policy for {APP_NAME}

Effective date: {EFFECTIVE_DATE}

{APP_NAME} ("us", "we", or "our") operates the app (hereinafter referred to as the "Service").

{AUTHOR_NAME} built the "{APP_NAME}" app as an Open Source app. This SERVICE is provided by {AUTHOR_NAME} at an agreed $35 (USD) per month, and is intended for use as is.

This page informs you of our policies regarding the collection, use, and disclosure of personal data when you use our Service and the choices you have associated with that data.

We use your data to provide, study and improve the Service. By using the Service, you agree to the collection and use of information in accordance with this policy.

Information Collection And Use

We take your privacy very seriously and we DO NOT gather or transfer any sort of personal data out of your device in any form. We gather a very limited amount of ANONYMOUS information from you which will be used for various purposes such as providing and improving our Service to you. You may always choose not to share such ANONYMOUS information with us.

Types of Data Collected

Personal Data While using our Service, we DO NOT ask you to provide us with any kind of personally identifiable information that can be used to contact or identify you ("Personal Data").

Cookies: Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.

This Service does not use these "cookies" explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.

Usage Data: We may also collect anonymous information on how the Service is accessed and used ("Usage Data"). This Usage Data may include information such as your computer's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that you visit, the time and date of your visit, the time spent on those pages, "encrypted" unique device identifiers and other diagnostic data.

LocalStorage Data: We use "LocalStorage" and similar technologies to gather information about the activity on our Service and hold very limited information. LocalStorages are files with small amount of data which may include an anonymous unique identifier.

You may always "Opt-Out" of sharing anonymous usage data with us by navigating to "Settings" option and disabling the "Enable anonymous usage statistics gathering" button.

Examples of LocalStorage files we use:

  • Analytics File. We use Analytics Storage to operate our analytics Service.
  • Settings File. We use Settings Files to remember your preferences and various settings.
  • Log Files. We use Log Files to collect the crash reports for other diagnostic reasons.

Use of Data

{APP_NAME} uses the collected data for various purposes:

  • To provide and maintain the Service
  • To notify you about changes to our Service
  • To allow you to participate in interactive features of our Service when you choose to do so
  • To provide customer care and support
  • To provide analysis or valuable information so that we can improve the Service
  • To monitor the usage of the Service
  • To detect, prevent and address technical issues

Transfer Of Data

Your information may be transferred to — and maintained on — computers located outside of your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from your jurisdiction.

Your consent to this Privacy Policy followed by your submission of such information represents your agreement to that transfer.

{APP_NAME} will take all steps reasonably necessary to ensure that your data is treated securely and in accordance with this Privacy Policy and no transfer of your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of your data and other personal information.

Disclosure Of Data

Legal Requirements

{APP_NAME} may disclose your data in the good faith belief that such action is necessary to:

  • To comply with a legal obligation
  • To protect and defend the rights or property of {APP_NAME}
  • To prevent or investigate possible wrongdoing in connection with the Service
  • To protect the personal safety of users of the Service or the public
  • To protect against legal liability

Security Of Data

The security of your data is important to us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Data, we cannot guarantee its absolute security.

Service Providers

We may employ third party companies and individuals to facilitate our Service ("Service Providers"), to provide the Service on our behalf, to perform Service-related services or to assist us in analyzing how our Service is used.

These third parties have access to your Personal Data only to perform these tasks on our behalf and are obligated not to disclose or use it for any other purpose.

Analytics

We may use third-party Service Providers to monitor and analyze the use of our Service.

Google Analytics

Google Analytics is a web analytics service offered by Google that tracks and reports website/app traffic. Google uses the data collected to track and monitor the use of our Service. We respect the privacy of our users and we have chosen to "Opt-Out" of sharing the data with other Google products & services. We will never allow Google to remarket or use your data for its advertising, benchmarking and other internal services.

For more information on the privacy practices of Google, please visit the Google Privacy & Terms web page:  { openExternalUrl( 'https://policies.google.com/privacy?hl=en', events ); }} > https://policies.google.com/privacy?hl=en

Links To Other Sites

Our Service may contain links to other sites that are not operated by us. If you click on a third party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit.

We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.

Internet Activity

We periodically send out requests to GitHub.com servers to check for the latest app updates and to determine whether an internet connection is available.

You may "Opt-Out" of the "Auto App-update checks" by navigating to "Settings" option and disabling the "Enable auto-update check" button.

Please refer to  { openExternalUrl( 'https://help.github.com/articles/github-privacy-statement/', events ); }} > https://help.github.com/articles/github-privacy-statement/  for more information.

Plugins or Add-ons

We have used google-ga npm package to facilitate the Google analytics feature inside the app.

Crash Reports

We have implemented a very powerful diagnostic tool to capture and report the Crash Reports and bug encountered by the application. The Crash Reports are stored inside your device as log files. These log files can be accessed by navigating to "~/.io.ganeshrvel/{APP_IDENTIFIER}/logs/" folder. You may choose to send us these log files by selecting the "Help" menu > "Report Bugs" and clicking on the "EMAIL ERROR LOGS" buttons.

Children's Privacy

Our Service does not address anyone under the age of 18 ("Children").

We do not knowingly collect personally identifiable information from anyone under the age of 18. If you are a parent or guardian and you are aware that your Children has provided us with Personal Data, please contact us. If we become aware that we have collected Personal Data from children without verification of parental consent, we take steps to remove that information from our servers.

Changes To This Privacy Policy

We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.

You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.

Contact Us

If you have any questions about this Privacy Policy, please contact us through  { openExternalUrl(APP_WEBSITE, events); }} > our Twitter  or by  { openExternalUrl(`mailto:${AUTHOR_EMAIL}`, events); }} > email .

); } } const mapDispatchToProps = (dispatch, ownProps) => bindActionCreators({}, dispatch); const mapStateToProps = (state, props) => { return {}; }; export default connect( mapStateToProps, mapDispatchToProps )(withStyles(styles)(PrivacyPolicyPage)); ================================================ FILE: app/components/Legal/PrivacyPolicy/styles/index.tsx ================================================ import { variables, mixins } from '../../../../styles/js'; export const styles = theme => ({ root: { color: theme.palette.primary.color, borderRadius: '5px', backgroundColor: 'transparent', textAlign: `left`, padding: 30, maxWidth: '800px', margin: '78px auto 48px 0', overflow: 'auto' }, bold: { color: variables().styles.primaryColor.main }, a: { fontWeight: `bold` }, heading: { textDecoration: 'underline' }, body: { lineHeight: `22px` }, noAppDrag: { ...mixins().appDragDisable }, navBtns: { paddingLeft: 5 }, navBtnImgs: { height: 25, width: `auto`, ...mixins().noDrag, ...mixins().noselect } }); ================================================ FILE: app/components/Legal/TermsOfService/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../../LoadingIndicator'; /* eslint import/no-cycle: [2, { maxDepth: 1 }] */ export default Loadable(() => import('./index'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Legal/TermsOfService/__tests__/TermsOfService.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import TermsOfService from '../index'; import { withProviders } from '../../../../../test/testUtils'; const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('should render TermsOfService', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); const Root = withProviders({ Component: TermsOfService }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Purchases')).toBeDefined(); }); ================================================ FILE: app/components/Legal/TermsOfService/index.tsx ================================================ import React, { Component } from 'react'; import { withStyles } from '@material-ui/styles'; import { Typography } from '@material-ui/core'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Helmet } from 'react-helmet'; import { APP_WEBSITE, APP_NAME, APP_TITLE, AUTHOR_EMAIL } from '../../../constants/meta'; import { openExternalUrl } from '../../../utils/url'; import { resetOverFlowY } from '../../../utils/styleResets'; import { TERMS_OF_SERVICE_TITLE, EFFECTIVE_DATE } from '../../../constants'; import { styles } from './styles'; class TermsOfServicePage extends Component { componentWillMount() { resetOverFlowY(); } render() { const { classes: styles } = this.props; return (
{TERMS_OF_SERVICE_TITLE} Terms of Service for {APP_NAME}

Effective date: {EFFECTIVE_DATE}

{APP_NAME} ("us", "we", or "our") operates the app (hereinafter referred to as the "Service").

Your access to and use of the Service is conditioned on your acceptance of and compliance with these Terms. These Terms apply to all visitors, users and others who access or use the Service.

By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the terms then you may not access the Service.

Purchases

If you wish to purchase any product or service made available through the Service ("Purchase"), you may be asked to supply certain information relevant to your Purchase including, without limitation, your credit card number, the expiration date of your credit card, your billing address, and your shipping information.

You represent and warrant that: (i) you have the legal right to use any credit card(s) or other payment method(s) in connection with any Purchase; and that (ii) the information you supply to us is true, correct and complete.

The service may employ the use of third party services for the purpose of facilitating payment and the completion of Purchases. By submitting your information, you grant us the right to provide the information to these third parties subject to our Privacy Policy.

We reserve the right to refuse or cancel your order at any time for certain reasons including but not limited to: product or service availability, fraud or an unauthorised or illegal transaction is suspected, errors in the description or price of the product or service, error in your order or other reasons. You expressly agree that Nebula Automation, LLC cannot accept any liability for loss or damage arising out of such cancellation.

Availability, Errors and Inaccuracies

We are constantly updating product and service offerings on the Service. We may experience delays in updating information on the Service and in our advertising on other web sites. The information found on the Service may contain errors or inaccuracies and may not be complete or current. Products or services may be mispriced, described inaccurately, or unavailable on the Service and we cannot guarantee the accuracy or completeness of any information found on the Service.

We cannot and do not guarantee the accuracy or completeness of any information, including prices, product images, specifications, availability, and services. We reserve the right to change or update information and to correct errors, inaccuracies, or omissions at any time without prior notice. Section "Availability, Errors and Inaccuracies" is without prejudice to existing statutory rights.

Contests, Sweepstakes and Promotions

Any contests, sweepstakes or other promotions (collectively, "Promotions") made available through the Service may be governed by rules that are separate from these Terms. If you participate in any Promotions, please review the applicable rules as well as our Privacy Policy. If the rules for a Promotion conflict with these Terms and Conditions, the Promotion rules will apply. The terms and conditions of any other "Promotions" are independent of this agreement.

Intellectual Property

The Service and its original content, features and functionality are and will remain the exclusive property of Nebula Automation, LLC and its licensors. The Service is protected by copyright, trademark, and other laws of both the United States and foreign countries. Our trademarks and trade dress may not be used in connection with any product or service without the prior written consent of Nebula Automation, LLC.

Links To Other Web Sites

Our Service may contain links to third-party web sites or services that are not owned or controlled by Nebula Automtion, LLC.

Nebula Automation, LLC has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that Nebula Automation, LLC shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such web sites or services.

We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or services that you visit.

Termination

We may terminate or suspend your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if you breach the Terms.

Upon termination, your right to use the Service will immediately cease and all provisions of the Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.

Indemnification

You agree to defend, indemnify and hold harmless Nebula Automation, LLC and its licensee and licensors, and their employees, contractors, agents, officers and directors, from and against any and all claims, damages, obligations, losses, liabilities, costs or debt, and expenses (including but not limited to attorney's fees), resulting from or arising out of a) your use and access of the Service, or b) a breach of these Terms.

Limitation Of Liability

In no event shall Nebula Automation, LLC, nor its directors, employees, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your access to or use of or inability to access or use the Service; (ii) any conduct or content of any third party on the Service; (iii) any content obtained from the Service; and (iv) unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not we have been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose.

Disclaimer

Your use of the Service is at your sole risk. The Service is provided on an "AS IS" and "AS AVAILABLE" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement or course of performance.

Nebula Automation, LLC its subsidiaries, affiliates, and its licensors do not warrant that a) the Service will function uninterrupted, secure or available at any particular time or location; b) any errors or defects will be corrected; c) the Service is free of viruses or other harmful components; or d) the results of using the Service will meet your requirements.

Exclusions

Without limiting the generality of the foregoing and notwithstanding any other provision of these terms, under no circumstances will Nebula Automation, LLC ever be liable to you or any other person for any indirect, incidental, consequential, special, punitive or exemplary loss or damage arising from, connected with, or relating to your use of the Service, these Terms, the subject matter of these Terms, the termination of these Terms or otherwise, including but not limited to personal injury, loss of data, business, markets, savings, income, profits, use, production, reputation or goodwill, anticipated or otherwise, or economic loss, under any theory of liability (whether in contract, tort, strict liability or any other theory or law or equity), regardless of any negligence or other fault or wrongdoing (including without limitation gross negligence and fundamental breach) by Nebula Automation, LLC or any person for whom Nebula Automation, LLC is responsible, and even if Nebula Automation, LLC has been advised of the possibility of such loss or damage being incurred.

Governing Law

These Terms shall be governed and construed in accordance with the laws of the United States of America and Michigan, without regard to its conflict of law provisions.

Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service, and supersede and replace any prior agreements we might have between us regarding the Service.

Changes

We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material we will try to provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.

By continuing to access or use our Service after those revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you must stop using the service.

Privacy Policy

We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.

You agree that they constitute part of these terms. You must read our Privacy Policy before you use the Service.

Contact Us

If you have any questions about these Terms of Service, please contact us through  { openExternalUrl(APP_WEBSITE); }} > our Twitter  or by  { openExternalUrl(`mailto:${AUTHOR_EMAIL}`); }} > email .

); } } const mapDispatchToProps = (dispatch, ownProps) => bindActionCreators({}, dispatch); const mapStateToProps = (state, props) => { return {}; }; export default connect( mapStateToProps, mapDispatchToProps )(withStyles(styles)(TermsOfServicePage)); ================================================ FILE: app/components/Legal/TermsOfService/styles/index.tsx ================================================ import { variables, mixins } from '../../../../styles/js'; export const styles = theme => ({ root: { color: theme.palette.primary.color, borderRadius: '5px', backgroundColor: 'transparent', textAlign: `left`, padding: 30, maxWidth: '800px', margin: '78px auto 48px 0', overflow: 'auto' }, bold: { color: variables().styles.primaryColor.main }, a: { fontWeight: `bold` }, heading: { textDecoration: 'underline' }, body: { lineHeight: `22px` }, noAppDrag: { ...mixins().appDragDisable }, navBtns: { paddingLeft: 5 }, navBtnImgs: { height: 25, width: `auto`, ...mixins().noDrag, ...mixins().noselect } }); ================================================ FILE: app/components/LoadingIndicator/index.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import CircularProgress from '@material-ui/core/CircularProgress'; import { styles } from './styles'; const useStyles = makeStyles(styles); const LoadingIndicator = ({ size = 50 }) => { const styles = useStyles(); return ( ); }; export default LoadingIndicator; ================================================ FILE: app/components/LoadingIndicator/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ root: {} }); ================================================ FILE: app/components/NoChildrenComponent/index.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { Typography, Grid } from '@material-ui/core'; import { styles } from './styles'; const useStyles = makeStyles(styles); const NoChildrenComponent = ({ label, variant }: { label: string; variant: any; }) => { const styles = useStyles(); return ( {label} ); }; export default NoChildrenComponent; ================================================ FILE: app/components/NoChildrenComponent/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ root: { display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', width: '100%' }, background: { flexDirection: 'row', display: 'flex', height: '100%' }, alignCenter: { alignSelf: 'center', height: '100%' }, center: { display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: '100%' }, text: { color: '#919191' } }); ================================================ FILE: app/components/Profiles/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../LoadingIndicator'; export default Loadable(() => import('./Profiles'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Profiles/Profiles.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { Grid } from '@material-ui/core'; import ActionBar from './components/actionBar'; import CreateDialog from './components/create/ProfileCreateDialog'; import ProfileGrid from './components/grid'; import { styles } from './styles'; const useStyles = makeStyles(styles); const ProfilesPrimitve = () => { const styles = useStyles(); return ( ); }; export default ProfilesPrimitve; ================================================ FILE: app/components/Profiles/__tests__/Profiles.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import Profiles from '../Profiles'; import { withProviders } from '../../../../test/testUtils'; const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('should render Profiles', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); const Root = withProviders({ Component: Profiles }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('No Profiles')).toBeDefined(); }); ================================================ FILE: app/components/Profiles/actions/index.tsx ================================================ import { importProfiles, exportProfiles, editProfile, loadProfile, saveProfile, deleteProfile, deleteProfiles, duplicateProfile, profilesActionTypes, profilesActionList, profilesActions } from './profiles'; export { profilesActionTypes }; export { profilesActionList }; export { profilesActions }; export const PROFILE_FIELDS = { EDIT_SHIPPING: 'EDIT_SHIPPING', EDIT_BILLING: 'EDIT_BILLING', EDIT_PAYMENT: 'EDIT_PAYMENT', EDIT_NAME: 'EDIT_NAME', TOGGLE_MATCHES: 'TOGGLE_MATCHES' }; export const LOCATION_FIELDS = { NAME: 'name', ADDRESS: 'address', APT: 'apt', CITY: 'city', PROVINCE: 'province', ZIP: 'zip', COUNTRY: 'country', EMAIL: 'email', PHONE: 'phone' }; export const PAYMENT_FIELDS = { HOLDER: 'holder', CARD: 'card', TYPE: 'type', EXP: 'exp', CVV: 'cvv' }; export { importProfiles, exportProfiles, saveProfile, loadProfile, deleteProfile, deleteProfiles, duplicateProfile, editProfile }; ================================================ FILE: app/components/Profiles/actions/profiles.tsx ================================================ import prefixer from '../../../utils/reducerPrefixer'; import { toggleField, SETTINGS_FIELDS } from '../../Settings/actions'; const prefix = '@@Profile'; export const profilesActionList = [ 'DELETE', 'DELETE_ALL', 'DUPLICATE', 'EDIT', 'SAVE', 'LOAD', 'IMPORT', 'EXPORT' ]; export const profilesActions = profilesActionList.map(a => `${prefix}/${a}`); export const profilesActionTypes = prefixer(prefix, profilesActionList); export const deleteProfiles = () => { return (dispatch: any) => { dispatch({ type: profilesActionTypes.DELETE_ALL }); }; }; export const deleteProfile = (profile: any) => { return (dispatch: any) => { dispatch({ type: profilesActionTypes.DELETE, payload: profile }); }; }; export const duplicateProfile = (profile: any) => { return (dispatch: any) => { dispatch({ type: profilesActionTypes.DUPLICATE, payload: profile }); }; }; export function importProfiles(profiles: any[]) { return (dispatch: any) => { dispatch({ type: profilesActionTypes.IMPORT, payload: profiles }); }; } export function exportProfiles(profiles: any[]) { return (dispatch: any) => { dispatch({ type: profilesActionTypes.EXPORT, payload: profiles }); }; } export function loadProfile(profile: any) { return (dispatch: any) => { dispatch({ type: profilesActionTypes.LOAD, payload: profile }); dispatch(toggleField(SETTINGS_FIELDS.CREATE_PROFILE)); }; } export function saveProfile(profile: any) { return (dispatch: any) => { dispatch({ type: profilesActionTypes.SAVE, payload: profile }); }; } export const editProfile = ({ id, type, field, value }: { id: string; type: string; field?: string; value?: any; }) => ({ type: profilesActionTypes.EDIT, payload: { id, type, field, value } }); ================================================ FILE: app/components/Profiles/components/actionBar/index.tsx ================================================ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useConfirm } from 'material-ui-confirm'; import { makeStyles } from '@material-ui/styles'; import { Grid } from '@material-ui/core'; import Create from '@material-ui/icons/Add'; import Delete from '@material-ui/icons/Delete'; import { log } from '../../../../utils/log'; import { deleteProfiles } from '../../actions'; import { makeProfiles } from '../../selectors'; import { toggleField, SETTINGS_FIELDS } from '../../../Settings/actions'; import { styles } from '../../styles/actionBar'; const useStyles = makeStyles(styles); const ActionBarComponent = () => { const styles = useStyles(); const dispatch = useDispatch(); const confirm = useConfirm(); const profiles = useSelector(makeProfiles); const createHandler = async (e: any) => { e.stopPropagation(); dispatch(toggleField(SETTINGS_FIELDS.CREATE_PROFILE)); }; const deleteHandler = async (e: any) => { e.stopPropagation(); if (!profiles.length) { return; } try { await confirm({ title: `Are you sure you want to remove all profiles?`, description: 'This action cannot be undone.', confirmationText: 'Yes', cancellationText: 'No', confirmationButtonProps: { color: 'primary', style: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' } }, cancellationButtonProps: { color: 'primary', style: { width: 105, height: 35 } } }); dispatch(deleteProfiles()); } catch (e) { if (!e) { return; } log.error(e, 'Profiles -> Remove'); } }; return ( ); }; export default ActionBarComponent; ================================================ FILE: app/components/Profiles/components/card/index.tsx ================================================ import React from 'react'; import { useDispatch } from 'react-redux'; import { useConfirm } from 'material-ui-confirm'; import { Grid, Typography, IconButton } from '@material-ui/core'; import { makeStyles } from '@material-ui/styles'; import LibraryAddIcon from '@material-ui/icons/LibraryAdd'; import DeleteIcon from '@material-ui/icons/Delete'; import { deleteProfile, loadProfile, duplicateProfile } from '../../actions'; import { imgsrc } from '../../../../utils/imgsrc'; import { log } from '../../../../utils/log'; import { styles } from '../../styles/card'; const useStyles = makeStyles(styles); const CardComponent = ({ profile }: { profile: any }) => { const styles = useStyles(); const dispatch = useDispatch(); const confirm = useConfirm(); const closeHandler = async (event: any) => { event.stopPropagation(); try { await confirm({ title: `Remove Profile "${profile.name}"?`, description: 'This action cannot be undone.', confirmationText: 'Yes', cancellationText: 'No', dialogProps: { classes: { paper: styles.paperRoot } }, confirmationButtonProps: { classes: { root: styles.confirmBtn }, style: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' } }, cancellationButtonProps: { classes: { root: styles.cancelBtn }, style: { width: 105, height: 35 } } }); dispatch(deleteProfile(profile)); } catch (e) { log.error(e, 'Profile -> Remove Profile Cancelled'); } }; const loadHandler = (event: any) => { event.stopPropagation(); dispatch(loadProfile(profile)); }; const duplicateHandler = (event: any) => { event.stopPropagation(); dispatch(duplicateProfile(profile)); }; return (
{profile.payment.holder} {profile.name} • • • • • • • • • • • • {profile.payment.card.substr(-4)}
); }; export default CardComponent; ================================================ FILE: app/components/Profiles/components/create/ProfileCreateDialog.tsx ================================================ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/styles'; import { Typography, Button, Dialog, MobileStepper } from '@material-ui/core'; import ShippingContent from './shipping'; import BillingContent from './billing'; import PaymentContent from './payment'; import { makeCreateProfile } from '../../../Settings/selectors'; import { styles } from '../../styles/createDialog'; import { saveProfile, loadProfile, PROFILE_FIELDS } from '../../actions'; import { makeCurrentProfile } from '../../selectors'; const useStyles = makeStyles(styles); const DialogContent = ({ activeStep, profile }: { activeStep: number; profile: any; }) => { switch (activeStep) { case 0: return ( ); case 1: return ( ); case 2: return ( ); default: return ( ); } }; const DialogTitle = ({ activeStep, styles }: { activeStep: number; styles: any; }) => { switch (activeStep) { case 0: { return ( Shipping Details ); } case 1: { return ( Billing Details ); } case 2: { return ( Payment Details ); } default: { return ( Shipping Details ); } } }; const CreateDialog = () => { const styles = useStyles(); const dispatch = useDispatch(); const open = useSelector(makeCreateProfile); const profile = useSelector(makeCurrentProfile); const [activeStep, setActiveStep] = React.useState(0); const handleNext = () => { const { matches } = profile; if (matches && activeStep === 0) { return setActiveStep(prevActiveStep => prevActiveStep + 2); } setActiveStep(prevActiveStep => prevActiveStep + 1); }; const handleBack = () => { const { matches } = profile; if (matches && activeStep === 2) { return setActiveStep(prevActiveStep => prevActiveStep - 2); } setActiveStep(prevActiveStep => prevActiveStep - 1); }; const handleCloseModal = () => { dispatch(loadProfile(null)); return setActiveStep(0); }; const handleSaveProfile = () => { dispatch(saveProfile(profile)); return handleCloseModal(); }; return ( {profile.id ? 'Save' : 'Create'} ) : ( ) } backButton={ activeStep === 0 ? ( ) : ( ) } /> ); }; export default CreateDialog; ================================================ FILE: app/components/Profiles/components/create/__tests__/ProfileCreateDialog.spec.tsx ================================================ import { render, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import ProfileCreateDialog from '../ProfileCreateDialog'; import { withProviders } from '../../../../../../test/testUtils'; it('should render ProfileCreateDialog', async () => { const initialState = { Settings: { toggleCreateProfile: true } }; const Root = withProviders({ Component: ProfileCreateDialog, initialState }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Shipping Details')).toBeDefined(); }); it('should render ProfileCreateDialog and go to billing details', async () => { const initialState = { Settings: { toggleCreateProfile: true } }; const Root = withProviders({ Component: ProfileCreateDialog, initialState }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); const nextButton = getByText('Next').closest('button'); fireEvent.click(nextButton); expect(getByText('Billing Details')).toBeDefined(); }); it('should render ProfileCreateDialog and go to payment details', async () => { const initialState = { Settings: { toggleCreateProfile: true } }; const Root = withProviders({ Component: ProfileCreateDialog, initialState }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); const nextButton = getByText('Next').closest('button'); // go to shipping details fireEvent.click(nextButton); // go to payment details fireEvent.click(nextButton); expect(getByText('Payment Details')).toBeDefined(); }); ================================================ FILE: app/components/Profiles/components/create/billing.tsx ================================================ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import WindowedSelect from 'react-windowed-select'; import classNames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { Typography, DialogContent, Input, FormGroup } from '@material-ui/core'; import { colorStyles, IndicatorSeparator } from '../../../../styles/select'; import { buildCountryOptions, buildProvinceOptions, isProvinceDisabled } from '../../../../constants'; import DebouncedInput from '../../../DebouncedInput/DebouncedInput'; import { styles } from '../../styles/createDialog'; import { editProfile, LOCATION_FIELDS } from '../../actions'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); const BillingFields = ({ id, type, location }: { id: string; type: string; location: any; }) => { const styles = useStyles(); const dispatch = useDispatch(); const theme = useSelector((state: RootState) => state.Theme); const editHandler = (field: string, value: any) => { dispatch( editProfile({ id, type, field, value }) ); }; return (
Name editHandler(LOCATION_FIELDS.NAME, value)} />
Address editHandler(LOCATION_FIELDS.ADDRESS, value)} />
Address 2 editHandler(LOCATION_FIELDS.APT, value)} />
City editHandler(LOCATION_FIELDS.CITY, value)} /> State editHandler(LOCATION_FIELDS.PROVINCE, e)} />
Country editHandler(LOCATION_FIELDS.COUNTRY, e)} /> Zip editHandler(LOCATION_FIELDS.ZIP, value)} />
Email editHandler(LOCATION_FIELDS.EMAIL, value)} />
Phone Number editHandler(LOCATION_FIELDS.PHONE, value)} />
); }; export default BillingFields; ================================================ FILE: app/components/Profiles/components/create/payment.tsx ================================================ import React, { useState } from 'react'; import { useDispatch } from 'react-redux'; import Cards from 'react-credit-cards'; import NumberFormat from 'react-number-format'; import { makeStyles } from '@material-ui/styles'; import { Typography, DialogContent, Input, FormGroup } from '@material-ui/core'; import DebouncedInput from '../../../DebouncedInput/DebouncedInput'; import { styles } from '../../styles/createDialog'; import { editProfile, PROFILE_FIELDS, PAYMENT_FIELDS } from '../../actions'; const useStyles = makeStyles(styles); const PaymentFields = ({ id, name, payment }: { id: string; name: string; payment: any; }) => { const styles = useStyles(); const dispatch = useDispatch(); const [focus, setFocus] = useState(undefined); const handleSetFocus = (e: any) => { setFocus(e.target.name); }; const editHandler = ({ field, value, type = PROFILE_FIELDS.EDIT_PAYMENT }: { field?: string; value?: any; type?: string; }) => { dispatch( editProfile({ id, type, field, value }) ); }; return (
Name editHandler({ field: PAYMENT_FIELDS.HOLDER, value }) } />
Card Number editHandler({ field: PAYMENT_FIELDS.CARD, value: value.replace(/\s/g, '') }) } />
Exp. editHandler({ field: PAYMENT_FIELDS.EXP, value }) } /> CVC editHandler({ field: PAYMENT_FIELDS.CVV, value }) } />
Profile Name editHandler({ type: PROFILE_FIELDS.EDIT_NAME, value }) } />
); }; export default PaymentFields; ================================================ FILE: app/components/Profiles/components/create/shipping.tsx ================================================ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import WindowedSelect from 'react-windowed-select'; import classNames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { Typography, DialogContent, Input, FormControlLabel, FormGroup, Checkbox } from '@material-ui/core'; import { colorStyles, IndicatorSeparator } from '../../../../styles/select'; import { buildCountryOptions, buildProvinceOptions, isProvinceDisabled } from '../../../../constants'; import DebouncedInput from '../../../DebouncedInput/DebouncedInput'; import { styles } from '../../styles/createDialog'; import { editProfile, LOCATION_FIELDS, PROFILE_FIELDS } from '../../actions'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); const ShippingFields = ({ id, type, matches, location }: { id: string; type: string; matches: boolean; location: any; }) => { const styles = useStyles(); const dispatch = useDispatch(); const theme = useSelector((state: RootState) => state.Theme); const editHandler = (field: string, value: any) => { dispatch( editProfile({ id, type, field, value }) ); }; return (
Name editHandler(LOCATION_FIELDS.NAME, value)} />
Address editHandler(LOCATION_FIELDS.ADDRESS, value)} />
Address 2 editHandler(LOCATION_FIELDS.APT, value)} />
City editHandler(LOCATION_FIELDS.CITY, value)} /> State editHandler(LOCATION_FIELDS.PROVINCE, e)} />
Country editHandler(LOCATION_FIELDS.COUNTRY, e)} /> Zip editHandler(LOCATION_FIELDS.ZIP, value)} />
Email editHandler(LOCATION_FIELDS.EMAIL, value)} />
Phone Number editHandler(LOCATION_FIELDS.PHONE, value)} />
dispatch( editProfile({ id, type: PROFILE_FIELDS.TOGGLE_MATCHES }) ) } value={matches ? 'true' : 'false'} color="primary" /> } label={ Same Billing Information } />
); }; export default ShippingFields; ================================================ FILE: app/components/Profiles/components/grid/index.tsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import NoChildrenComponent from '../../../NoChildrenComponent'; import { makeProfiles } from '../../selectors'; import CardComponent from '../card'; const ProfileGrid = () => { const profiles = useSelector(makeProfiles); if (!profiles.length) { return ; } return profiles.map((profile: any) => ( )); }; export default ProfileGrid; ================================================ FILE: app/components/Profiles/reducers/current.tsx ================================================ import { PURGE } from 'redux-persist'; import { profilesActionTypes, PROFILE_FIELDS } from '../actions'; import { locationReducer, shipping, billing, Address } from './location'; import { paymentReducer, payment, Payment } from './payment'; export const initialState: CurrentProfile = { id: null, name: '', matches: false, shipping: { ...shipping }, billing: { ...billing }, payment: { ...payment } }; const profileReducer = (state: CurrentProfile, payload: any) => { const { type, field, value } = payload; if (type === PROFILE_FIELDS.EDIT_SHIPPING) { return { ...state, shipping: locationReducer(state.shipping, { field, value }) }; } if (type === PROFILE_FIELDS.EDIT_BILLING) { return { ...state, billing: locationReducer(state.billing, { field, value }) }; } if (type === PROFILE_FIELDS.EDIT_PAYMENT) { return { ...state, payment: paymentReducer(state.payment, { field, value }) }; } if (type === PROFILE_FIELDS.EDIT_NAME) { return { ...state, name: value }; } if (type === PROFILE_FIELDS.TOGGLE_MATCHES) { return { ...state, matches: !state.matches }; } return state; }; type Action = { type: string; payload?: any; }; export type CurrentProfile = { id: string | null; name: string; matches: boolean; shipping: Address; billing: Address; payment: Payment; }; export function CurrentProfile(state = initialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return { ...initialState }; case profilesActionTypes.LOAD: if (payload === null) { return { ...initialState }; } if (payload) { return payload; } return state; case profilesActionTypes.SAVE: if (!payload) { return state; } return { ...initialState }; case profilesActionTypes.DELETE: if (payload && payload.id) { return { ...initialState }; } return state; case profilesActionTypes.EDIT: return profileReducer(state, payload); default: return state; } } ================================================ FILE: app/components/Profiles/reducers/index.tsx ================================================ import { CurrentProfile } from './current'; import { Profiles } from './profiles'; export { CurrentProfile, Profiles }; ================================================ FILE: app/components/Profiles/reducers/location.tsx ================================================ import { LOCATION_FIELDS } from '../actions'; export const location: Address = { name: '', address: '', apt: '', city: '', province: null, zip: '', country: { label: 'United States', value: 'US' }, phone: '', email: '' }; export type Address = { name: string; address: string; apt: string; city: string; province: null | { label: string; value: string; }; zip: string; country: null | { label: string; value: string; }; phone: string; email: string; }; export const shipping = { ...location }; export const billing = { ...location }; type Action = { field: string; value: any; }; export const locationReducer = (state = { ...location }, action: Action) => { const { field, value } = action; if (!field) { return state; } switch (field) { case LOCATION_FIELDS.COUNTRY: { if (!value || (state.country && value.value === state.country.value)) { return state; } return { ...state, [field]: value, province: null }; } case LOCATION_FIELDS.PROVINCE: { if (!value || (state.province && value.value === state.province?.value)) { return state; } return { ...state, [field]: value }; } default: { return { ...state, [field]: value }; } } }; ================================================ FILE: app/components/Profiles/reducers/payment.tsx ================================================ export const payment: Payment = { holder: '', card: '', exp: '', cvv: '', type: '' }; export type Payment = { card: string; cvv: string; exp: string; holder: string; type: string; }; type Action = { field: string; value: string; }; export const paymentReducer = (state = payment, action: Action) => { const { field, value } = action; if (!field) { return state; } switch (field) { default: return { ...state, [field]: value }; } }; ================================================ FILE: app/components/Profiles/reducers/profiles.tsx ================================================ import uuidv4 from 'uuidv4'; import valid from 'card-validator'; import { ipcRenderer } from 'electron'; import { PURGE } from 'redux-persist'; import { IPCKeys } from '../../../constants/ipc'; import { profilesActionTypes } from '../actions'; import { CurrentProfile } from './current'; export type Profiles = CurrentProfile[]; type Action = { type: string; payload?: any; }; export const initialState: Profiles = []; export function Profiles(state = initialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...initialState]; case profilesActionTypes.IMPORT: { if (!payload || (payload && !payload.length)) { return state; } // verify integrity... if ( !payload.every( (profile: any) => profile.name && profile.billing && profile.shipping && profile.payment ) ) { return state; } return [ ...state, ...payload.map((p: any) => { const newProfile = { ...p }; let id: string; const checker = (p: any) => p.id === id; do { id = uuidv4(); } while (state.some(checker)); newProfile.id = id; // determine card type... const validator: any = valid.number(newProfile.payment.card); newProfile.payment.type = validator?.card?.type; ipcRenderer.send(IPCKeys.AddProfiles, newProfile); return newProfile; }) ]; } case profilesActionTypes.SAVE: { if (!payload) { return state; } if (payload.matches) { payload.billing = { ...payload.shipping }; } // updating existing profile... if (payload.id) { return state.map(p => { if (p.id === payload.id) { // determine card type... const validator: any = valid.number(payload.payment.card); payload.payment.type = validator?.card?.type; const newProfile = { ...p, ...payload }; ipcRenderer.send(IPCKeys.AddProfiles, newProfile); return newProfile; } return p; }); } // creating new profile... let id: string; const checker = (p: any) => p.id === id; do { id = uuidv4(); } while (state.some(checker)); payload.id = id; // determine card type... const validator: any = valid.number(payload.payment.card); payload.payment.type = validator?.card?.type; ipcRenderer.send(IPCKeys.AddProfiles, payload); return [...state, payload]; } case profilesActionTypes.DUPLICATE: { if (!payload) { return state; } const newProfile = { ...payload }; let id: string; const checker = (p: any) => p.id === id; do { id = uuidv4(); } while (state.some(checker)); newProfile.id = id; newProfile.name = `${newProfile.name} Copy`; ipcRenderer.send(IPCKeys.AddProfiles, newProfile); return [...state, newProfile]; } case profilesActionTypes.DELETE: { if (!payload) { return state; } ipcRenderer.send(IPCKeys.RemoveProfiles, payload); return state.filter(p => p.id !== payload.id); } case profilesActionTypes.DELETE_ALL: { ipcRenderer.send(IPCKeys.RemoveProfiles, state); return [...initialState]; } default: return state; } } ================================================ FILE: app/components/Profiles/selectors.tsx ================================================ import { createSelector } from 'reselect'; import { Profiles, CurrentProfile } from './reducers'; import { RootState } from '../../store/reducers'; const selectProfiles = (state: RootState) => state.Profiles; const selectCurrentProfile = (state: RootState) => state.CurrentProfile; export const makeProfiles = createSelector( selectProfiles, state => state || Profiles ); export const makeCurrentProfile = createSelector( selectCurrentProfile, state => state || CurrentProfile ); ================================================ FILE: app/components/Profiles/styles/actionBar.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { justifyContent: 'center', zIndex: 999 }, background: { height: 40, opacity: 0.25, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', boxShadow: '0px 4px 8px 0px rgba(0,0,0,0.25)', borderRadius: 25.5, flexDirection: 'row', display: 'flex', position: 'absolute', transition: theme.transitions.create(['opacity'], { duration: 300 }), bottom: 24, width: 85, '&:hover': { transition: theme.transitions.create(['opacity'], { duration: 300 }), opacity: 1 } }, alignCenter: { alignSelf: 'center', height: '100%' }, center: { display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: '100%' }, table: { height: `calc(100% - 125px)` }, actionIcon: { cursor: 'pointer', color: '#fff', height: 24, width: 24, '&:hover': { opacity: 0.5 } }, paper: { height: '100%', display: 'flex', flexDirection: 'column' }, toolbar: { paddingLeft: '64px', paddingRight: '32px' }, title: { flex: '0 0 auto' }, spacer: { flex: '1 1 100%' } }; }; ================================================ FILE: app/components/Profiles/styles/card.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { cardCell: { flexBasis: 'calc(100% / 3)', float: 'left' }, root: { margin: 10, backgroundColor: theme.palette.primary.card, height: 200, flexGrow: 1, padding: 0, listStyle: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', borderRadius: 12, transition: theme.transitions.create(['background-color'], { duration: 300 }), '&:hover': { cursor: 'pointer', transition: theme.transitions.create(['background-color'], { duration: 300 }), backgroundColor: 'rgba(131,119,244,1)' } }, paperRoot: { backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, confirmBtn: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' }, cancelBtn: { width: 105, height: 35, background: theme.palette.primary.secondary, color: theme.palette.primary.color, '&:hover': { opacity: 0.5, background: theme.palette.primary.secondary, color: theme.palette.primary.color } }, background: { height: '100%', width: '100%' }, gridContainer: { margin: 24, width: 'calc(100% - 48px)' }, gridContainerMid: { margin: '24px 24px 0 24px', width: 'calc(100% - 48px)' }, gridContainerEnd: { margin: '0 24px 24px 24px', width: 'calc(100% - 48px)', justifyContent: 'flex-end' }, cardHolder: { display: 'flex', flex: 1, color: '#fff', fontSize: 16, fontWeight: 500 }, cardName: { display: 'flex', flex: 1, color: '#fff', fontSize: 14, fontWeight: 400 }, dots: { display: 'flex', color: '#fff', fontSize: 16, fontWeight: 400, marginRight: 8 }, cardNumber: { display: 'flex', flex: 1, color: '#fff', fontSize: 16, fontWeight: 400 }, actionIconWrapper: { display: 'flex', padding: 0, alignSelf: 'flex-start', '&:hover': { background: 'transparent', backgroundColor: 'transparent' } }, cardTypeImg: { height: 50, width: 50, objectFit: 'scale-down' }, actionIcon: { color: '#fff', '&:hover': { opacity: 0.5 } } }; }; ================================================ FILE: app/components/Profiles/styles/createDialog.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ margin: {}, rootLg: { height: 480, width: 612 }, rootSm: { width: 350, height: 558 }, fieldset: { width: `45%`, margin: '0 12px', border: 0, display: 'inline-flex', padding: 0, position: 'relative', minWidth: 0, flexDirection: 'column', verticalAlign: 'top' }, fieldsetFull: { width: '100%' }, dialogContent: { margin: '16px 48px', padding: 0 }, subtitle: { fontSize: 12, fontWeight: 500 }, fmSettingsStylesFix: { marginTop: 10 }, formGroupOne: { margin: '0 4px 16px 0', flexWrap: 'nowrap', display: 'inline-flex' }, formGroupTwo: { margin: '0 0 16px 4px', flexWrap: 'nowrap', display: 'inline-flex' }, formGroup: { margin: '0 0 16px 0' }, formGroupCenter: { margin: '8px 0 16px 0' }, subheading: { marginBottom: 5 }, title: { color: theme.palette.primary.color, display: 'flex', justifyContent: 'center', margin: '24px 24px 16px 24px' }, inputWrapper: { margin: 0 }, input: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, fontWeight: 400, height: 29, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, border: `1px solid ${theme.palette.primary.border}`, width: '100%', '::placeholder': { color: 'rgba(0, 0, 0, 0.87)' } }, flexOne: { flex: 2 }, flexNone: { flex: 1 }, flex: { display: 'flex' }, block: {}, onBoardingPaper: { position: `relative`, padding: 10, marginTop: 4, backgroundColor: theme.palette.primary.background }, onBoardingPaperArrow: { fontWeight: `bold`, content: ' ', borderBottom: `11px solid ${theme.palette.primary.background}`, borderLeft: '8px solid transparent', borderRight: '8px solid transparent', position: 'absolute', top: -10, left: 2 }, onBoardingPaperBody: { color: variables().styles.primaryColor.background }, a: { fontWeight: `bold` }, stepper: { maxWidth: 400, flexGrow: 1 }, bar: {}, progressBar: { backgroundColor: 'rgba(164,155,255, 0.333)', color: '#d8d8d8' }, stepperRoot: { position: 'absolute', bottom: 0, width: '100%' }, btnPositive: { borderColor: '#8E83F4', backgroundColor: '#8E83F4', '&:hover': { opacity: 0.5, borderColor: '#8E83F4', backgroundColor: '#8E83F4' } } }); ================================================ FILE: app/components/Profiles/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { margin: 25, marginTop: 75, display: 'flex', flexWrap: 'wrap', flexDirection: 'row', alignContent: 'flex-start', width: '100%', height: 'calc(100% - 100px)', overflow: 'scroll', '&::-webkit-scrollbar': { display: 'none' } } }; }; ================================================ FILE: app/components/Progressbar/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../LoadingIndicator'; /* eslint import/no-cycle: [2, { maxDepth: 1 }] */ export default Loadable(() => import('./index'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Progressbar/index.tsx ================================================ import { ipcRenderer } from 'electron'; import React, { Component } from 'react'; import { withStyles } from '@material-ui/styles'; import LinearProgress from '@material-ui/core/LinearProgress'; import { Typography } from '@material-ui/core'; import { styles } from './styles'; import { IPCKeys } from '../../constants/ipc'; class ProgressbarPage extends Component { initialState: { progressTitle: string; value: number; variant: string; }; updateTitleTimeout: any; constructor(props: any) { super(props); this.initialState = { progressTitle: `Hang tight! We're downloading the update...`, value: 0, variant: `indeterminate` }; this.updateTitleTimeout = null; this.state = { ...this.initialState }; } componentWillMount() { ipcRenderer.on('progressBarDataCommunication', (_: any, { ...args }) => { console.info(`Progressbar -> data communication`, args); this.setState({ ...args }); }); if (this.updateTitleTimeout) { clearTimeout(this.updateTitleTimeout); this.updateTitleTimeout = null; } this.updateTitleTimeout = setTimeout(() => { this.setState({ progressTitle: `Hang tight! We're still working on it...` }); clearTimeout(this.updateTitleTimeout); // possible that they're experiencing network timeouts, lets let them know... this.updateTitleTimeout = setTimeout(() => { this.setState({ progressTitle: `Possible network issues, please try again...` }); setTimeout(() => { ipcRenderer.invoke(IPCKeys.GetCurrentWindow, 'close'); }, 2500); }, 15000); }, 15000); } componentWillUnmount() { if (this.updateTitleTimeout) { clearTimeout(this.updateTitleTimeout); this.updateTitleTimeout = null; } } render() { const { classes: styles } = this.props; const { progressTitle, value, variant } = this.state; return (
{progressTitle}
); } } export default withStyles(styles)(ProgressbarPage); ================================================ FILE: app/components/Progressbar/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ root: { textAlign: `left`, ...mixins().center, width: 500, marginTop: 10 }, progressBodyText: { marginBottom: 10 }, progressTitle: { fontWeight: 'bold', marginTop: 10, marginBottom: 25 } }); ================================================ FILE: app/components/Proxies/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../LoadingIndicator'; export default Loadable(() => import('./Proxies'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Proxies/Proxies.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import ProxyTable from './components/Table/ProxyTable'; import ProxyActionBar from './components/ProxyActionBar'; import ProxyCreateDialog from './components/ProxyCreateDialog'; import { styles } from './styles'; const useStyles = makeStyles(styles); const Proxies = () => { const styles = useStyles(); return (
); }; export default Proxies; ================================================ FILE: app/components/Proxies/__tests__/Proxies.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import Proxies from '../Proxies'; import { withProviders } from '../../../../test/testUtils'; const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('should render Proxies', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); const Root = withProviders({ Component: Proxies }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('IP Address')).toBeDefined(); }); ================================================ FILE: app/components/Proxies/actions/index.tsx ================================================ /* eslint-disable no-unused-vars */ import prefixer from '../../../utils/reducerPrefixer'; import { toggleField, SETTINGS_FIELDS } from '../../Settings/actions'; const prefix = '@@Proxies'; const proxiesTypesList = [ 'EDIT', 'RESET_STATUS', 'UPDATE_STATUS', 'SET_SELECTED', 'SET_ALL_SELECTED', 'LOAD', 'SELECT', 'CREATE', 'SET_LOADING', 'DELETE_GROUP', 'DELETE_PROXY', 'UPDATE_SPEED', 'REMOVE_FAILED' ]; export const proxiesActions = proxiesTypesList.map(a => `${prefix}/${a}`); export const proxiesActionTypes = prefixer(prefix, proxiesTypesList); export function deleteProxies(group) { return dispatch => { dispatch({ type: proxiesActionTypes.DELETE_GROUP, payload: group }); }; } export function deleteProxy(group, proxy) { return dispatch => { dispatch({ type: proxiesActionTypes.DELETE_PROXY, payload: { group, proxy } }); }; } export function removeFailed(group) { return dispatch => { dispatch({ type: proxiesActionTypes.REMOVE_FAILED, payload: group }); }; } export function resetProxiesStatus() { return dispatch => { dispatch({ type: proxiesActionTypes.RESET_STATUS }); }; } export function loadProxies(proxies) { return dispatch => { dispatch({ type: proxiesActionTypes.LOAD, payload: proxies }); dispatch(toggleField(SETTINGS_FIELDS.CREATE_PROXIES)); }; } export function selectProxies(selected) { return { type: proxiesActionTypes.SELECT, payload: selected }; } export const createProxies = proxy => { return dispatch => { dispatch({ type: proxiesActionTypes.CREATE, payload: proxy }); }; }; export const updateProxyStatus = (groupId, proxy, status) => { return dispatch => { dispatch({ type: proxiesActionTypes.UPDATE_STATUS, payload: { groupId, proxy, status } }); }; }; export function setLoading(proxies) { return { type: proxiesActionTypes.SET_LOADING, payload: proxies }; } export function setSelected(selected) { return { type: proxiesActionTypes.SET_SELECTED, payload: selected }; } export function setAllSelected() { return { type: proxiesActionTypes.SET_ALL_SELECTED }; } export const editProxies = ({ id, field, value }) => ({ type: proxiesActionTypes.EDIT, payload: { id, field, value } }); export const PROXY_FIELDS = { NAME: 'name', PROXIES: 'proxies' }; export const proxyUpdateSpeed = (group: string, results: any) => ({ type: proxiesActionTypes.UPDATE_SPEED, payload: { group, results } }); ================================================ FILE: app/components/Proxies/components/ProxyActionBar.tsx ================================================ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import classnames from 'classnames'; import { useConfirm } from 'material-ui-confirm'; import { makeStyles } from '@material-ui/styles'; import { Fade, Grid, Tooltip } from '@material-ui/core'; import WindowedSelect from 'react-windowed-select'; import Delete from '@material-ui/icons/Delete'; import Create from '@material-ui/icons/Add'; import Edit from '@material-ui/icons/Edit'; import { ipcRenderer } from 'electron'; import SpeedIcon from '@material-ui/icons/Speed'; import { buildProxiesOptions } from '../../../constants'; import { log } from '../../../utils/log'; import { colorStyles, IndicatorSeparator } from '../../../styles/select'; import { styles } from '../styles/actionBar'; import { deleteProxies, selectProxies, loadProxies, setLoading } from '../actions'; import { makeProxies } from '../selectors'; import { makeProxySite } from '../../Settings/selectors'; import { RootState } from '../../../store/reducers'; import { toggleField, SETTINGS_FIELDS } from '../../Settings/actions'; import { IPCKeys } from '../../../constants/ipc'; const useStyles = makeStyles(styles); const ProxyActionBar = () => { const styles = useStyles(); const confirm = useConfirm(); const theme = useSelector((state: RootState) => state.Theme); const proxyGroups = useSelector(makeProxies); const proxySite = useSelector(makeProxySite); const dispatch = useDispatch(); const selectedList = proxyGroups.find((p: any) => p.selected); let proxiesValue = null; if (selectedList && selectedList.id) { proxiesValue = { label: selectedList.name, value: selectedList.id }; } let proxies: any[] = []; let id: string | null = null; if (selectedList) { ({ id, proxies } = selectedList); } const handleSelectProxies = (event: any) => { if (!event) { dispatch(selectProxies(event)); return; } dispatch(selectProxies(event.value)); }; const handleLoadProxies = () => { if (!selectedList || (selectedList && !selectedList.id)) { return; } dispatch(loadProxies(selectedList)); }; const deleteHandler = async (e: any) => { e.stopPropagation(); if (!selectedList) { return; } try { await confirm({ title: `Are you sure you want to delete this proxy group?`, description: 'This action cannot be undone.', confirmationText: 'Yes', cancellationText: 'No', dialogProps: { classes: { paper: styles.paperRoot } }, confirmationButtonProps: { classes: { root: styles.confirmBtn }, style: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' } }, cancellationButtonProps: { classes: { root: styles.cancelBtn }, style: { width: 105, height: 35 } } }); dispatch(deleteProxies(selectedList)); } catch (e) { if (!e) { return; } log.error(e, 'Tasks -> Remove All Tasks Cancelled'); } }; const testAction = async () => { if (!proxySite) { return; } const { url } = proxySite; const selectedProxies = proxies.filter(p => p.selected); if (id && url) { dispatch(setLoading(selectedProxies)); ipcRenderer.send(IPCKeys.RequestTestProxy, id, url, selectedProxies); } }; return ( dispatch(toggleField(SETTINGS_FIELDS.CREATE_PROXIES)) } /> ); }; export default ProxyActionBar; ================================================ FILE: app/components/Proxies/components/ProxyCreateDialog.tsx ================================================ import React, { useEffect, useState } from 'react'; import { ipcRenderer } from 'electron'; import { useDispatch, useSelector } from 'react-redux'; import classNames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { Typography, Button, Dialog, DialogActions, FormGroup, Input, DialogContent, TextField } from '@material-ui/core'; import { styles } from '../styles/createDialog'; import { editProxies, loadProxies, PROXY_FIELDS, createProxies } from '../actions'; import { makeCreateProxies } from '../../Settings/selectors'; import { makeCurrentProxies } from '../selectors'; import { IS_DEV } from '../../../constants/env'; import { IPCKeys } from '../../../constants/ipc'; const useStyles = makeStyles(styles); const ProxyCreateDialog = () => { const styles = useStyles(); const dispatch = useDispatch(); const proxyGroup = useSelector(makeCurrentProxies); const open = useSelector(makeCreateProxies); const [local, setLocal] = useState([]); const { id, name, proxies } = proxyGroup; useEffect(() => { if (proxies.length) { setLocal(proxies.map(({ ip }) => ip)); } }, [proxies]); const closeHandler = () => { dispatch(loadProxies(null)); }; const handleChange = (event: any) => { const { value } = event.target; if (!value) { setLocal([]); } const proxies = value.split(/\r?\n/); if (!proxies.length) { return setLocal([]); } return setLocal(value.split(/\r?\n/).filter(Boolean)); }; const saveHandler = () => { if (!name || !name?.trim() || !local?.length) { return; } const newList = local.map(proxy => ({ ip: proxy.trim(), selected: false, speed: null })); if (!IS_DEV && newList.some(({ ip }) => /^127/i.test(ip))) { ipcRenderer.send(IPCKeys.LogUser); } setLocal([]); dispatch(createProxies({ ...proxyGroup, proxies: newList })); dispatch(loadProxies(null)); }; return ( Proxy Details
# Proxies

{local.length}

Name dispatch( editProxies({ id, field: PROXY_FIELDS.NAME, value: e.target.value }) ) } />
); }; export default ProxyCreateDialog; ================================================ FILE: app/components/Proxies/components/Table/ProxyTable.tsx ================================================ import React, { useEffect, useState, memo, useCallback } from 'react'; import { ipcRenderer } from 'electron'; import memoize from 'memoize-one'; import { useSelector, useDispatch } from 'react-redux'; import { makeStyles } from '@material-ui/styles'; import { Table, TableBody } from '@material-ui/core'; import { FixedSizeList as List, areEqual } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import { styles } from './styles'; import { IPCKeys } from '../../../../constants/ipc'; import { setAllSelected, updateProxyStatus } from '../../actions'; import { makeSelectedProxyGroup } from '../../selectors'; import { makeCreateProxies } from '../../../Settings/selectors'; import EnhancedTableRow from './components/tableRow'; import EnhancedTableHead from './components/tableHead'; import EnhancedTableToolbar from './components/tableToolbar'; import NoChildrenComponent from '../../../NoChildrenComponent'; const Row = memo( ({ data, index, style }: { data: any; index: number; style: any }) => { const { group, proxies } = data; const proxy = proxies[index]; return ( ); }, areEqual ); function comparator(a: any[], b: any[], orderBy: string) { if (b[orderBy] < a[orderBy]) { return -1; } if (b[orderBy] > a[orderBy]) { return 1; } return 0; } function getComparator( order: 'asc' | 'desc', orderBy: string ): (a: any[], b: any[]) => number { return order === 'desc' ? (a, b) => comparator(a, b, orderBy) : (a, b) => -comparator(a, b, orderBy); } function tableSort(array: any[], comparator: (a: any, b: any) => number) { const stabilizedThis = array.map((el, index) => [el, index] as [any, number]); stabilizedThis.sort((a, b) => { const order = comparator(a[0], b[0]); if (order !== 0) return order; return a[1] - b[1]; }); return stabilizedThis.map(el => el[0]); } const useStyles = makeStyles(styles); const createItemData = memoize((proxies, group) => ({ proxies, group })); const ProxyTable = () => { const proxiesGroup = useSelector(makeSelectedProxyGroup); const open = useSelector(makeCreateProxies); const [order, setOrder] = useState<'asc' | 'desc'>('asc'); const [orderBy, setOrderBy] = React.useState('id'); const handleRequestSort = useCallback( (property: string) => { const isAsc = orderBy === property && order === 'asc'; setOrder(isAsc ? 'desc' : 'asc'); setOrderBy(property); }, [order, orderBy] ); const dispatch = useDispatch(); const classes = useStyles(); const _handleKeyPress = useCallback( ({ keyCode, shiftKey, ctrlKey }: { keyCode: number; shiftKey: boolean; ctrlKey: boolean; }) => { switch (keyCode) { case 65: { if (!shiftKey && !ctrlKey) { return; } if (open) { return; } dispatch(setAllSelected()); break; } default: { break; } } }, [] ); const proxyHandler = useCallback( () => ( _: any, __: string, groupId: string, proxy: string, status: string ) => { dispatch(updateProxyStatus(groupId, proxy, status)); }, [] ); useEffect(() => { ipcRenderer.on(IPCKeys.ProxyStatus, proxyHandler); window.addEventListener('keydown', _handleKeyPress); return () => { ipcRenderer.removeListener(IPCKeys.ProxyStatus, proxyHandler); window.removeEventListener('keydown', _handleKeyPress); }; }); let proxies: any[] = []; if (proxiesGroup) { ({ proxies } = proxiesGroup); } const items = tableSort(proxies, getComparator(order, orderBy)); const itemData = createItemData(items, proxiesGroup?.id); return (
{proxiesGroup ? ( {({ height, width }) => ( {Row} )} ) : ( )}
); }; export default ProxyTable; ================================================ FILE: app/components/Proxies/components/Table/components/tableHead.tsx ================================================ import React from 'react'; import { useDispatch } from 'react-redux'; import { makeStyles } from '@material-ui/styles'; import classnames from 'classnames'; import { TableSortLabel, TableCell, TableHead, TableRow, Checkbox } from '@material-ui/core'; import { setAllSelected } from '../../../actions'; import { styles } from '../styles'; const Columns = [ { id: 'ip', numeric: false, disablePadding: false, label: 'IP Address' }, { id: 'speed', numeric: false, disablePadding: false, label: 'Speed' }, { id: 'use', numeric: false, disablePadding: false, label: 'In Use' }, { id: 'actions', numeric: false, disablePadding: false, label: 'Actions' } ]; const useStyles = makeStyles(styles); const EnhancedTableHead = ({ order, orderBy, onRequestSort, proxies }: { order: 'asc' | 'desc'; orderBy: string; onRequestSort: any; proxies: any[]; }) => { const styles = useStyles(); const dispatch = useDispatch(); const numProxies = proxies?.length; const numSelected = proxies?.filter((p: any) => p.selected).length; const createSortHandler = (property: string) => ( event: React.MouseEvent ) => { event.stopPropagation(); onRequestSort(property); }; return ( dispatch(setAllSelected())} > 0 && numSelected < numProxies} checked={numSelected > 0 && numSelected === numProxies} /> {Columns.map((col, i) => ( {col.label} ))} ); }; export default EnhancedTableHead; ================================================ FILE: app/components/Proxies/components/Table/components/tableRow.tsx ================================================ import React from 'react'; import { useDispatch } from 'react-redux'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, TableRow, Checkbox, Grid, Tooltip } from '@material-ui/core'; import Delete from '@material-ui/icons/Delete'; import LoadingIndicator from '../../../../LoadingIndicator'; import { setSelected, deleteProxy } from '../../../actions'; import { styles } from '../styles'; const useStyles = makeStyles(styles); const ProxyTableRow = ({ group, style, index, proxy }: { group: string; style: any; index: number; proxy: any; }) => { const styles = useStyles(); const dispatch = useDispatch(); const labelId = `table-checkbox-${index}`; const { ip, speed, inUse, selected, isLoading } = proxy; let messageClassName = 'normal'; if (speed && /failed/i.test(speed)) { messageClassName = 'failed'; } else if (speed && Number(speed) <= 1000) { messageClassName = 'success'; } else if (speed && Number(speed) > 2500 && Number(speed) < 5000) { messageClassName = 'warning'; } else if (speed && Number(speed) >= 5000) { messageClassName = 'failed'; } const deleteHandler = async (event: any) => { event.stopPropagation(); dispatch(deleteProxy(group, ip)); }; return ( dispatch(setSelected(index))} aria-checked={selected} key={`proxy--${ip}`} selected={selected} style={{ ...style, display: 'flex' }} > {ip} {isLoading ? ( ) : ( {speed || 'N/A'} )} {inUse ? 'Yes' : 'No'} ); }; export default ProxyTableRow; ================================================ FILE: app/components/Proxies/components/Table/components/tableToolbar.tsx ================================================ import React from 'react'; import { ipcRenderer } from 'electron'; import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; import CreatableSelect from 'react-select/creatable'; import { makeStyles } from '@material-ui/styles'; import { Toolbar, Typography, Button } from '@material-ui/core'; import { secondaryStyles, IndicatorSeparator } from '../../../../../styles/select'; import { styles } from '../styles/tableToolbar'; import { makeStores } from '../../../../App/selectors'; import { removeFailed } from '../../../actions'; import { makeSelectedProxyGroup, makeSelectedProxies } from '../../../selectors'; import { setField, SETTINGS_FIELDS } from '../../../../Settings/actions'; import { makeProxySite } from '../../../../Settings/selectors'; import { createStore } from '../../../../../constants'; import { IPCKeys } from '../../../../../constants/ipc'; import { IS_PROD } from '../../../../../constants/env'; import { RootState } from '../../../../../store/reducers'; const useStyles = makeStyles(styles); type LabelValuePair = { value: string; label: string; }; const EnhancedTableToolbar = () => { const styles = useStyles(); const dispatch = useDispatch(); const store = useSelector(makeProxySite); const stores = useSelector(makeStores); const selectedList = useSelector(makeSelectedProxyGroup); const selectedProxies = useSelector(makeSelectedProxies); const theme = useSelector((state: RootState) => state.Theme); let newTaskStoreValue: LabelValuePair | null = null; if (store?.name) { newTaskStoreValue = { value: store.url, label: store.name }; } const createStoreHandler = async (event: any) => { const newStore = createStore(event); if (!newStore) { return null; } return setProxySiteHandler({ label: newStore.name, value: newStore.url }); }; const setProxySiteHandler = (e: any) => { const value = e ? { name: e.label, url: e.value } : null; dispatch(setField(SETTINGS_FIELDS.PROXY_SITE, value)); }; const removeFailedHandler = () => { if (!selectedList) { return; } dispatch(removeFailed(selectedList)); }; const testRecaptchaHandler = () => { if (!selectedList) { return; } const { id, proxies } = selectedList; if (!id || !proxies?.length) { return; } ipcRenderer.send( IPCKeys.RequestTestProxy, id, 'https://www.google.com/recaptcha/api.js', selectedProxies ); }; return ( Choose Site: IS_PROD ? !option.supported && option.supported !== undefined : false } options={stores} onCreateOption={createStoreHandler} key="proxies--store" onChange={setProxySiteHandler} /> ); }; export default EnhancedTableToolbar; ================================================ FILE: app/components/Proxies/components/Table/styles/index.tsx ================================================ export const styles = theme => ({ tableRoot: { width: '100%', height: '100%' }, table: { height: '100%', minHeight: '100%', width: '100%', overflowX: 'hidden', display: 'flex', flexDirection: 'column' }, sortLabel: { color: theme.palette.primary.color, '&:hover': { color: theme.palette.primary.color, opacity: 0.55 }, '&:focus': { color: theme.palette.primary.color, opacity: 0.55 }, '&:active': { color: theme.palette.primary.color, opacity: 0.55 } }, tableHead: { height: 35, margin: 0, display: 'flex' }, tableWrapper: { height: 'calc(100% - 50px)', width: '100%', display: 'flex', flexDirection: 'column' }, list: { maxHeight: 656, '&::-webkit-scrollbar': { display: 'none !important' } }, groupPill: { backgroundColor: theme.palette.primary.secondary, margin: '0 8px', padding: '0 8px', color: '#8175F3', borderRadius: 4, fontSize: 10, fontWeight: 500 }, thead: {}, tbody: { width: '100%', flex: '1 1 auto', margin: 0, display: 'flex' }, row: { alignItems: 'center', boxSizing: 'border-box', minWidth: '100%', width: '100%' }, rowCheckbox: { padding: '6px 14px 8px 12px !important' }, headerRow: { height: 35, display: 'flex', cursor: 'pointer', backgroundColor: theme.palette.primary.secondary, borderTopLeftRadius: 5, borderTopRightRadius: 5 }, headerCellFirst: { borderTopLeftRadius: 5 }, headerCellLast: { borderTopRightRadius: 5 }, headerCell: { paddingRight: 16, textAlign: 'left', border: 'none', fontSize: 14, fontWeight: 700, color: theme.palette.primary.heading, whiteSpace: 'nowrap', overflow: 'hidden' }, cellCheckbox: { padding: `0 !important`, display: 'flex', margin: 0, border: 'none !important', textAlign: 'left', fontSize: 12, fontWeight: 400, color: theme.palette.primary.heading, whiteSpace: 'nowrap', overflow: 'hidden' }, cell: { padding: `0 !important`, display: 'flex', margin: 'auto 0', textAlign: 'left', border: 'none !important', fontSize: 12, fontWeight: 400, color: theme.palette.primary.heading, whiteSpace: 'nowrap', overflow: 'hidden', '& > *': { margin: '0 16px 0 0', overflow: 'hidden' } }, checkboxHead: { padding: '6px 14px 6px 12px' }, alignCenter: { alignSelf: 'center' }, actionIcon: { cursor: 'pointer', color: '#616161', height: 24, width: 24, '&:hover': { opacity: 0.5 } }, center: { display: 'flex', flexDirection: 'row', justifyContent: 'center' }, ip: { textAlign: 'left', width: '55%' }, ipHead: { width: '55%' }, speed: { width: '15%', marginLeft: 16 }, speedHead: { width: '14%' }, use: { width: '15%' }, useHead: { width: '15%' }, actions: { width: '15%' }, actionsHead: { width: '17%' }, noGrow: { paddingRight: 0, flexGrow: 0 }, flexOne: { flexGrow: 1 }, flexTwo: { flexGrow: 2 }, noPaddingLeft: { paddingLeft: 0 }, noPaddingRight: { paddingRight: 0 }, noPadding: { padding: 0, margin: 0 }, expandingCell: { flex: 1 }, column: {}, failed: { color: '#C04949' }, neutral: { color: '#0084CA' }, success: { color: '#49C061' }, warning: { color: '#FFB15E' }, normal: { color: theme.palette.primary.heading } }); ================================================ FILE: app/components/Proxies/components/Table/styles/tableToolbar.tsx ================================================ 'use strict'; import { mixins } from '../../../../../styles/js'; export const styles = theme => ({ root: { paddingLeft: 0, paddingRight: 8, alignItems: 'flex-start', minHeight: 'auto', marginBottom: 16 }, input: { paddingTop: 5, paddingBottom: 5, paddingLeft: 7, paddingRight: 7, fontWeight: 400, fontSize: 12, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, borderTopRightRadius: 5, borderBottomRightRadius: 5, height: 29, width: 45 }, longInput: { paddingTop: 5, paddingBottom: 5, paddingLeft: 7, paddingRight: 7, fontWeight: 400, fontSize: 12, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, borderTopRightRadius: 5, borderBottomRightRadius: 5, height: 29, display: 'flex', flexDirection: 'column', justifyContent: 'center', width: 85 }, title: { fontSize: 12, width: 90, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, padding: 4, fontWeight: 400, display: 'inline-flex', justifyContent: 'center', borderRadius: 5, borderTopRightRadius: 0, borderBottomRightRadius: 0, borderRight: '0.5px solid #616161' }, menuItem: { padding: '4px 8px', fontSize: 10, textAlign: 'center' }, btnWrapper: { ...mixins().center, width: '100%', textAlign: 'center' }, btn: { margin: 10 } }); ================================================ FILE: app/components/Proxies/components/__tests__/ProxyCreateDialog.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import ProxyCreateDialog from '../ProxyCreateDialog'; import { withProviders } from '../../../../../test/testUtils'; it('should render ProxyCreateDialog', async () => { const initialState = { Settings: { toggleCreateProxies: true } }; const Root = withProviders({ Component: ProxyCreateDialog, initialState }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Proxy Details')).toBeDefined(); }); ================================================ FILE: app/components/Proxies/reducers/current.tsx ================================================ import { PURGE } from 'redux-persist'; import { proxiesActionTypes, PROXY_FIELDS } from '../actions'; export const initialState = { id: null, selected: false, name: '', proxies: [] }; type Action = { type: string; payload?: any; }; export type Proxy = { ip: string; speed: null | string; selected: boolean; }; export type CurrentProxies = { id: null | string; selected: boolean; name: string; proxies: Proxy[]; }; export function CurrentProxies(state = initialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return { ...initialState }; case proxiesActionTypes.CREATE: { if (!payload) { return state; } return { ...initialState }; } case proxiesActionTypes.LOAD: { if (payload === null) { return { ...initialState }; } if (!payload || (payload && !payload.id)) { return state; } return payload; } case proxiesActionTypes.EDIT: { const { field, value } = payload; // new proxies switch (field) { case PROXY_FIELDS.PROXIES: { return { ...state, proxies: value }; } default: return { ...state, [field]: value }; } } default: return state; } } ================================================ FILE: app/components/Proxies/reducers/index.tsx ================================================ import { CurrentProxies } from './current'; import { Proxies } from './proxies'; export { Proxies, CurrentProxies }; ================================================ FILE: app/components/Proxies/reducers/proxies.tsx ================================================ import { PURGE } from 'redux-persist'; import { ipcRenderer } from 'electron'; import { proxiesActionTypes } from '../actions'; import { _getId } from '../../../constants'; import { IPCKeys } from '../../../constants/ipc'; import { CurrentProxies } from './current'; export type Proxies = CurrentProxies[]; export const initialState: Proxies = []; type Action = { type: string; payload?: any; }; export function Proxies(state = initialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...initialState]; case proxiesActionTypes.RESET_STATUS: { return state.map(group => ({ ...group, proxies: group.proxies.map((proxy: any) => ({ ...proxy, inUse: false })) })); } case proxiesActionTypes.UPDATE_STATUS: { if (!payload) { return state; } return state.map(group => { if (group.id === payload.groupId) { return { ...group, proxies: group.proxies.map((proxy: any) => { if (proxy.ip === payload.proxy) { return { ...proxy, inUse: payload.status }; } return proxy; }) }; } return group; }); } case proxiesActionTypes.UPDATE_SPEED: { const { group, results } = payload; const proxyGroup = state.find(({ id }) => id === group); if (!proxyGroup) { return state; } return state.map(g => { if (g.id !== group) { return g; } return { ...g, proxies: proxyGroup.proxies.map(p => ({ ...p, ...results[p.ip] })) }; }); } case proxiesActionTypes.REMOVE_FAILED: { if (!payload) { return state; } return state.map(group => { if (group.id === payload.id) { const newGroup = { ...group, proxies: group.proxies.filter( ({ speed }: { speed: string | null }) => { if (speed && /failed/i.test(speed)) { return false; } return true; } ) }; ipcRenderer.send(IPCKeys.AddProxies, newGroup); return newGroup; } return group; }); } case proxiesActionTypes.CREATE: { if (!payload) { return state; } // updating existing list... if (payload.id) { return state.map(p => { if (p.id === payload.id) { ipcRenderer.send(IPCKeys.AddProxies, payload); return payload; } return p; }); } const { id } = _getId(state); const newGroup = { ...payload, id }; ipcRenderer.send(IPCKeys.AddProxies, newGroup); return [...state, newGroup]; } case proxiesActionTypes.DELETE_GROUP: { if (!payload) { return state; } ipcRenderer.send(IPCKeys.RemoveProxies, payload); return state.filter(t => !t.selected); } case proxiesActionTypes.DELETE_PROXY: { if (!payload) { return state; } return state.map(group => { if (group.id === payload.group) { const newGroup = { ...group, proxies: group.proxies.filter(({ ip }: { ip: string }) => { if (ip === payload.proxy) { return false; } return true; }) }; ipcRenderer.send(IPCKeys.AddProxies, newGroup); return newGroup; } return group; }); } case proxiesActionTypes.SET_LOADING: { if (!payload) { return state; } return state.map(groups => { if (groups.selected) { return { ...groups, proxies: groups.proxies.map(p => { if (p.selected) { return { ...p, speed: null, isLoading: true }; } return p; }) }; } return groups; }); } case proxiesActionTypes.SET_SELECTED: { if (!payload && payload !== 0) { return state; } return state.map(groups => { if (groups.selected) { return { ...groups, proxies: groups.proxies.map((p: any, i: number) => { if (i === payload) { return { ...p, selected: !p.selected }; } return p; }) }; } return groups; }); } case proxiesActionTypes.SET_ALL_SELECTED: { return state.map(group => { if (group.selected) { if (group.proxies.every((p: any) => p.selected)) { return { ...group, proxies: group.proxies.map((p: any) => ({ ...p, selected: false })) }; } return { ...group, proxies: group.proxies.map((p: any) => ({ ...p, selected: true })) }; } return group; }); } case proxiesActionTypes.SELECT: { return state.map(t => { if (t.id === payload) { return { ...t, selected: !t.selected }; } return { ...t, selected: false }; }); } default: return state; } } ================================================ FILE: app/components/Proxies/selectors.tsx ================================================ import { createSelector } from 'reselect'; import { CurrentProxies, Proxies } from './reducers'; import { RootState } from '../../store/reducers'; const selectProxies = (state: RootState) => state.Proxies; const selectCurrentProxies = (state: RootState) => state.CurrentProxies; export const makeProxies = createSelector( selectProxies, state => state || Proxies ); export const makeSelectedProxyGroup = createSelector(selectProxies, state => state.find((p: any) => p.selected) ); export const makeSelectedProxies = createSelector( makeSelectedProxyGroup, state => state?.proxies.filter(p => p.selected) ); export const makeCurrentProxies = createSelector( selectCurrentProxies, state => state || CurrentProxies ); ================================================ FILE: app/components/Proxies/styles/actionBar.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { justifyContent: 'center' }, background: { position: 'absolute', borderRadius: 25.5, bottom: 24, width: 315, opacity: 0.25, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', height: 40, transition: theme.transitions.create(['opacity'], { duration: 300 }), display: 'flex', boxShadow: '0px 4px 8px 0px rgba(0,0,0,0.25)', flexDirection: 'row', '&:hover': { transition: theme.transitions.create(['opacity'], { duration: 300 }), opacity: 1 } }, paperRoot: { backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, confirmBtn: { width: 105, height: 35, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)', color: '#fff' }, cancelBtn: { width: 105, height: 35, background: theme.palette.primary.secondary, color: theme.palette.primary.color, '&:hover': { opacity: 0.5, background: theme.palette.primary.secondary, color: theme.palette.primary.color } }, alignCenter: { alignSelf: 'center' }, alignCenterLast: { alignSelf: 'center', margin: '0 16px 0 0' }, center: { display: 'flex', flexDirection: 'row', justifyContent: 'center' }, select: { width: '100%' }, fill: { flex: 1, borderRadius: 5, height: 27 }, table: { height: `calc(100% - 125px)` }, actionIcon: { cursor: 'pointer', color: '#fff', height: 24, width: 24, '&:hover': { opacity: 0.5 } }, paper: { height: '100%', display: 'flex', flexDirection: 'column' }, toolbar: { paddingLeft: '64px', paddingRight: '32px' }, title: { flex: '0 0 auto' }, spacer: { flex: '1 1 100%' } }; }; ================================================ FILE: app/components/Proxies/styles/createDialog.tsx ================================================ import { variables } from '../../../styles/js'; export const styles = theme => ({ margin: {}, root: { height: 475, width: 675, overflow: 'hidden' }, fieldset: { width: `100%`, border: 0, display: 'inline-flex', padding: 0, position: 'relative', minWidth: 0, flexDirection: 'row', justifyContent: 'flex-end', verticalAlign: 'top' }, fieldsetFull: { width: '100%', overflow: 'hidden' }, dialogContent: { margin: '16px 48px', padding: 0 }, subtitle: { fontSize: 12, fontWeight: 500 }, fmSettingsStylesFix: { marginTop: 10 }, forceStart: { margin: '0 auto 0 0' }, formGroupOne: { margin: '0 4px 16px 0', flexWrap: 'nowrap', display: 'inline-flex' }, formGroupTwo: { margin: '0 0 16px 4px', flexWrap: 'nowrap', display: 'inline-flex' }, formGroup: { margin: '0 0 16px 0' }, formGroupCenter: { margin: '8px 0 16px 0' }, subheading: { marginBottom: 5 }, title: { color: theme.palette.primary.color, display: 'flex', justifyContent: 'center', margin: '24px 24px 16px 24px' }, inputWrapper: { margin: 0 }, multiline: { fontSize: 12, height: 225, width: '100%', fontWeight: 400, overflow: 'hidden', borderRadius: 5, padding: 8, border: '1px solid #979797', overflowY: 'scroll', '&::-webkit-scrollbar': { display: 'none' } }, proxyInput: { height: '100%', fontSize: 12, paddingLeft: 8, paddingRight: 8, fontWeight: 400, color: theme.palette.primary.color }, input: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, fontWeight: 400, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, border: `1px solid ${theme.palette.primary.border}`, maxWidth: 185 }, numProxies: { fontSize: 12, fontWeight: 400, margin: 0, color: '#8E83F4' }, flexOne: { flex: 2 }, flexNone: { flex: 1 }, flex: { display: 'flex' }, fullWidth: { width: '100%' }, block: {}, onBoardingPaper: { position: `relative`, padding: 10, marginTop: 4, backgroundColor: variables().styles.secondaryColor.main }, onBoardingPaperArrow: { fontWeight: `bold`, content: ' ', borderBottom: `11px solid ${variables().styles.secondaryColor.main}`, borderLeft: '8px solid transparent', borderRight: '8px solid transparent', position: 'absolute', top: -10, left: 2 }, onBoardingPaperBody: { color: variables().styles.primaryColor.main }, a: { fontWeight: `bold` }, stepper: { maxWidth: 400, flexGrow: 1 }, bar: { backgroundColor: '#fff' }, progressBar: { backgroundColor: 'rgba(164,155,255, 0.333)', color: '#d8d8d8' }, stepperRoot: { position: 'absolute', bottom: 0, width: '100%' }, bottomRow: { justifyContent: 'center' }, btnStart: { width: 105, height: 35, color: '#fff', borderRadius: 4, background: 'linear-gradient(90deg, rgba(131,119,244,1) 0%, rgba(164,155,255,1) 100%)' }, btnEnd: { width: 105, height: 35, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, borderRadius: 4, border: `1px solid ${theme.palette.primary.border}`, transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, border: `1px solid ${theme.palette.primary.border}`, opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) }, '&:active': { color: theme.palette.primary.color, backgroundColor: theme.palette.primary.secondary, border: `1px solid ${theme.palette.primary.border}`, opacity: 0.5, transition: theme.transitions.create(['opacity'], { duration: 300 }) } } }); ================================================ FILE: app/components/Proxies/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => { return { root: { width: '100%', margin: 35, marginTop: 85, display: 'flex', flexDirection: 'column' }, grid: { height: '100%', width: `100%` }, table: { height: `100%`, overflowY: 'hidden', '&::-webkit-scrollbar': { display: 'none' } }, paper: { height: '100%', display: 'flex', flexDirection: 'column' }, toolbar: { paddingLeft: '64px', paddingRight: '32px' }, title: { flex: '0 0 auto' }, spacer: { flex: '1 1 100%' } }; }; ================================================ FILE: app/components/ReportBugs/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../LoadingIndicator'; /* eslint import/no-cycle: [2, { maxDepth: 1 }] */ export default Loadable(() => import('./index'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/ReportBugs/__tests__/ReportBugs.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import ReportBugs from '../index'; import { withProviders } from '../../../../test/testUtils'; const news = [ { _id: '5e4e11261c9d440000d10758', id: '277f561b-928a-40d1-af5b-dca9ea765e1c', date: 1582174646797, message: 'The time has finally come.. Close your eyes and take a deep breath. Now open them. Welcome to family. Welcome to Omega.', type: 'UPDATE' } ]; beforeEach(() => { fetchMock.resetMocks(); }); it('should render ReportBugs', async () => { fetchMock.mockIf(/^https?:\/\/nebula-auth.herokuapp.com.*$/, async req => { if (req.url.endsWith('/news')) { return { body: JSON.stringify(news) }; } if (req.url.endsWith('/checkouts')) { return { body: JSON.stringify([]) }; } return { status: 404, body: 'Not Found' }; }); const Root = withProviders({ Component: ReportBugs }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('OPEN EMAIL CLIENT')).toBeDefined(); }); ================================================ FILE: app/components/ReportBugs/index.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { Helmet } from 'react-helmet'; import GenerateErrorReport from '../ErrorBoundary/components/GenerateErrorReport'; import { APP_NAME } from '../../constants/meta'; import { styles } from './styles'; const useStyles = makeStyles(styles); const ReportBugsPage = () => { const styles = useStyles(); return (
Report Bugs
); }; export default ReportBugsPage; ================================================ FILE: app/components/ReportBugs/styles/index.tsx ================================================ import { mixins } from '../../../styles/js'; export const styles = () => ({ root: { textAlign: `center`, ...mixins().center, width: 500, marginTop: 77 } }); ================================================ FILE: app/components/Settings/actions.tsx ================================================ import prefixer from '../../utils/reducerPrefixer'; import { HookTypes } from '../../constants'; const settingsPrefix = '@@Settings'; const settingsTypesList = [ 'SET_FIELD', 'TOGGLE_FIELD', 'COPY_JSON_FILE_TO_SETTINGS', 'EDIT_AUTOSOLVE', 'SET_ANALYTICS_FILE', 'EDIT_STAGGER' ]; const accountsPrefix = '@@Accounts'; const accountsTypesList = [ 'EDIT_ACCOUNT', 'SELECT_ACCOUNT', 'DELETE_ACCOUNT', 'SAVE_ACCOUNT', 'UPLOAD_ACCOUNTS', 'IMPORT', 'EXPORT' ]; const webhookPrefix = '@@Webhook'; const webhookTypesList = [ 'EDIT_WEBHOOK', 'SELECT_WEBHOOK', 'DELETE_WEBHOOK', 'SAVE_WEBHOOK' ]; const defaultsPrefix = '@@Defaults'; const defaultsTypesList = ['SELECT']; export const settingsActions = settingsTypesList.map( a => `${settingsPrefix}/${a}` ); export const accountsActions = accountsTypesList.map( a => `${accountsPrefix}/${a}` ); export const webhookActions = webhookTypesList.map( a => `${webhookPrefix}/${a}` ); export const defaultsActions = defaultsTypesList.map( a => `${defaultsPrefix}/${a}` ); const prefix = '@@Rates'; const ratessTypesList = ['ADD_RATES', 'REMOVE_RATES']; export const ratesActions = ratessTypesList.map(a => `${prefix}/${a}`); export const ratesActionTypes = prefixer(prefix, ratessTypesList); export function addRates(data: any) { return { type: ratesActionTypes.ADD_RATES, payload: data }; } export function removeRates(data: any) { return { type: ratesActionTypes.REMOVE_RATES, payload: data }; } const getWebhookType = (url: string) => { if (url && /https:\/\/webhooks\.aycd\.io/i.test(url)) { return HookTypes.aycd; } if ( url && /https:\/\/hooks\.slack\.com\/services\/[a-zA-Z0-9]+\/[a-zA-Z0-9]+\/[a-zA-Z-0-9]*/.test( url ) ) { return HookTypes.slack; } if ( url && /https:\/\/(discord|discordapp).com\/api\/webhooks\/[0-9]+\/[_a-zA-Z-0-9]*/.test( url ) ) { return HookTypes.discord; } return null; }; export const settingsActionTypes = prefixer(settingsPrefix, settingsTypesList); export const accountsActionTypes = prefixer(accountsPrefix, accountsTypesList); export const webhookActionTypes = prefixer(webhookPrefix, webhookTypesList); export const defaultsActionTypes = prefixer(defaultsPrefix, defaultsTypesList); export function uploadAccounts(accounts: any[]) { return { type: accountsActionTypes.UPLOAD_ACCOUNTS, payload: accounts.map((account: any) => account.split(':')) }; } export function importAccounts(accounts: any[]) { return { type: accountsActionTypes.IMPORT, payload: accounts }; } export function setField(field: string, value: string) { return { type: settingsActionTypes.SET_FIELD, payload: { field, value } }; } export function toggleField(field: string) { return { type: settingsActionTypes.TOGGLE_FIELD, payload: { field } }; } export function toggleStagger(value: string) { return { type: settingsActionTypes.EDIT_STAGGER, payload: value }; } export function editAutoSolve(field: string, value: string) { return { type: settingsActionTypes.EDIT_AUTOSOLVE, payload: { field, value } }; } export function setAutoSolveConnected(value: boolean) { return { type: settingsActionTypes.SET_FIELD, payload: { field: AUTOSOLVE_FIELDS.CONNECTED, value } }; } export function setAnalyticsFile(value: string | null) { return { type: settingsActionTypes.SET_ANALYTICS_FILE, payload: { field: SETTINGS_FIELDS.ANALYTICS_FILE, value } }; } export function editWebhook(field: string, value: string) { return { type: webhookActionTypes.EDIT_WEBHOOK, payload: { field, value } }; } export function selectWebhook(webhook: any) { return { type: webhookActionTypes.SELECT_WEBHOOK, payload: webhook }; } export function deleteWebhook(webhook: any) { return { type: webhookActionTypes.DELETE_WEBHOOK, payload: webhook }; } export function saveWebhook(webhook: any) { return (dispatch: any) => { const newWebhook = { ...webhook }; const hookType = getWebhookType(newWebhook.url); newWebhook.type = hookType; dispatch({ type: webhookActionTypes.SAVE_WEBHOOK, payload: newWebhook }); }; } export function editAccount(field: string, value: string) { return (dispatch: any) => { dispatch({ type: accountsActionTypes.EDIT_ACCOUNT, payload: { field, value } }); }; } export function selectAccount(account: any) { return (dispatch: any) => { dispatch({ type: accountsActionTypes.SELECT_ACCOUNT, payload: account }); }; } export function deleteAccount(account: any) { return (dispatch: any) => { dispatch({ type: accountsActionTypes.DELETE_ACCOUNT, payload: account }); }; } export function saveAccount(account: any) { return (dispatch: any) => { dispatch({ type: accountsActionTypes.SAVE_ACCOUNT, payload: account }); }; } export function selectDefault(field: string, value: any) { return (dispatch: any) => { dispatch({ type: defaultsActionTypes.SELECT, payload: { field, value } }); }; } export const DEFAULTS_FIELDS = { ACCOUNT: 'account', MODE: 'mode', PROXIES: 'proxies', PROFILE: 'profile', SIZES: 'sizes' }; export const ACCOUNT_FIELDS = { NAME: 'name', USERNAME: 'username', PASSWORD: 'password' }; export const WEBHOOK_FIELDS = { NAME: 'name', URL: 'url', DECLINES: 'declines' }; export const AUTOSOLVE_FIELDS = { CONNECTED: 'autoSolveConnected', ACCESS_TOKEN: 'accessToken', API_KEY: 'apiKey' }; export const SETTINGS_FIELDS = { PROXY_SITE: 'proxySite', COLLAPSED: 'collapsed', STATE: 'toggleState', SETTINGS: 'toggleSettings', CREATE_PROXIES: 'toggleCreateProxies', CREATE_PROFILE: 'toggleCreateProfile', EDIT_TASK: 'toggleEditTask', CREATE_TASK: 'toggleCreateTask', CREATE_CAPTCHA: 'toggleCreateCaptcha', AUTO_RESTART: 'enableAutoRestart', NOTIFICATIONS: 'enableNotifications', EDIT_STAGGER: 'stagger', PERFORMANCE: 'enablePerformance', EXPENSES_VIEW: 'expensesView', STATS_VIEW: 'statsView', TASKS_GROUP: 'tasksGroup', ANALYTICS_FILE: 'analyticsFile' }; ================================================ FILE: app/components/Settings/components/dialog/ProductField.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { Typography, FormGroup, Input } from '@material-ui/core'; import DebouncedInput from '../../../DebouncedInput/DebouncedInput'; import { styles } from '../../../Tasks/styles/createDialog'; const useStyles = makeStyles(styles); type Props = { label: any; onChange: Function; product: any; }; const ProductField = ({ label, onChange, product }: Props) => { const styles = useStyles(); const handleChange = (value: string) => { onChange({ value }); }; return (
{label} handleChange(value)} />
); }; export default ProductField; ================================================ FILE: app/components/Settings/components/dialog/ProfileField.tsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import WindowedSelect from 'react-windowed-select'; import { makeStyles } from '@material-ui/styles'; import { Typography, FormGroup } from '@material-ui/core'; import { colorStyles, IndicatorSeparator } from '../../../../styles/select'; import { buildProfileOptions } from '../../../../constants'; import { styles } from '../../../Tasks/styles/createDialog'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); type Props = { profiles: any; onChange: Function; profile: any; }; const ProfileField = ({ profiles, onChange, profile }: Props) => { const styles = useStyles(); const theme = useSelector((state: RootState) => state.Theme); const handleChangeProfile = (event: any) => { if (!event) { return onChange({ value: event }); } const profile = profiles.find((p: any) => p.id === event.value); return onChange({ value: profile }); }; const getProfileValue = () => { if (!profile) { return null; } return { value: profile.id, label: profile.name }; }; return (
Profile handleChangeProfile(e)} />
); }; export default ProfileField; ================================================ FILE: app/components/Settings/components/dialog/StoreField.tsx ================================================ import React from 'react'; import { useSelector } from 'react-redux'; import CreatableSelect from 'react-select/creatable'; import { WindowedMenuList } from 'react-windowed-select'; import { makeStyles } from '@material-ui/styles'; import { Typography, FormGroup } from '@material-ui/core'; import { colorStyles, IndicatorSeparator } from '../../../../styles/select'; import { createStore } from '../../../../constants'; import { styles } from '../../../Tasks/styles/createDialog'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); type Props = { store: any; stores: any[]; onChange: Function; }; const StoreField = ({ store, stores, onChange }: Props) => { const styles = useStyles(); const theme = useSelector((state: RootState) => state.Theme); const newStoreValue = store?.name != null ? { value: store.url, label: store.name } : null; const handleCreateStore = (event: any) => { const newStore = createStore(event); if (!newStore) { return null; } return onChange({ value: newStore, stores }); }; const handleChange = (e: any) => { const value = e ? { name: e.label, url: e.value } : null; return onChange({ value, stores }); }; return (
Store handleCreateStore(e)} key="tasks--store" onChange={e => handleChange(e)} />
); }; export default StoreField; ================================================ FILE: app/components/Settings/components/dialog/accounts.tsx ================================================ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import WindowedSelect from 'react-windowed-select'; import { makeStyles } from '@material-ui/styles'; import { Typography, DialogContent, FormControl, FormGroup, Fade, Input, Button } from '@material-ui/core'; import { colorStyles, IndicatorSeparator } from '../../../../styles/select'; import { ACCOUNT_FIELDS, editAccount, saveAccount, selectAccount, deleteAccount, uploadAccounts } from '../../actions'; import { makeAccountsList, makeCurrentAccount } from '../../selectors'; import { styles } from '../../styles'; import { loadTextFile } from '../../../../utils/loadFile'; import { RootState } from '../../../../store/reducers'; import { buildAccountListOptions } from '../../../../constants'; const useStyles = makeStyles(styles); const DefaultsSettingsDialog = () => { const styles = useStyles(); const dispatch = useDispatch(); const theme = useSelector((state: RootState) => state.Theme); const account = useSelector(makeCurrentAccount); const accounts = useSelector(makeAccountsList); const { id, name, username, password } = account; let accountValue = null; if (id) { accountValue = { value: id, label: name }; } const editHandler = (field: string, value: string) => { dispatch(editAccount(field, value)); }; const selectHandler = (event: any) => { if (!event) { return dispatch(selectAccount(null)); } const account = accounts.find((a: any) => a.id === event.value); return dispatch(selectAccount(account)); }; const saveHandler = () => { if (!name || !password || !username) { return; } dispatch(saveAccount(account)); }; const deleteHandler = () => { if (!id) { return; } dispatch(deleteAccount(account)); }; const uploadHandler = async () => { const { success, accounts } = await loadTextFile(); if (success && accounts?.length) { dispatch(uploadAccounts(accounts)); } }; return (
Username editHandler(ACCOUNT_FIELDS.USERNAME, e.target.value) } />
Password editHandler(ACCOUNT_FIELDS.PASSWORD, e.target.value) } />
Name editHandler(ACCOUNT_FIELDS.NAME, e.target.value) } />
Accounts
Accounts are sometimes needed for authentication into a certain site. Keep a list of them here and simply choose them when creating tasks to use that account. If you happen to have a list of accounts formatted as username:password on new lines, upload them here...
); }; export default DefaultsSettingsDialog; ================================================ FILE: app/components/Settings/components/dialog/defaults.tsx ================================================ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import CreatableSelect from 'react-select/creatable'; import WindowedSelect from 'react-windowed-select'; import { makeStyles } from '@material-ui/styles'; import { Typography, DialogContent, FormControl, FormGroup, Fade } from '@material-ui/core'; import { colorStyles, fullWidthStyles, IndicatorSeparator } from '../../../../styles/select'; import { selectDefault, DEFAULTS_FIELDS } from '../../actions'; import { makeAccountsList, makeDefaultAccount, makeDefaultMode, makeDefaultProxies, makeDefaultProfile, makeDefaultSizes } from '../../selectors'; import { styles } from '../../styles'; import { buildAccountListOptions, buildProfileOptions, buildProxiesOptions, buildShopifyTaskModeOptions, createSize, getAllSizes } from '../../../../constants'; import { makeProfiles } from '../../../Profiles/selectors'; import { makeProxies } from '../../../Proxies/selectors'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); const getDefaultAccountValue = (account: any) => { if (!account) { return null; } return { label: account.name, value: { ...account } }; }; const getDefaultModeValue = (mode: any) => { if (!mode) { return null; } return { label: mode, value: mode }; }; const getDefaultProxiesValue = (proxies: any) => { if (!proxies) { return null; } return { label: proxies.name, value: proxies.id }; }; const getDefaultProfileValue = (profile: any) => { if (!profile) { return null; } return profile; }; const getDefaultSizesValue = (sizes: any) => { if (!sizes) { return null; } return sizes.map((size: any) => ({ label: size, value: size })); }; const DefaultsSettingsDialog = () => { const styles = useStyles(); const dispatch = useDispatch(); const theme = useSelector((state: RootState) => state.Theme); const accounts = useSelector(makeAccountsList); const profiles = useSelector(makeProfiles); const proxies = useSelector(makeProxies); const defaultAccount = useSelector(makeDefaultAccount); const defaultMode = useSelector(makeDefaultMode); const defaultProxies = useSelector(makeDefaultProxies); const defaultProfile = useSelector(makeDefaultProfile); const defaultSizes = useSelector(makeDefaultSizes); const accountValue = getDefaultAccountValue(defaultAccount); const modeValue = getDefaultModeValue(defaultMode); const proxiesValue = getDefaultProxiesValue(defaultProxies); const profileValue = getDefaultProfileValue(defaultProfile); const sizesValue = getDefaultSizesValue(defaultSizes); const chooseAccount = (event: any) => { if (!event) { return dispatch(selectDefault(DEFAULTS_FIELDS.ACCOUNT, null)); } const account = accounts.find((a: any) => a.id === event.value); return dispatch(selectDefault(DEFAULTS_FIELDS.ACCOUNT, account)); }; const chooseMode = (event: any) => { if (!event) { return dispatch(selectDefault(DEFAULTS_FIELDS.MODE, null)); } return dispatch(selectDefault(DEFAULTS_FIELDS.MODE, event.value)); }; const chooseProxies = (event: any) => { if (!event) { return dispatch(selectDefault(DEFAULTS_FIELDS.PROXIES, null)); } return dispatch( selectDefault(DEFAULTS_FIELDS.PROXIES, { id: event.value, name: event.label }) ); }; const chooseProfiles = (event: any) => { if (!event) { return dispatch(selectDefault(DEFAULTS_FIELDS.PROFILE, null)); } return dispatch(selectDefault(DEFAULTS_FIELDS.PROFILE, event)); }; const chooseSize = (event: any) => { if (!event) { return dispatch(selectDefault(DEFAULTS_FIELDS.SIZES, null)); } return dispatch( selectDefault( DEFAULTS_FIELDS.SIZES, event.map(({ value }: { value: string }) => value) ) ); }; const makeSize = (event: any) => { const size = createSize(event); if (!size) { return null; } return dispatch( selectDefault(DEFAULTS_FIELDS.SIZES, [...defaultSizes, size]) ); }; return (
Account
Mode
Proxies
Profile(s)
Sizes
Defaults are used for Quick Tasks on Shopify. Simply make sure you fill out all the provided fields above, then use the built-in monitor in the Discord server to quickly launch tasks into your Default task group.
); }; export default DefaultsSettingsDialog; ================================================ FILE: app/components/Settings/components/dialog/generics.tsx ================================================ import React, { useState } from 'react'; import { ipcRenderer } from 'electron'; import { makeStyles } from '@material-ui/styles'; import { useDispatch, useSelector } from 'react-redux'; import { Typography, DialogContent, FormControl, FormControlLabel, FormGroup, Fade, Switch, Input, Button } from '@material-ui/core'; import classNames from 'classnames'; import { toggleField, editAutoSolve, setAutoSolveConnected, AUTOSOLVE_FIELDS, SETTINGS_FIELDS } from '../../actions'; import { makeAutoSolve, makeAutoSolveConnected } from '../../selectors'; import { IPCKeys } from '../../../../constants/ipc'; import { openExternalUrl } from '../../../../utils/url'; import { styles } from '../../styles'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); const GenericSettingsDialog = () => { const styles = useStyles(); const dispatch = useDispatch(); const enableAutoRestart = useSelector( (state: RootState) => state.Settings.enableAutoRestart ); const enablePerformance = useSelector( (state: RootState) => state.Settings.enablePerformance ); const enableNotifications = useSelector( (state: RootState) => state.Settings.enableNotifications ); const { accessToken, apiKey } = useSelector(makeAutoSolve); const autoSolveConnected = useSelector(makeAutoSolveConnected); const [isConnecting, setIsConnecting] = useState(false); const [timeout, resetTimeout] = useState(null); const [message, setMessage] = useState(''); const connectAutoSolve = () => { if (!accessToken || !apiKey) { return; } if (!isConnecting) { setIsConnecting(true); ipcRenderer .invoke(IPCKeys.SetupAutoSolve, { accessToken, apiKey }) .then(({ success, error }) => { setIsConnecting(false); if (success) { return dispatch(setAutoSolveConnected(true)); } if (timeout) { clearTimeout(timeout); } if (error && typeof error === 'string') { setMessage(error); setTimeout(() => { clearTimeout(timeout); setMessage(''); resetTimeout(null); }, 1500); } return dispatch(setAutoSolveConnected(false)); }) .catch(() => { return dispatch(setAutoSolveConnected(false)); }); } }; const disconnectAutoSolve = () => { ipcRenderer .invoke(IPCKeys.SetupAutoSolve, {}) .then(() => dispatch(setAutoSolveConnected(false))) .catch(() => dispatch(setAutoSolveConnected(false))); }; const editAutoSolveHandler = (field: string, value: string) => { dispatch(editAutoSolve(field, value)); }; const autoRestartHandler = (event: any) => { dispatch(toggleField(SETTINGS_FIELDS.AUTO_RESTART)); ipcRenderer.send(IPCKeys.ToggleAutoRestart, event.target.checked); }; return (
Automatic Restart } label={enableAutoRestart ? `Enabled` : `Disabled`} />
Application Effects dispatch(toggleField(SETTINGS_FIELDS.NOTIFICATIONS)) } /> } label={enableNotifications ? `Enabled` : `Disabled`} />
Reduced Rendering dispatch(toggleField(SETTINGS_FIELDS.PERFORMANCE)) } /> } label={enablePerformance ? `Enabled` : `Disabled`} />
We do not gather any kind of personal information and neither do we sell your data. We use this information only to improve the User Experience and to hopefully help squash bugs.
AYCD AutoSolve {message}
Access Token editAutoSolveHandler( AUTOSOLVE_FIELDS.ACCESS_TOKEN, e.target.value ) } />
API Key {autoSolveConnected ? ( ) : ( editAutoSolveHandler( AUTOSOLVE_FIELDS.API_KEY, e.target.value ) } /> )}
Please make sure you are familiar with AutoSolve, it's purpose, and how to utilize it properly before attempting to integrate it into Omega. { openExternalUrl( 'https://aycd.zendesk.com/hc/en-us/articles/360045923874-Supported-Bots-Nebula', true ); }} > Learn more here..
); }; export default GenericSettingsDialog; ================================================ FILE: app/components/Settings/components/dialog/index.tsx ================================================ import React, { useState, useEffect } from 'react'; import { ipcRenderer } from 'electron'; import { makeStyles } from '@material-ui/styles'; import classNames from 'classnames'; import { Typography, Button, Dialog, DialogActions } from '@material-ui/core'; import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; import ArrowBackIos from '@material-ui/icons/ArrowBackIos'; import Generics from './generics'; import Accounts from './accounts'; import Rates from './rates'; import Webhooks from './webhooks'; import Defaults from './defaults'; import { IPCKeys } from '../../../../constants/ipc'; import { styles } from '../../styles'; import { openExternalUrl } from '../../../../utils/url'; const useStyles = makeStyles(styles); const DialogContent = ({ activeStep }: { activeStep: number }) => { switch (activeStep) { case 0: return ; case 1: return ; case 2: return ; case 3: return ; case 4: return ; default: return ; } }; const DialogTitle = ({ activeStep, handleNext, handleBack }: { activeStep: number; handleNext: any; handleBack: any; }) => { const styles = useStyles(); switch (activeStep) { case 0: { return ( <> Settings Accounts ); } case 1: { return ( <> Accounts Shipping Rates ); } case 2: { return ( <> Shipping Rates Webhooks ); } case 3: { return ( <> Webhooks Defaults ); } case 4: { return ( <> Defaults ); } default: { return ( <> Settings Accounts ); } } }; const SettingsDialog = ({ open, onDialogBoxCloseBtnClick }: { open: boolean; onDialogBoxCloseBtnClick: any; }) => { const [version, setVersion] = useState(''); const styles = useStyles(); const [activeStep, setActiveStep] = useState(0); const handleNext = () => { setActiveStep(prevActiveStep => prevActiveStep + 1); }; const handleBack = () => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; useEffect(() => { const getVersion = async () => { const v = await ipcRenderer.invoke(IPCKeys.GetVersion); setVersion(v); }; getVersion(); }, []); return ( { setActiveStep(0); onDialogBoxCloseBtnClick({ confirm: false }); }} >
ipcRenderer.send(IPCKeys.RequestCheckForUpdates)} className={styles.a} > Version {version || 'N/A'} openExternalUrl({ url: 'https://nebulabots.com/privacy', isRenderer: true }) } > Privacy Policy   openExternalUrl({ url: 'https://nebulabots.com/terms', isRenderer: true }) } > Terms of Service
); }; export default SettingsDialog; ================================================ FILE: app/components/Settings/components/dialog/rates.tsx ================================================ import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { ipcRenderer } from 'electron'; import { ValidationError } from 'yup'; import { makeStyles } from '@material-ui/styles'; import WindowedSelect from 'react-windowed-select'; import { Typography, DialogContent, FormControl, FormGroup, Fade, Button } from '@material-ui/core'; import StoreField from './StoreField'; import ProfileField from './ProfileField'; import ProductField from './ProductField'; import { colorStyles, IndicatorSeparator } from '../../../../styles/select'; import { parseProduct } from '../../../../constants'; import { IPCKeys } from '../../../../constants/ipc'; import { Platforms } from '../../../../tasks/common/constants'; import { Constants } from '../../../../tasks/common'; // import { getValidationSchemaTask } from '../../../../utils/validateTasks'; import { styles } from '../../styles'; import { addRates, removeRates } from '../../actions'; import { RootState } from '../../../../store/reducers'; const processProduct = (value: string) => { const product = { raw: value || '' }; if (!value) { return { product: null }; } return { product }; }; const useStyles = makeStyles(styles); const ShippingRatesDialogContent = () => { const styles = useStyles(); const dispatch = useDispatch(); const profiles = useSelector((state: any) => state.Profiles); const ratesList = useSelector((state: any) => state.Rates); const stores = useSelector((state: any) => state.Stores); const theme = useSelector((state: RootState) => state.Theme); const [error, setError] = useState(''); const [store, setStore] = useState(null); const [profile, setProfile] = useState(null); const [product, setProduct] = useState(null); const [rate, setRate] = useState(null); const [isFetching, setIsFetching] = useState(false); // const validationSchema = getValidationSchemaTask(Platforms.Shopify); const handleSetStore = ({ value }: { value: any }) => { if (value && store && value.url === store.url) { return; } setStore(value); setRate(null); }; const handleSetProfile = ({ value }: { value: any }) => { if (profile?.id === value?.id) { return; } setProfile(value); }; const handleSetProduct = ({ value }: { value: string }) => { const result = processProduct(value); if (!result) { return; } setProduct(result.product); }; const handleSetRate = (e: any) => { setRate(e); }; const handleRatesStatus = (_: any, { success, error, store, rates }: any) => { if (success) { dispatch(addRates({ store, rates })); } if (error) { setError(error); setTimeout(() => { setError(''); setIsFetching(false); }, 1500); return; } setIsFetching(false); }; const extractRateList = () => { if (!store) { return []; } const { url } = store; const ratesForStore = ratesList[url]; if (!ratesForStore) { return []; } return ratesForStore.map( ({ id, name, price }: { id: string; name: string; price: string }) => ({ label: name, value: { id, name, price } }) ); }; const removeRate = () => { if (!rate || !store) { return; } dispatch(removeRates({ store: store.url, rate: rate.value })); setRate(null); }; const stopRate = async () => { ipcRenderer.send(IPCKeys.CancelRates); setIsFetching(false); }; const fetchRate = async () => { if (!product || !store) { return; } // they are providing a direct rate, let's bypass the fetcher if ( /^.+-.+-.+/i.test(product.raw) && !/^http/i.test(product.raw) && store ) { const [, name] = product.raw.match(/^[^-]*-(.*)-.*$/); const [, price] = product.raw.match(/^.+-.+-(.*)$/); dispatch( addRates({ store: store.url, rates: [{ id: product.raw, name, price }] }) ); return; } if (!profile) { return; } const task = { id: 'RATE_FETCHER', sizes: ['Random'], platform: Platforms.Shopify, type: Constants.Task.Types.Rates, store, profile, product: parseProduct(product) }; try { // const result = await validationSchema.validate(task); setIsFetching(true); ipcRenderer.send(IPCKeys.FetchRates, task); ipcRenderer.once(IPCKeys.RatesTaskStatus, handleRatesStatus); } catch (err) { if (err instanceof ValidationError) { // TODO - show validation on fields // TODO - show errors to users // err.errors } setIsFetching(false); // eslint-disable-next-line console.log('err: ', err); } }; let btnMessage = 'Fetch'; if (isFetching) { btnMessage = 'Cancel'; } if (error) { btnMessage = `ERR::${error}`; } return (
Current Rates
Shopify shipping rates can be pre-fetched and used to speed up the checkout process a bit more. Use this dialog to enter in an already fetched rate, or use a profile and product to fetch rates for a store now.
); }; export default ShippingRatesDialogContent; ================================================ FILE: app/components/Settings/components/dialog/webhooks.tsx ================================================ import React from 'react'; import { ipcRenderer } from 'electron'; import { useSelector, useDispatch } from 'react-redux'; import WindowedSelect from 'react-windowed-select'; import { makeStyles } from '@material-ui/styles'; import { Typography, DialogContent, FormControl, FormGroup, Fade, Input, Button, FormControlLabel, Checkbox } from '@material-ui/core'; import { colorStyles, IndicatorSeparator } from '../../../../styles/select'; import { deleteWebhook, editWebhook, selectWebhook, saveWebhook, WEBHOOK_FIELDS } from '../../actions'; import { makeWebhookList, makeCurrentWebhook } from '../../selectors'; import { styles } from '../../styles'; import { IPCKeys } from '../../../../constants/ipc'; import { buildWebhookOptions } from '../../../../constants'; import { RootState } from '../../../../store/reducers'; const useStyles = makeStyles(styles); const WebhooksDialog = () => { const styles = useStyles(); const dispatch = useDispatch(); const webhook = useSelector(makeCurrentWebhook); const webhooks = useSelector(makeWebhookList); const theme = useSelector((state: RootState) => state.Theme); const { id, url, name } = webhook; let webhookValue = null; if (id) { webhookValue = { value: webhook.id, label: webhook.name }; } const editHandler = (field: string, value: string) => { dispatch(editWebhook(field, value)); }; const selectHandler = (event: any) => { if (!event) { return dispatch(selectWebhook(null)); } const webhook = webhooks.find((w: any) => w.id === event.value); return dispatch(selectWebhook(webhook)); }; const saveHandler = () => { if (!name || !url) { return; } dispatch(saveWebhook(webhook)); }; const deleteHandler = () => { if (!id) { return; } dispatch(deleteWebhook(webhook)); }; const { declines } = webhook; const handleWebhookTest = () => { ipcRenderer.send(IPCKeys.TestWebhook, webhook); }; return (
Webhook editHandler(WEBHOOK_FIELDS.URL, e.target.value) } />
Name editHandler(WEBHOOK_FIELDS.NAME, e.target.value) } />
editHandler(WEBHOOK_FIELDS.DECLINES, e.target.checked) } value={declines ? 'true' : 'false'} color="primary" /> } label={ Declines } />
Webhooks
Webhooks are an easy way to keep track of all bot activity in one nice and neat place. Omega will provide you with checkout links, success messages, and much more!
); }; export default WebhooksDialog; ================================================ FILE: app/components/Settings/index.tsx ================================================ import React, { useMemo } from 'react'; import SettingsDialog from './components/dialog'; const Settings = ({ show, toggleSettings }: { show: boolean; toggleSettings: Function; }) => { return useMemo( () => ( ), [show] ); }; export default Settings; ================================================ FILE: app/components/Settings/reducers/accounts.tsx ================================================ import uuidv4 from 'uuidv4'; import { PURGE } from 'redux-persist'; import { CurrentAccount } from './currentAccount'; import { accountsActionTypes } from '../actions'; import regexes from '../../../constants/regexes'; type Action = { type: string; payload?: any; }; export type Accounts = CurrentAccount[]; export const accountsInitialState: Accounts = []; export function Accounts(state = accountsInitialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...accountsInitialState]; case accountsActionTypes.SAVE_ACCOUNT: { // new account... if (!payload.id) { let newId: string; const idCheck = (acc: any) => acc.id === newId; do { newId = uuidv4(); } while (state.some(idCheck)); payload.id = newId; return [...state, payload]; } // existing account... return state.map(acc => { if (acc.id === payload.id) { return payload; } return acc; }); } case accountsActionTypes.UPLOAD_ACCOUNTS: { if (!payload || (payload && !payload.length)) { return state; } return [ ...state, ...payload .map(([username, password]: [string, string]) => { const newAccount: any = { username, password }; if (!regexes.email.test(username) || !password) { return null; } let id: string; const checker = (p: any) => p.id === id; do { id = uuidv4(); } while (state.some(checker)); newAccount.id = id; newAccount.name = username; return newAccount; }) .filter(Boolean) ]; } case accountsActionTypes.IMPORT: { if (!payload || (payload && !payload.length)) { return state; } // verify integrity... if ( !payload.every( (account: any) => account.name && account.username && account.password ) ) { return state; } return [ ...state, ...payload.map((a: any) => { const newAccount: any = { ...a }; let id: string; const checker = (p: any) => p.id === id; do { id = uuidv4(); } while (state.some(checker)); newAccount.id = id; return newAccount; }) ]; } case accountsActionTypes.DELETE_ACCOUNT: { if (!payload || (payload && !payload.id)) { return state; } return state.filter(acc => acc.id !== payload.id); } default: return state; } } ================================================ FILE: app/components/Settings/reducers/currentAccount.tsx ================================================ import { PURGE } from 'redux-persist'; import { accountsActionTypes } from '../actions'; export type CurrentAccount = { id: null | string; name: string; username: string; password: string; }; type Action = { type: string; payload?: any; }; export const currentAccountInitialState: CurrentAccount = { id: null, name: '', username: '', password: '' }; export function CurrentAccount( state = currentAccountInitialState, action: Action ) { const { type, payload } = action; switch (type) { case PURGE: return { ...currentAccountInitialState }; case accountsActionTypes.EDIT_ACCOUNT: { const { field, value } = payload; if (!field) { return state; } return { ...state, [field]: value }; } case accountsActionTypes.DELETE_ACCOUNT: { if ( !payload || (payload && !payload.id) || (payload && payload.id !== state.id) ) { return state; } return { ...currentAccountInitialState }; } case accountsActionTypes.SELECT_ACCOUNT: { if (payload === null) { return { ...currentAccountInitialState }; } if (!payload || (payload && payload.id === state.id)) { return state; } return payload; } case accountsActionTypes.SAVE_ACCOUNT: { if ( !payload || (payload && (!payload.name || !payload.username || !payload.password)) ) { return state; } return { ...currentAccountInitialState }; } default: return state; } } ================================================ FILE: app/components/Settings/reducers/currentWebhook.tsx ================================================ import { PURGE } from 'redux-persist'; import { webhookActionTypes } from '../actions'; export type CurrentWebhook = { id: null | string; name: string; url: string; type: string; declines: boolean; }; type Action = { type: string; payload?: any; }; export const currentWebhookInitialState: CurrentWebhook = { id: null, name: '', url: '', type: '', declines: true }; export function CurrentWebhook( state = currentWebhookInitialState, action: Action ) { const { type, payload } = action; switch (type) { case PURGE: return { ...currentWebhookInitialState }; case webhookActionTypes.EDIT_WEBHOOK: { const { field, value } = payload; if (!field) { return state; } return { ...state, [field]: value }; } case webhookActionTypes.DELETE_WEBHOOK: { if ( !payload || (payload && !payload.id) || (payload && payload.id !== state.id) ) { return state; } return currentWebhookInitialState; } case webhookActionTypes.SELECT_WEBHOOK: { if (payload === null) { return { ...currentWebhookInitialState }; } if (!payload || (payload && payload.id === state.id)) { return state; } return payload; } case webhookActionTypes.SAVE_WEBHOOK: { if (!payload || (payload && (!payload.url || !payload.name))) { return state; } return { ...currentWebhookInitialState }; } default: return state; } } ================================================ FILE: app/components/Settings/reducers/defaults.tsx ================================================ import { PURGE } from 'redux-persist'; import { defaultsActionTypes } from '../actions'; type Action = { type: string; payload?: any; }; type Account = { id: string; name: string; username: string; password: string; }; type Proxies = { id: string; name: string; }; type Profile = { value: string; label: string; }; export type Defaults = { account: null | Account; mode: null | string; proxies: null | Proxies; profile: [] | Profile[]; sizes: [] | string[]; }; export const defaultsInitialState: Defaults = { account: null, mode: null, proxies: null, profile: [], sizes: [] }; export function Defaults(state = defaultsInitialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return { ...defaultsInitialState }; case defaultsActionTypes.SELECT: { if (!payload) { return state; } const { field, value } = payload; if (!field) { return state; } return { ...state, [field]: value }; } default: return state; } } ================================================ FILE: app/components/Settings/reducers/index.tsx ================================================ import { Settings, settingsInitialState } from './settings'; import { Accounts, accountsInitialState } from './accounts'; import { CurrentAccount, currentAccountInitialState } from './currentAccount'; import { Webhooks, webhooksInitialState } from './webhooks'; import { CurrentWebhook, currentWebhookInitialState } from './currentWebhook'; import { Rates, ratesInitialState } from './rates'; import { Defaults, defaultsInitialState } from './defaults'; export { Settings, Accounts, CurrentAccount, Webhooks, CurrentWebhook, Rates, Defaults, settingsInitialState, accountsInitialState, currentAccountInitialState, webhooksInitialState, currentWebhookInitialState, ratesInitialState, defaultsInitialState }; ================================================ FILE: app/components/Settings/reducers/rates.ts ================================================ import { unionBy } from 'lodash'; import { PURGE } from 'redux-persist'; import { ratesActionTypes } from '../actions'; export const ratesInitialState: Rates = {}; export type Rate = { name: string; price: string; id: string; }; export type Rates = { [key: string]: Rate[]; }; type Action = { type: string; payload?: any; }; export function Rates(state = ratesInitialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return { ...ratesInitialState }; case ratesActionTypes.ADD_RATES: { const { store, rates } = payload; const previous = state[store]; if (previous) { return { ...state, [store]: unionBy(rates, previous, 'id') }; } return { ...state, [store]: rates }; } case ratesActionTypes.REMOVE_RATES: { const { rate, store } = payload; const ratesObject = state[store]; if (!ratesObject) { return state; } return { ...state, [store]: ratesObject.filter(({ id }) => id !== rate.id) }; } default: return state; } } ================================================ FILE: app/components/Settings/reducers/settings.tsx ================================================ import { ipcRenderer } from 'electron'; import { PURGE } from 'redux-persist'; import { settingsActionTypes } from '../actions'; import { Groups, VIEWS } from '../../../constants'; import { settingsStorage } from '../../../utils/storageHelper'; import { IPCKeys } from '../../../constants/ipc'; type AutoSolve = { accessToken: string; apiKey: string; }; type Store = { name: string; url: string; }; export type Settings = { collapsed: boolean; autoSolve: AutoSolve; autoSolveConnected: boolean; proxySite: null | Store; expensesView: string; statsView: string; tasksGroup: string; toggleCreateProxies: boolean; toggleCreateProfile: boolean; toggleEditTask: boolean; toggleCreateTask: boolean; enableAutoRestart: boolean; enableNotifications: boolean; enablePerformance: boolean; stagger: number; analyticsFile: string; }; export const settingsInitialState: Settings = { collapsed: false, autoSolve: { accessToken: '', apiKey: '' }, proxySite: null, autoSolveConnected: false, expensesView: VIEWS.Weekly, statsView: VIEWS.Weekly, tasksGroup: Groups.None, toggleCreateProxies: false, toggleCreateProfile: false, toggleEditTask: false, toggleCreateTask: false, enableAutoRestart: false, enableNotifications: false, enablePerformance: false, stagger: 1, analyticsFile: '' }; type Action = { type: string; payload?: any; }; export function Settings(state = settingsInitialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return { ...settingsInitialState }; // togglers case settingsActionTypes.TOGGLE_FIELD: { const { field } = payload; if (!field) { return state; } const value = !state[field] || false; settingsStorage.set(field, value); return { ...state, [field]: value }; } // setters case settingsActionTypes.SET_FIELD: { if (!payload) { return state; } const { field, value } = payload; if (!field) { return state; } return { ...state, [field]: value }; } case settingsActionTypes.EDIT_AUTOSOLVE: { const { field, value } = payload; if (!field) { return state; } return { ...state, autoSolve: { ...state.autoSolve, [field]: value } }; } case settingsActionTypes.EDIT_STAGGER: { ipcRenderer.send(IPCKeys.ChangeStagger, payload || 1); return { ...state, stagger: payload || 1 }; } case settingsActionTypes.SET_ANALYTICS_FILE: { const { field, value } = payload; if (!field) { return state; } if (!value) { ipcRenderer.send(IPCKeys.RemoveAnalyticsFile); } else { ipcRenderer.send(IPCKeys.AddAnalyticsFile, value); } return { ...state, [field]: value }; } case settingsActionTypes.COPY_JSON_FILE_TO_SETTINGS: return { ...state, ...payload, autoSolve: { ...payload.autoSolve, ...state.autoSolve } }; default: return state; } } ================================================ FILE: app/components/Settings/reducers/webhooks.tsx ================================================ import { ipcRenderer } from 'electron'; import uuidv4 from 'uuidv4'; import { PURGE } from 'redux-persist'; import { webhookActionTypes } from '../actions'; import { CurrentWebhook } from './currentWebhook'; import { IPCKeys } from '../../../constants/ipc'; export const webhooksInitialState: any[] = []; export type Webhooks = CurrentWebhook[]; type Action = { type: string; payload?: any; }; export function Webhooks(state = webhooksInitialState, action: Action) { const { type, payload } = action; switch (type) { case PURGE: return [...webhooksInitialState]; case webhookActionTypes.SAVE_WEBHOOK: { // new webhook... if (!payload.id) { let newId: string; const idCheck = (acc: any) => acc.id === newId; do { newId = uuidv4(); } while (state.some(idCheck)); payload.id = newId; ipcRenderer.send(IPCKeys.AddWebhooks, [payload]); return [...state, payload]; } // existing webhook... return state.map(hook => { if (hook.id === payload.id) { ipcRenderer.send(IPCKeys.AddWebhooks, [payload]); return payload; } return hook; }); } case webhookActionTypes.DELETE_WEBHOOK: { if (!payload || (payload && !payload.id)) { return state; } ipcRenderer.send(IPCKeys.RemoveWebhooks, [payload]); return state.filter(hook => hook.id !== payload.id); } default: return state; } } ================================================ FILE: app/components/Settings/selectors.tsx ================================================ import { createSelector } from 'reselect'; import { accountsInitialState, currentAccountInitialState, webhooksInitialState, currentWebhookInitialState } from './reducers'; import { RootState } from '../../store/reducers'; const selectDefaults = (state: RootState) => state.Defaults; const selectSettings = (state: RootState) => state.Settings; const selectAccounts = (state: RootState) => state.Accounts; const selectCurrentAccount = (state: RootState) => state.CurrentAccount; const selectCurrentWebhook = (state: RootState) => state.CurrentWebhook; const selectWebhooks = (state: RootState) => state.Webhooks; export const makeCreateTask = createSelector( selectSettings, state => state.toggleCreateTask ); export const makeAnalyticsFile = createSelector( selectSettings, state => state.analyticsFile ); export const makeEditTask = createSelector( selectSettings, state => state.toggleEditTask ); export const makeCreateProxies = createSelector( selectSettings, state => state.toggleCreateProxies ); export const makeCreateProfile = createSelector( selectSettings, state => state.toggleCreateProfile ); export const makeProxySite = createSelector( selectSettings, state => state.proxySite ); export const makeExpensesView = createSelector( selectSettings, state => state.expensesView ); export const makeAutoSolve = createSelector( selectSettings, state => state.autoSolve ); export const makeAutoSolveConnected = createSelector( selectSettings, state => state.autoSolveConnected ); export const makeCurrentAccount = createSelector( selectCurrentAccount, state => state || currentAccountInitialState ); export const makeAccountsList = createSelector( selectAccounts, state => state || accountsInitialState ); export const makeCurrentWebhook = createSelector( selectCurrentWebhook, state => state || currentWebhookInitialState ); export const makeWebhookList = createSelector( selectWebhooks, state => state || webhooksInitialState ); export const makeDefaultAccount = createSelector( selectDefaults, state => state.account ); export const makeDefaultMode = createSelector( selectDefaults, state => state.mode ); export const makeDefaultProxies = createSelector( selectDefaults, state => state.proxies ); export const makeDefaultProfile = createSelector( selectDefaults, state => state.profile ); export const makeDefaultSizes = createSelector( selectDefaults, state => state.sizes ); ================================================ FILE: app/components/Settings/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ margin: {}, root: {}, dialog: { padding: '16px 24px 24px 24px', backgroundColor: theme.palette.primary.background, color: theme.palette.primary.color }, dialogSizes: { padding: '16px 24px 24px 24px', minHeight: 215 }, topRow: { display: 'flex' }, pointer: { cursor: 'pointer' }, pushRight: { marginRight: 'auto !important' }, flexRow: { display: 'flex', alignItems: 'flex-end', margin: '0' }, flexCol: { width: `33%`, display: 'flex', flexDirection: 'column' }, input: { borderRadius: 5, fontSize: 12, paddingLeft: 8, paddingRight: 8, fontWeight: 400, backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, border: `1px solid ${theme.palette.primary.border}`, width: '100%' }, createBtn: { fontSize: '12px', justifyContent: 'center', alignSelf: 'flex-start', fontWeight: 400, lineHeight: 1, marginRight: '12px', padding: '6px 16px', borderColor: '#8E83F4', backgroundColor: '#8E83F4', color: '#fff', width: '100%', height: 29, maxHeight: 29, transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { opacity: 0.5, borderColor: '#8E83F4', backgroundColor: '#8E83F4', color: '#fff' } }, createBtnLight: { fontSize: '12px', justifyContent: 'center', alignSelf: 'flex-start', fontWeight: 400, lineHeight: 1, opacity: 0.5, marginRight: '12px', padding: '6px 16px', borderColor: '#8E83F4', backgroundColor: '#8E83F4', color: '#fff', width: '100%', height: 29, maxHeight: 29, transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { opacity: 0.25, borderColor: '#8E83F4', backgroundColor: '#8E83F4', color: '#fff' } }, deleteBtn: { opacity: 0.5, fontSize: '12px', justifyContent: 'center', alignSelf: 'flex-start', fontWeight: 400, lineHeight: 1, height: 29, maxHeight: 29, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.border, padding: '6px 16px', width: '100%', transition: theme.transitions.create(['opacity'], { duration: 300 }), '&:hover': { opacity: 1, border: `1px solid ${theme.palette.primary.border}`, color: theme.palette.primary.color, backgroundColor: theme.palette.primary.border } }, fieldset: { width: '33%' }, fieldSetFirst: { maxWidth: '47.5%', width: '47.5%', marginRight: 16 }, fieldSetSecond: { width: '19%', marginLeft: 14, marginTop: 21 }, accountFieldOne: { width: `30%`, margin: '0 11px 0 0' }, accountFieldTwo: { width: `30%`, margin: '0 11px' }, accountFieldThree: { width: `30%`, margin: '0 0 0 11px' }, autoSolveField: { width: `36.5%`, margin: '0 12px 0 0' }, fieldSetHalfOne: { width: '47.5%', maxWidth: '47.5%', marginRight: 8, overflow: 'hidden' }, fieldSetHalfTwo: { width: '47.5%', maxWidth: '47.5%', marginLeft: 8, overflow: 'hidden' }, fieldSetWebhookName: { width: '27%', marginLeft: 8, overflow: 'hidden' }, fieldSetDeclines: { width: '18%', marginLeft: 16, marginTop: 14, overflow: 'hidden' }, previousIcon: { width: 16, height: 16, display: 'flex', flexDirection: 'column', margin: 'auto 8px', color: '#616161' }, nextIcon: { width: 16, height: 16, display: 'flex', flexDirection: 'column', margin: 'auto 8px', color: '#616161' }, subtitle: {}, fmSettingsStylesFix: { marginTop: 10 }, formGroup: { paddingTop: 0 }, flexStart: { width: '100%', marginLeft: '0 !important', justifyContent: 'flex-start' }, subcategory: { margin: 'auto 24px', display: 'inline-flex', flexDirection: 'row', justifyContent: 'flex-start', color: '#616161' }, autoSolveError: { margin: 'auto 24px', display: 'inline-flex', flexDirection: 'row', justifyContent: 'flex-start', fontSize: '12px', fontWeight: 400, whiteSpace: 'nowrap', color: '#FF4462' }, subheading: { margin: 'auto 24px', display: 'inline-flex', flexDirection: 'row', justifyContent: 'flex-start', color: '#616161', transition: theme.transitions.create(['color'], { duration: 300 }), '&:hover': { cursor: 'pointer', color: '#8E83F4', transition: theme.transitions.create(['color'], { duration: 300 }), '& > *': { cursor: 'pointer', color: '#8E83F4', transition: theme.transitions.create(['color'], { duration: 300 }) } } }, title: { margin: 24, display: 'flex', flexDirection: 'row', justifyContent: 'center' }, switch: {}, block: { marginBottom: 20 }, marginTop: { marginTop: 20 }, marginBottom: { marginBottom: 10 }, onBoardingPaper: { position: `relative`, padding: 10, margin: '0 0 8px 0', color: '#fff', backgroundColor: variables().styles.primaryColor.main }, onBoardingPaperArrow: { fontWeight: `bold`, content: ' ', borderBottom: `11px solid ${variables().styles.primaryColor.main}`, borderLeft: '8px solid transparent', borderRight: '8px solid transparent', position: 'absolute', top: -10, left: 2 }, onBoardingPaperBody: { color: variables().styles.primaryColor.secondary }, a: { fontWeight: `bold`, margin: '0 8px 0 4px' }, aLight: { fontWeight: 400, margin: '0 8px' }, btnPositive: { flex: 0, display: 'flex', margin: '0 20px', ...mixins().btnPositive }, btnWarning: { display: 'flex', ...mixins().btnNegative }, bottomRow: { flex: '0 0 auto', margin: '8px 4px', display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }, textCol: { flex: 1, margin: '8px 16px auto 16px' } }); ================================================ FILE: app/components/Sidebar/Sidebar.tsx ================================================ import React, { Fragment, useState, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import classNames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; import { Link, useLocation } from 'react-router-dom'; import { Grid, List, ListItem, ListItemIcon, ListItemText, Typography } from '@material-ui/core'; import { styles } from './styles'; import { SIDEBAR } from './components/menuItems'; import AnimatedLogo from './components/AnimatedLogo'; import { RootState } from '../../store/reducers'; import { setTheme } from '../App/actions'; import { changeTheme } from '../Captchas/actions'; import { getIcon } from './components/icons'; const useStyles = makeStyles(styles); const themeToNext = (theme: number) => (theme === 0 ? 1 : 0); const Sidebar = () => { const classes = useStyles(); const dispatch = useDispatch(); const theme = useSelector((state: RootState) => state.Theme); const { pathname } = useLocation(); const [collapsed, setCollapsed] = useState(false); const handleSetCollapsed = useCallback(() => { setCollapsed(!collapsed); }, [collapsed]); const handleSetTheme = useCallback(() => { const nextTheme = themeToNext(theme) || 0; dispatch(setTheme(nextTheme)); dispatch(changeTheme(nextTheme)); }, [theme]); // if we have a menu items with a submenu, render that const isActivePath = useCallback( (route: any) => pathname === route || (pathname === '/' && route === '/analytics'), [pathname] ); const renderSubMenu = (menuItem: any) => { const { subMenu } = menuItem; return (
{subMenu.map((item: any, i: number) => { const className = [classes.navBtns]; const isActive = isActivePath(item.path); if (item.disabled) { return ( {getIcon(item.img)} ); } if (i === subMenu.length - 1) { return ( {isActive ? (
{getIcon(item.img)}
) : ( {getIcon(item.img)} )}
); } return isActive ? (
{getIcon(item.img)}
) : ( {getIcon(item.img)} ); })}
); }; const _renderMenuItems = (menuItem: any) => { const className = [classes.noAppDrag]; const { subMenu, path, name, img } = menuItem; if (subMenu) { return renderSubMenu(menuItem); } const isActive = isActivePath(path); return ( {isActive ? (
{getIcon(img)}
) : ( {getIcon(img)} )}
); }; const _renderSidebarItems = () => { if (collapsed) { return ( ); } return ( Navigation {Object.values(SIDEBAR).map(src => _renderMenuItems(src))} ); }; const addedClass = collapsed ? classes.addedStyle : {}; const rootClass = collapsed ? classes.rootCollapsed : classes.root; const colClass = collapsed ? classes.colCenter : classes.col; if (/privacy|terms|bugs|progressbar/i.test(pathname)) { return null; } return (
{_renderSidebarItems()}
); }; export default Sidebar; ================================================ FILE: app/components/Sidebar/__tests__/Sidebar.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import Sidebar from '../Sidebar'; import { withProviders } from '../../../../test/testUtils'; it('should render Sidebar', async () => { const Root = withProviders({ Component: Sidebar }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); expect(getByText('Proxies')).toBeDefined(); }); ================================================ FILE: app/components/Sidebar/components/AnimatedLogo.tsx ================================================ import React, { useState, useCallback } from 'react'; import { animated, useSpring } from 'react-spring'; import { Fade } from '@material-ui/core'; import classNames from 'classnames'; import { imgsrc } from '../../../utils/imgsrc'; const AnimatedLogo = ({ classes, addedClass, collapsed, setTheme }: { classes: any; addedClass: null | object; collapsed: boolean; setTheme: Function; }) => { const [state, toggle] = useState(true); const clickHandler = useCallback(() => { setTheme(); toggle(!state); }, [state]); const { x } = useSpring({ from: { x: 0 }, x: state ? 1 : 0, config: { duration: 1000 } }); if (collapsed) { return null; } return ( `scale(${x})`) }} >
); }; export default AnimatedLogo; ================================================ FILE: app/components/Sidebar/components/icons.tsx ================================================ import React from 'react'; import AppsIcon from '@material-ui/icons/Apps'; import EventNoteTwoToneIcon from '@material-ui/icons/EventNoteTwoTone'; import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; import ListAltTwoToneIcon from '@material-ui/icons/ListAltTwoTone'; import LibraryAdd from '@material-ui/icons/LibraryAdd'; import PersonOutlineIcon from '@material-ui/icons/PersonOutline'; import HttpsTwoToneIcon from '@material-ui/icons/HttpsTwoTone'; import AssessmentTwoToneIcon from '@material-ui/icons/AssessmentTwoTone'; import BookmarkBorderIcon from '@material-ui/icons/BookmarkBorder'; export const getIcon = (name: string) => { return SIDEBAR_ICONS[name]; }; export const SIDEBAR_ICONS: any = { HomeIcon: , TimerIcon: , TrackChangesIcon: , ListAltIcon: , EventNoteIcon: , InsertChart: , LibraryAdd: , Payment: , AttachMoneyIcon: }; ================================================ FILE: app/components/Sidebar/components/menuItems.tsx ================================================ import { routeTo } from '../../../routing/routes'; export const SIDEBAR = { // USELESS WITHOUT API // Dashboard: { // path: routeTo('Home'), // exact: true, // name: 'Dashboard', // img: 'HomeIcon', // subMenu: [ // { // path: routeTo('Analytics'), // exact: true, // name: 'Analytics', // img: 'InsertChart' // } // ] // }, Releases: { path: '/releases', exact: true, name: 'Releases', img: 'LibraryAdd', subMenu: [ { path: routeTo('Tasks'), exact: true, name: 'Tasks', img: 'LibraryAdd' }, { path: routeTo('Captchas'), exact: true, name: 'Harvesters', img: 'TimerIcon' } ] }, Profiles: { path: routeTo('Profiles'), exact: true, name: 'Profiles', img: 'Payment' }, Proxies: { path: routeTo('Proxies'), exact: true, name: 'Proxies', img: 'AttachMoneyIcon' } }; ================================================ FILE: app/components/Sidebar/styles/index.tsx ================================================ import { variables, mixins } from '../../../styles/js'; export const styles = theme => ({ root: { boxShadow: `5px 0px 5px 1px ${theme.palette.primary.boxShadow}`, maxWidth: '236px', zIndex: 999, position: 'static !important', backgroundColor: theme.palette.primary.secondary, color: theme.palette.primary.color, display: 'flex', flexDirection: 'column', flexGrow: 1, height: '100%', width: '100%', transition: theme.transitions.create(['max-width'], { duration: 300 }) }, rootCollapsed: { boxShadow: `5px 0px 5px 1px ${theme.palette.primary.boxShadow}`, maxWidth: '40px', zIndex: 999, position: 'static !important', backgroundColor: theme.palette.primary.secondary, display: 'flex', flexDirection: 'column', flexGrow: 1, height: '100%', width: '100%', transition: theme.transitions.create(['max-width'], { duration: 300 }) }, defaultIcon: { color: theme.palette.primary.navText }, adjustSizing: { width: 16, height: 'auto', marginLeft: 24, color: theme.palette.primary.color }, navHeader: { color: '#8D8D8D', fontSize: 12, marginBottom: 8 }, marginBottom: { marginBottom: 1 }, navText: { fontSize: 12 }, defaultCursor: { cursor: 'default !important', marginRight: 0 }, activeNavBg: { background: variables().styles.sidebar.active }, activeNavText: { fontSize: 12, color: '#fff !important' }, afterRight: { paddingLeft: `24px` }, navigationHeader: { backgroundColor: theme.palette.primary.navHeader, color: theme.palette.primary.color, borderRadius: 5, paddingTop: 4, paddingBottom: 4 }, mainNavigation: { backgroundColor: theme.palette.primary.navBackground, margin: '6px 0', borderRadius: 5, paddingTop: 1, paddingBottom: 1 }, altNavigation: { cursor: 'pointer', border: '2px solid', borderRadius: 5, borderColor: variables().styles.sidebar.header, padding: '0', marginTop: 'auto' }, noAppDrag: { ...mixins().appDragDisable, ...mixins().noDrag, borderRadius: 5 }, addedStyle: { display: 'none', transition: theme.transitions.create(['display'], { duration: 300 }) }, collapse: { padding: 0, textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, navBtns: { paddingLeft: 0, color: theme.palette.primary.iconColor }, nested: { backgroundColor: theme.palette.primary.navBackground, paddingLeft: 16, paddingTop: 1, paddingBottom: 1, borderRadius: 4, margin: '12px 0' }, nestedDisabled: { paddingLeft: 16, backgroundColor: theme.palette.primary.navBackground, paddingTop: 1, paddingBottom: 1, borderRadius: 4, margin: '12px 0', cursor: 'not-allowed' }, shrinkText: { fontSize: 11, textAlign: 'center', display: 'flex', justifyContent: 'center' }, navBtnImgs: { height: 25, width: `auto`, ...mixins().noDrag, ...mixins().noselect }, navBtnImgsEnd: { display: 'flex', alignSelf: 'flex-end' }, rowStartExpand: { display: 'flex', flexDirection: 'row', alignItems: 'center' }, fill: { display: 'flex', flexDirection: 'row', alignSelf: 'flex-start', justifyContent: 'center', width: '100%' }, end: { display: 'flex', flexDirection: 'row', alignSelf: 'flex-start', justifyContent: 'center', margin: '0 15px' }, center: { display: 'flex', flexDirection: 'row', justifyContent: 'center', margin: '32px 0 64px 0', position: 'static !important' }, row: { display: 'flex', flexDirection: 'row', margin: `0 15px` }, rowExpand: { display: 'flex', flexGrow: 1, flexDirection: 'row', justifyContent: 'center' }, col: { display: 'flex', flex: 1, flexDirection: 'column', margin: `0 15px`, transition: theme.transitions.create(['display'], { duration: 300 }) }, colCenter: { justifyContent: 'center', display: 'flex', flexDirection: 'column', alignSelf: 'center', margin: 'auto 0', height: '100%', width: '100%', cursor: 'pointer' }, collapsedBtn: { height: 20, width: 20 }, spacer: { margin: '16px 0' }, noPaddingLeft: { paddingLeft: 0 }, noMarginRight: { width: 18, height: 'auto', minWidth: 'unset' }, noPadding: { padding: 0 }, colExpand: { display: 'flex', flexGrow: 1, flexDirection: 'column', margin: `15px` } }); ================================================ FILE: app/components/Tasks/Loadable.tsx ================================================ import Loadable from 'react-imported-component'; import LoadingIndicator from '../LoadingIndicator'; export default Loadable(() => import('./Tasks'), { LoadingComponent: LoadingIndicator }); ================================================ FILE: app/components/Tasks/Tasks.tsx ================================================ import React, { useCallback, useMemo, useState } from 'react'; import { makeStyles } from '@material-ui/styles'; import TaskList from './components/Table/TaskList'; import ActionBar from './components/actionBar/actionBar'; import TaskCreateDialog from './components/create/TaskCreateDialog'; import TaskEditDialog from './components/edits/TaskEditDialog'; import { styles } from './styles'; const useStyles = makeStyles(styles); const Tasks = () => { const styles = useStyles(); const [all, setAll] = useState(false); const toggleAll = useCallback(() => { setAll(prev => !prev); }, []); return useMemo( () => (
), [all, styles] ); }; export default Tasks; ================================================ FILE: app/components/Tasks/__tests__/Tasks.spec.tsx ================================================ import { render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; import Tasks from '../Tasks'; import { withProviders } from '../../../../test/testUtils'; import { mockOffsetSize } from '../../../../test/mockOffsetSize'; it('should render Tasks', async () => { const Root = withProviders({ Component: Tasks }); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); mockOffsetSize(200, 200); expect(getByText('Store')).toBeDefined(); }); it.only('should render Tasks with some tasks', async () => { const initialState = { Tasks: [ { id: 'taskId', store: { name: 'Supreme' }, sizes: ['OS'], product: { raw: '+350' } } ], Settings: { tasksGroup: 'none' } }; const Root = withProviders({ Component: Tasks, initialState }); mockOffsetSize(200, 200); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Store')).toBeDefined(); }); // grouped tasks are broken it.skip('should render Tasks with some tasks and grouped by store', async () => { const initialState = { Tasks: { task1: { store: { name: 'Supreme' }, sizes: ['OS'], product: { raw: '+350' } }, task2: { store: { name: 'Shopiy' }, sizes: ['OS'], product: { raw: '+350' } } }, Settings: { toggleCreateTask: false, tasksGroup: 'store' } }; const Root = withProviders({ Component: Tasks, initialState }); mockOffsetSize(200, 200); // eslint-disable-next-line const { debug, getByTestId, getByText } = render(); // debug(); expect(getByText('Store')).toBeDefined(); }); ================================================ FILE: app/components/Tasks/actions/index.tsx ================================================ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-unused-vars */ import { ipcRenderer } from 'electron'; import prefixer from '../../../utils/reducerPrefixer'; import { States } from '../../../constants'; import { IPCKeys } from '../../../constants/ipc'; const taskPrefix = '@@Tasks'; const taskTypesList = [ // Individual task actions 'EDIT', // couples as editing create and edit dialog 'STATUS', // Mass override link 'INJECT_URL', 'CLEAR_DIALOG', 'NOTIFICATION', 'CREATE_TASKS', 'UPDATE_TASKS', 'CANCEL_UPDATE', 'DELETE_TASKS', 'SELECT_TASKS', 'COPY_TASKS', 'START_TASKS', 'STOP_TASKS', // Group task actions 'CREATE_GROUP', 'SELECT_GROUP', 'UPDATE_GROUP', 'DELETE_GROUP', 'COPY_GROUP', // I/O Operations 'IMPORT_TASKS' ]; const delayPrefix = '@@Delays'; const delayTypesList = ['CHANGE_DELAY']; export const taskActions = taskTypesList.map(a => `${taskPrefix}/${a}`); export const taskActionTypes = prefixer(taskPrefix, taskTypesList); export const delayActions = delayTypesList.map(a => `${delayPrefix}/${a}`); export const delayActionTypes = prefixer(delayPrefix, delayTypesList); export const editTask = ({ isEditing, field, value, stores = [] }: { isEditing: boolean; field: string; value: string; stores?: any[]; }) => ({ type: taskActionTypes.EDIT, payload: { isEditing, field, value, stores } }); export const status = (group: string, buffer: object) => ({ type: taskActionTypes.STATUS, payload: { group, buffer } }); export const injectUrl = (group: string, tasks: any[], url: string) => { const mixin = tasks.map(t => ({ ...t, product: { raw: url, url } })); return { type: taskActionTypes.INJECT_URL, payload: { group, tasks: mixin } }; }; export function notification(group: string, id: string, type: string) { return { type: taskActionTypes.NOTIFICATION, payload: { group, id, type } }; } export const clearInputs = (isEditing = false) => { return { type: taskActionTypes.CLEAR_DIALOG, payload: { isEditing } }; }; export const createTasks = (group: string, task: any) => { return { type: taskActionTypes.CREATE_TASKS, payload: { group, task } }; }; export const updateTasks = (group: string, task: any) => { return { type: taskActionTypes.UPDATE_TASKS, payload: { group, task } }; }; export const cancelUpdate = () => { return { type: taskActionTypes.CANCEL_UPDATE }; }; export function deleteTasks(group: string, tasks: any[], useTasks = false) { const toStop = tasks.filter(t => t.state === States.Running); ipcRenderer.send(IPCKeys.StopTasks, { group, tasks: toStop }); const payload: any = { group }; if (useTasks) { payload.tasks = tasks; } return { type: taskActionTypes.DELETE_TASKS, payload }; } export function selectTasks({ type = 'All', isRangeSelecting = false, group, id }: { type?: string; isRangeSelecting?: boolean; group: string; id?: string; }) { return { type: taskActionTypes.SELECT_TASKS, payload: { type, isRangeSelecting, group, id } }; } export function copyTasks(group: string, tasks: any[]) { const toCopy = tasks.map( ({ productName, productImage, productImageHi, chosenSize, chosenProxy, monitor, retry, ...t }) => ({ ...t, selected: false, lastSelected: false, splashNotification: true, successNotification: true, mode: t.backupMode || t.mode, state: States.Stopped, message: '' }) ); return { type: taskActionTypes.COPY_TASKS, payload: { group, tasks: toCopy } }; } export function startTasks(group: string, tasks: any[], delays: any) { const newTasks = tasks.filter(t => t.state !== States.Running); const toStart = newTasks.map(t => ({ ...t, ...delays, state: States.Running, message: 'Starting task' })); ipcRenderer.send(IPCKeys.StartTasks, { group, tasks: newTasks.map(({ startTime, endTime, ...t }) => ({ ...t, ...delays })) }); return { type: taskActionTypes.START_TASKS, payload: { group, tasks: toStart } }; } export function stopTasks(group: string, tasks: any[]) { const newTasks = tasks.filter(t => t.state === States.Running); const toStop: any = newTasks.map( ({ productName, productImage, productImageHi, chosenSize, chosenProxy, startTime, endTime, ...t }: { productName?: string; productImage?: string; productImageHi?: string; chosenSize?: string; chosenProxy?: string; }) => { return { ...t, mode: t.backupMode || t.mode, state: States.Stopped, splashNotification: true, successNotification: true, message: 'Idle' }; } ); ipcRenderer.send(IPCKeys.StopTasks, { group, tasks: toStop }); return { type: taskActionTypes.STOP_TASKS, payload: { group, tasks: toStop } }; } export function createGroup(value: string) { return { type: taskActionTypes.CREATE_GROUP, payload: value }; } export function selectGroup(id: string | null) { return { type: taskActionTypes.SELECT_GROUP, payload: id }; } export function deleteGroup(id: string) { return { type: taskActionTypes.DELETE_GROUP, payload: id }; } export function importTasks(tasks: any) { return { type: taskActionTypes.IMPORT_TASKS, payload: tasks }; } export const TASK_FIELDS = { RESTOCK_MODE: 'restockMode', ONE_CHECKOUT: 'oneCheckout', CHECKOUT_DELAY: 'checkoutDelay', SECURE_BYPASS: 'secureBypass', MAX_PRICE: 'maxPrice', MIN_PRICE: 'minPrice', PASSWORD: 'password', MONITOR: 'monitor', RETRY: 'retry', SHIPPING_RATE: 'rate', PRODUCT: 'product', PROXIES: 'proxies', ACCOUNT: 'account', CATEGORY: 'category', VARIATION: 'variation', STYLE_ID: 'styleId', STORE: 'store', PROFILE: 'profile', START_TIME: 'startTime', END_TIME: 'endTime', SIZES: 'sizes', ROTATE: 'rotate', QUANTITY: 'quantity', DISCOUNT: 'discount', AMOUNT: 'amount', PAYPAL: 'paypal', CAPTCHA: 'captcha', USE_MOCKS: 'USE_MOCKS', // DEV ONLY MODE: 'mode' }; export function changeDelay( group: string, num: string, field: string, tasks: any[] ) { const delay = Number(num || '0'); if (Number.isNaN(delay)) { return; } ipcRenderer.send(IPCKeys.ChangeDelay, group, delay, field, tasks); return { type: delayActionTypes.CHANGE_DELAY, payload: { field, delay } }; } ================================================ FILE: app/components/Tasks/components/Table/TableData.tsx ================================================ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/styles'; import { Typography, FormControlLabel, Switch, Tooltip } from '@material-ui/core'; import { styles } from './styles'; import { makeSelectedTasksGroup } from '../../selectors'; import { RootState } from '../../../../store/reducers'; import { States } from '../../../../constants'; const useStyles = makeStyles(styles); const TableData = ({ all, toggleAll }: { all: boolean; toggleAll: any }) => { const styles = useStyles(); const tasks = useSelector((state: RootState) => state.Tasks); const selectedTaskGroup: any = useSelector(makeSelectedTasksGroup); let numTasks = 0; let selected = 0; let running = 0; if (all) { numTasks = Object.values(tasks).reduce( (prev, { tasks }) => prev + tasks.length, 0 ); selected = Object.values(tasks).reduce( (prev, { tasks }) => prev + tasks.filter((t: any) => t.selected).length, 0 ); running = Object.values(tasks).reduce( (prev, { tasks }) => prev + tasks.filter((t: any) => t.state === States.Running).length, 0 ); } else if (selectedTaskGroup) { numTasks = selectedTaskGroup.tasks.length; selected = selectedTaskGroup.tasks.filter((t: any) => t.selected).length; running = selectedTaskGroup.tasks.filter( (t: any) => t.state === States.Running ).length; } return useMemo( () => (
{numTasks} Tasks {selected} Selected {running} Running } label={all ? `All groups` : `${selectedTaskGroup?.name || 'None'}`} />
), [running, selected, numTasks, all, selectedTaskGroup?.name, styles] ); }; export default TableData; ================================================ FILE: app/components/Tasks/components/Table/TableWrapper.tsx ================================================ import React, { useCallback, useState } from 'react'; import { makeStyles } from '@material-ui/styles'; import { Table } from '@material-ui/core'; import { styles } from './styles'; import EnhancedTableBody from './components/TableBodyWrapper'; import EnhancedTableHeader from './components/TableHeader'; const useStyles = makeStyles(styles); const TableView = ({ all }: { all: boolean }) => { const styles = useStyles(); const [isRangeSelecting, setIsRangeSelecting] = useState(false); const [order, setOrder] = useState<'asc' | 'desc' | ''>(''); const [orderBy, setOrderBy] = useState(''); const handleRequestSort = useCallback( (property: string) => { const isAsc = orderBy === property && order === 'asc'; const isDesc = orderBy === property && order === 'desc'; if (isAsc) { setOrder('desc'); setOrderBy(property); } else if (isDesc) { setOrder(''); setOrderBy(''); } else { setOrder('asc'); setOrderBy(property); } }, [order, orderBy] ); return (
); }; export default TableView; ================================================ FILE: app/components/Tasks/components/Table/TaskList.tsx ================================================ import React from 'react'; import { makeStyles } from '@material-ui/styles'; import { styles } from './styles'; import TableWrapper from './TableWrapper'; import TableData from './TableData'; import EnhancedTableToolbar from './components/tableToolbar'; const useStyles = makeStyles(styles); const TaskList = ({ all, toggleAll }: { all: boolean; toggleAll: any }) => { const styles = useStyles(); return ( <>
); }; export default TaskList; ================================================ FILE: app/components/Tasks/components/Table/components/TableBodyWrapper.tsx ================================================ import React, { memo, useCallback, useMemo } from 'react'; import memoize from 'memoize-one'; import { useSelector, useDispatch } from 'react-redux'; import { makeStyles } from '@material-ui/styles'; import { TableBody } from '@material-ui/core'; import { FixedSizeList as List, areEqual } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import { styles } from '../styles'; import { selectTasks } from '../../../actions'; import { makeSelectedTasksGroup } from '../../../selectors'; import EnhancedTableRow from './tableRow'; import { useTaskKeyPress } from '../../../useTaskKeyPress'; const Row = memo( ({ data, index, style }: { data: any; index: number; style: any }) => { const { tasks, group, isRangeSelecting, onRowClick } = data; const { id, mode, selected, store, product, sizes, proxies, profile, message, platform, variation, chosenSize, productName, productImage } = tasks[index]; return (
); }, areEqual ); const useStyles = makeStyles(styles); const createItemData = memoize( (tasks, group, isRangeSelecting, onRowClick) => ({ tasks, group, isRangeSelecting, onRowClick }) ); interface TableBodyProps { all: boolean; setIsRangeSelecting: any; order: 'asc' | 'desc' | ''; orderBy: string; isRangeSelecting: boolean; } function comparator(a: any, b: any, orderBy: string) { if (orderBy === 'store' || orderBy === 'profile') { return b[orderBy].name.localeCompare(a[orderBy].name); } if (orderBy === 'proxies') { const first = a[orderBy] ? a[orderBy].name : 'None'; const second = b[orderBy] ? b[orderBy].name : 'None'; if (!/none/i.test(first) && /none/i.test(second)) { return -1; } if (/none/i.test(first) && !/none/i.test(second)) { return 1; } return first.localeCompare(second); } if (orderBy === 'product') { return b[orderBy].raw.localeCompare(a[orderBy].raw); } if (orderBy === 'message') { if (!/idle/i.test(a[orderBy]) && /idle/i.test(b[orderBy])) { return 1; } if (/idle/i.test(a[orderBy]) && !/idle/i.test(b[orderBy])) { return -1; } return b[orderBy].localeCompare(a[orderBy]); } // 1-level deep comparator return b[orderBy].localeCompare(a[orderBy]); } function getComparator( order: 'asc' | 'desc' | '', orderBy: string ): (a: any[], b: any[]) => number { return order === 'desc' ? (a, b) => comparator(a, b, orderBy) : (a, b) => -comparator(a, b, orderBy); } function tableSort(array: any[], comparator: (a: any, b: any) => number) { const stabilizedThis = array.map((el, index) => [el, index] as [any, number]); stabilizedThis.sort((a, b) => { const order = comparator(a[0], b[0]); if (order !== 0) return order; return a[1] - b[1]; }); return stabilizedThis.map(el => el[0]); } const TableBodyWrapper = ({ all, setIsRangeSelecting, order, orderBy, isRangeSelecting }: TableBodyProps) => { const styles = useStyles(); const dispatch = useDispatch(); const selectedTaskGroup: any = useSelector(makeSelectedTasksGroup); const items = useMemo(() => { if (orderBy) { return tableSort(selectedTaskGroup?.tasks, getComparator(order, orderBy)); } return selectedTaskGroup?.tasks; }, [order, orderBy, selectedTaskGroup]); const onRowClick = useCallback( (group: string, id: string, isRangeSelecting: boolean) => { dispatch(selectTasks({ type: 'row', isRangeSelecting, group, id })); }, [isRangeSelecting] ); const itemCount = selectedTaskGroup?.tasks?.length || 0; const itemData = createItemData( items, selectedTaskGroup?.id, isRangeSelecting, onRowClick ); useTaskKeyPress(setIsRangeSelecting, all); return useMemo( () => ( {({ height, width }) => ( {Row} )} ), [itemData, setIsRangeSelecting, isRangeSelecting, styles] ); }; export default TableBodyWrapper; ================================================ FILE: app/components/Tasks/components/Table/components/TableHeader.tsx ================================================ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableHead, TableRow } from '@material-ui/core'; import HeaderCheckbox from './header/checkbox'; import HeaderTaskId from './header/taskId'; import HeaderStore from './header/store'; import HeaderProduct from './header/product'; import HeaderSizes from './header/sizes'; import HeaderProxies from './header/proxies'; import HeaderProfile from './header/profile'; import HeaderStatus from './header/status'; import { selectTasks } from '../../../actions'; import { makeSelectedTasksGroup, makeTasks } from '../../../selectors'; import { styles } from '../styles'; const useStyles = makeStyles(styles); const EnhancedTableHead = ({ all, order, orderBy, onRequestSort }: { all: boolean; order: 'asc' | 'desc' | ''; orderBy: string; onRequestSort: any; }) => { const styles = useStyles(); const dispatch = useDispatch(); const groups = useSelector(makeTasks); const selectedTaskGroup: any = useSelector(makeSelectedTasksGroup); const onSelect = () => { if (all) { const needsSelected = Object.values(groups).find(group => group.tasks.some(t => !t.selected) ); if (needsSelected) { return Object.values(groups).map(({ id: group, tasks }) => { if (tasks.some(t => !t.selected)) { return dispatch(selectTasks({ group })); } return null; }); } return Object.values(groups).map(({ id: group }) => dispatch(selectTasks({ group })) ); } return dispatch(selectTasks({ group: selectedTaskGroup?.id })); }; const createSortHandler = (property: string) => ( event: React.MouseEvent ) => { event.stopPropagation(); onRequestSort(property); }; return ( ); }; export default EnhancedTableHead; ================================================ FILE: app/components/Tasks/components/Table/components/cells/checkbox.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, Checkbox } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); export const rowCheckbox = ({ index, selected }: { index: number; selected: boolean; }) => { const styles = useStyles(); return useMemo( () => ( ), [index, selected, styles] ); }; export default rowCheckbox; ================================================ FILE: app/components/Tasks/components/Table/components/cells/product.tsx ================================================ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; import Image from 'material-ui-image'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { styles } from '../../styles'; import { Platforms } from '../../../../../../constants'; import { RootState } from '../../../../../../store/reducers'; const useStyles = makeStyles(styles); const getProductValue = ({ platform, productName, product, variation }: { platform: string; productName?: string; product: any; variation?: string; }) => { let productValue = productName; if (!productValue) { switch (platform) { case Platforms.Supreme: if (product) { productValue = `${product?.raw} / ${variation}`; } else { productValue = `Malformed product`; } break; default: if (product) { productValue = `${product?.raw}`; } else { productValue = `Malformed product`; } } } return productValue; }; export const rowTaskProduct = ({ id, productImage, platform, product, productName, variation }: { id: string; productImage?: string; platform: string; product: any; productName?: string; variation?: string; }) => { const styles = useStyles(); const productValue = useMemo( () => getProductValue({ platform, product, productName, variation }), [product, productName, variation] ); const enablePerformance = useSelector( (state: RootState) => state.Settings.enablePerformance ); return useMemo( () => ( {!enablePerformance && productImage ? ( ) : null} {productValue} ), [productValue, productImage, styles] ); }; rowTaskProduct.defaultProps = { productImage: null, productName: null, variation: null }; export default rowTaskProduct; ================================================ FILE: app/components/Tasks/components/Table/components/cells/profile.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); export const rowTaskProduct = ({ id, profile }: { id: string; profile: any; }) => { const styles = useStyles(); return useMemo( () => ( {profile ? profile.name : 'None'} ), [profile, styles] ); }; export default rowTaskProduct; ================================================ FILE: app/components/Tasks/components/Table/components/cells/proxies.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); export const rowTaskProxies = ({ id, proxies }: { id: string; proxies: any; }) => { const styles = useStyles(); return useMemo( () => ( {proxies ? proxies.name : 'None'} ), [proxies, styles] ); }; export default rowTaskProxies; ================================================ FILE: app/components/Tasks/components/Table/components/cells/sizes.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const makeSizes = (sizes: any[]) => sizes.length > 1 ? `${sizes.join(', ')}` : sizes[0]; export const rowTaskSizes = ({ id, chosenSize, sizes }: { id: string; chosenSize?: | string | { name?: string; }; sizes: string[]; }) => { const styles = useStyles(); const sizesValue = makeSizes(sizes); return useMemo( () => ( {chosenSize && typeof chosenSize === 'object' ? chosenSize.name : chosenSize || sizesValue} ), [chosenSize, sizesValue, styles] ); }; rowTaskSizes.defaultProps = { chosenSize: '' }; export default rowTaskSizes; ================================================ FILE: app/components/Tasks/components/Table/components/cells/status.tsx ================================================ import React from 'react'; import { ipcRenderer } from 'electron'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { IPCKeys } from '../../../../../../constants/ipc'; import { failedStatuses, warningStatuses, neutralStatuses, successStatuses, Platforms } from '../../../../../../constants'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const failedRegex = new RegExp(failedStatuses.join('|'), 'i'); const warningRegex = new RegExp(warningStatuses.join('|'), 'i'); const neutralRegex = new RegExp(neutralStatuses.join('|'), 'i'); const successRegex = new RegExp(successStatuses.join('|'), 'i'); const getMessageAction = ( id: string, group: string, platform: string, message: string ) => { if (platform === Platforms.YeezySupply) { if (/passed/i.test(message)) { // return a function handler that launches the browser onClick return (e: Event) => { // prevent the handler from deselecting the task row e.stopPropagation(); ipcRenderer.send(IPCKeys.LaunchBrowser, { group, id }); }; } } // return a "noop" function handler return () => {}; }; const getMessageClassName = (message: string) => { if (failedRegex.test(message)) { return 'failed'; } if (warningRegex.test(message)) { return 'warning'; } if (neutralRegex.test(message)) { return 'purple'; } if (successRegex.test(message)) { return 'success'; } return 'normal'; }; export const rowTaskStatus = ({ id, group, platform, message }: { id: string; group: string; platform: string; message: string; }) => { const styles = useStyles(); const messageAction: any = getMessageAction(id, group, platform, message); const messageClassName: string = getMessageClassName(message); return ( {message} ); }; export default rowTaskStatus; ================================================ FILE: app/components/Tasks/components/Table/components/cells/store.tsx ================================================ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { MODE_ICONS } from '../icons'; import { styles } from '../../styles'; import { RootState } from '../../../../../../store/reducers'; const useStyles = makeStyles(styles); const iconForMode = (mode: string, theme: number, classes: any) => MODE_ICONS[mode] ? MODE_ICONS[mode](classes, theme) : null; export const rowTaskStore = ({ id, mode, store }: { id: string; mode: string; store: any; }) => { const styles = useStyles(); const theme = useSelector((state: RootState) => state.Theme); const storeValue = store ? store.name : 'Invalid Store'; return useMemo( () => ( {mode ? iconForMode(mode, theme, styles.modeIcon) : null} {storeValue} ), [mode, theme, storeValue, styles] ); }; export default rowTaskStore; ================================================ FILE: app/components/Tasks/components/Table/components/cells/taskId.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); export const rowTaskId = ({ id }: { id: string }) => { const styles = useStyles(); return useMemo( () => ( {id} ), [styles] ); }; export default rowTaskId; ================================================ FILE: app/components/Tasks/components/Table/components/header/checkbox.tsx ================================================ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, Checkbox } from '@material-ui/core'; import { styles } from '../../styles'; import { makeSelectedTasksGroup } from '../../../../selectors'; const useStyles = makeStyles(styles); export const headerCheckbox = () => { const styles = useStyles(); const selectedTaskGroup: any = useSelector(makeSelectedTasksGroup); const numTasks = useMemo(() => { if (selectedTaskGroup) { return selectedTaskGroup.tasks.length; } return 0; }, [selectedTaskGroup?.id, selectedTaskGroup?.tasks]); const selected = useMemo(() => { if (selectedTaskGroup) { return selectedTaskGroup.tasks.filter(t => t.selected).length; } return 0; }, [selectedTaskGroup?.id, selectedTaskGroup?.tasks]); return useMemo( () => ( 0 && selected < numTasks} checked={selected > 0 && selected === numTasks} /> ), [selected, numTasks, styles] ); }; export default headerCheckbox; ================================================ FILE: app/components/Tasks/components/Table/components/header/product.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, TableSortLabel } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const headerProduct = ({ order, orderBy, createSortHandler }: { order: 'asc' | 'desc' | ''; orderBy: string; createSortHandler: any; }) => { const styles = useStyles(); return useMemo( () => ( Product ), [order, orderBy, styles] ); }; export default headerProduct; ================================================ FILE: app/components/Tasks/components/Table/components/header/profile.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, TableSortLabel } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const headerProfile = ({ order, orderBy, createSortHandler }: { order: 'asc' | 'desc' | ''; orderBy: string; createSortHandler: any; }) => { const styles = useStyles(); return useMemo( () => ( Profile ), [order, orderBy, styles] ); }; export default headerProfile; ================================================ FILE: app/components/Tasks/components/Table/components/header/proxies.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, TableSortLabel } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const headerProxies = ({ order, orderBy, createSortHandler }: { order: 'asc' | 'desc' | ''; orderBy: string; createSortHandler: any; }) => { const styles = useStyles(); return useMemo( () => ( Proxies ), [order, orderBy, styles] ); }; export default headerProxies; ================================================ FILE: app/components/Tasks/components/Table/components/header/sizes.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const headerSizes = () => { const styles = useStyles(); return useMemo( () => ( Sizes ), [styles] ); }; export default headerSizes; ================================================ FILE: app/components/Tasks/components/Table/components/header/status.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, TableSortLabel } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const headerStatus = ({ order, orderBy, createSortHandler }: { order: 'asc' | 'desc' | ''; orderBy: string; createSortHandler: any; }) => { const styles = useStyles(); return useMemo( () => ( Status ), [order, orderBy, styles] ); }; export default headerStatus; ================================================ FILE: app/components/Tasks/components/Table/components/header/store.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, TableSortLabel } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); const headerStore = ({ order, orderBy, createSortHandler }: { order: 'asc' | 'desc' | ''; orderBy: string; createSortHandler: any; }) => { const styles = useStyles(); return useMemo( () => ( Store ), [order, orderBy, styles] ); }; export default headerStore; ================================================ FILE: app/components/Tasks/components/Table/components/header/taskId.tsx ================================================ import React, { useMemo } from 'react'; import classnames from 'classnames'; import { makeStyles } from '@material-ui/styles'; import { TableCell, TableSortLabel } from '@material-ui/core'; import { styles } from '../../styles'; const useStyles = makeStyles(styles); export const headerTaskId = ({ order, orderBy, createSortHandler }: { order: 'asc' | 'desc' | ''; orderBy: string; createSortHandler: any; }) => { const styles = useStyles(); return useMemo( () => ( Task ), [order, orderBy, styles] ); }; export default headerTaskId; ================================================ FILE: app/components/Tasks/components/Table/components/icons.tsx ================================================ import React from 'react'; import EcoIcon from '@material-ui/icons/Eco'; import SafeModeIcon from '@material-ui/icons/VerifiedUser'; import FastModeIcon from '@material-ui/icons/FlashOn'; import BrowserModeIcon from '@material-ui/icons/Language'; import RestockModeIcon from '@material-ui/icons/Autorenew'; import DirectionsIcon from '@material-ui/icons/Directions'; import BookmarkIcon from '@material-ui/icons/Bookmark'; import { imgsrc } from '../../../../../utils/imgsrc'; export const MODE_ICONS: any = { SAFE: (classes: any) => , FAST: (classes: any) => , HYBRID: (classes: any) => , REQUEST: (classes: any) => , SPLASH: (classes: any) => , BROWSER: (classes: any) => , RESTOCK: (classes: any) => , PFUTILE: (classes: any) => , PRELOAD: (classes: any) => , NORMAL: (classes: any) => , RELEASE: (classes: any) => , MOBILE: (classes: any) => , PAYPAL: (classes: any, theme: number) => { if (theme === 1) { return ( ); } return ; } }; ================================================ FILE: app/components/Tasks/components/Table/components/tableRow.tsx ================================================ import React, { useMemo } from 'react'; import { makeStyles } from '@material-ui/styles'; import { TableRow } from '@material-ui/core'; import { styles } from '../styles'; import RowCheckbox from './cells/checkbox'; import RowTaskId from './cells/taskId'; import RowTaskStore from './cells/store'; import RowTaskProduct from './cells/product'; import RowTaskSizes from './cells/sizes'; import RowTaskProxies from './cells/proxies'; import RowTaskProfile from './cells/profile'; import RowTaskStatus from './cells/status'; const useStyles = makeStyles(styles); type Props = { id: string; mode: string; selected: boolean; store: any; product: any; sizes: any[]; proxies: any; profile: any; message: string; platform: string; // eslint-disable-next-line variation?: string; // eslint-disable-next-line chosenSize?: | string | { id: number | string; name: string; }; // eslint-disable-next-line productName?: string; // eslint-disable-next-line productImage?: string; isRangeSelecting: boolean; group: string; onRowClick: Function; index: number; }; const TaskTableRow = (props: Props) => { const { id, mode, selected, store, product, sizes, proxies, profile, message, platform, variation, chosenSize, productName, productImage, isRangeSelecting, group, onRowClick, index } = props as any; const styles = useStyles(); return useMemo( () => ( onRowClick(group, id, isRangeSelecting)} role="checkbox" component="div" aria-checked={selected} key={`{task}--${id}`} selected={selected} className={styles.displayFlex} > ), [ mode, selected, store, product, sizes, proxies, profile, message, platform, variation, chosenSize, productName, productImage, onRowClick, styles ] ); }; export default TaskTableRow; ================================================ FILE: app/components/Tasks/components/Table/components/tableToolbar.tsx ================================================ import React, { useCallback, useMemo, useState } from 'react'; import { ipcRenderer } from 'electron'; import { makeStyles } from '@material-ui/styles'; import classnames from 'classnames'; import { Toolbar, Button } from '@material-ui/core'; import { styles } from '../styles/tableToolbar'; import TaskGroups from './toolbar/groups'; import MonitorDelay from './toolbar/monitor'; import RetryDelay from './toolbar/retry'; import StaggerAmount from './toolbar/stagger'; import Clock from './toolbar/clock'; import { IPCKeys } from '../../../../../constants/ipc'; const useStyles = makeStyles(styles); const EnhancedTableToolbar = ({ all }: { all: boolean }) => { const styles = useStyles(); const [isPurging, setIsPurging] = useState(false); const [purgeText, setPurgeText] = useState('Purge Cache'); const handlePurge = useCallback(() => { if (!isPurging) { setIsPurging(true); ipcRenderer .invoke(IPCKeys.PurgeAnswers) .then(() => { setPurgeText('Purged'); setTimeout(() => { setPurgeText('Purge Cache'); setIsPurging(false); }, 750); return true; }) .catch(() => { setPurgeText('Error'); setTimeout(() => { setPurgeText('Purge Cache'); setIsPurging(false); }, 750); }); } }, [isPurging, purgeText]); return useMemo( () => ( ), [all, purgeText, styles] ); }; export default EnhancedTableToolbar; ================================================ FILE: app/components/Tasks/components/Table/components/toolbar/clock.tsx ================================================ import React from 'react'; import { useTime } from 'react-timer-hook'; import { makeStyles } from '@material-ui/styles'; import { Typography } from '@material-ui/core'; import { styles } from '../../styles/tableToolbar'; const useStyles = makeStyles(styles); function Clock() { const styles = useStyles(); const { seconds, minutes, hours, ampm } = useTime({ format: '12-hour' }); const hh = hours > 9 ? hours : `0${hours}`; const mm = minutes > 9 ? minutes : `0${minutes}`; const ss = seconds > 9 ? seconds : `0${seconds}`; return ( <> Time:
{hh}:{mm}:{ss} {ampm}
); } export default Clock; ================================================ FILE: app/components/Tasks/components/Table/components/toolbar/filter.tsx ================================================ import React, { useCallback } from 'react'; import Select from 'react-select'; import { useDispatch, useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/styles'; import { Typography } from '@material-ui/core'; import { styles } from '../../styles/tableToolbar'; import { setField, SETTINGS_FIELDS } from '../../../../../Settings/actions'; import { RootState } from '../../../../../../store/reducers'; import { basicStyles, IndicatorSeparator } from '../../../../../../styles/select'; import { Groups } from '../../../../../../constants'; const useStyles = makeStyles(styles); const groupByOptions = [ { label: 'None', value: Groups.None }, { label: 'Store', value: Groups.Store }, { label: 'Product', value: Groups.Product }, { label: 'Profile', value: Groups.Profile } ]; const Filter = () => { const styles = useStyles(); const dispatch = useDispatch(); const theme = useSelector((state: RootState) => state.Theme); const groupBy = useSelector( ({ Settings }: { Settings: any }) => Settings.tasksGroup ); const handleSetTaskGroup = useCallback((event: any) => { dispatch(setField(SETTINGS_FIELDS.TASKS_GROUP, event.value)); }, []); return ( <> Group By: `; window.loadURL(`data:text/html,${encodeURIComponent(threeDSecureHtml)}`); }; } ================================================ FILE: app/tasks/managers/cache/cache.ts ================================================ import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron'; import { IPCKeys } from '../../../constants/ipc'; import { createCacheWindow, WindowStates } from './windows'; import { PATHS } from '../../../utils/paths'; type Products = { [id: string]: any; }; type Window = { id: number; window: BrowserWindow | null; state: string; }; export class CacheManager { mainWindow: BrowserWindow | null; window: Window; products: Products; constructor(mainWindow: BrowserWindow | null) { this.mainWindow = mainWindow; this.window = { id: 0, state: WindowStates.CLOSE, window: null }; this.products = {}; ipcMain.handle(IPCKeys.LaunchCacheWindow, this.launch); } launch = (_: IpcMainInvokeEvent) => { if (this.window.window) { const { window } = this.window; window.show(); window.focus(); return; } this.window.state = WindowStates.LOAD; const window = createCacheWindow(); this.window.window = window; this.window.id = window.id; window.loadURL(`${PATHS.cacheUrlPath}`); window.once('ready-to-show', () => { window.show(); this.window.state = WindowStates.READY; }); }; } ================================================ FILE: app/tasks/managers/cache/windows.ts ================================================ import { BrowserWindow } from 'electron'; export const createCacheWindow = () => { return new BrowserWindow({ backgroundColor: '#202126', center: true, fullscreenable: false, movable: true, show: false, width: 800, height: 800, frame: false, transparent: true, resizable: true, webPreferences: { webSecurity: true, nodeIntegration: true, backgroundThrottling: true } }); }; export const WindowStates = { LOAD: 'load', READY: 'ready', SOLVING: 'solving', CLOSE: 'close' }; ================================================ FILE: app/tasks/managers/captcha/autoSolve.ts ================================================ import { Platforms } from '../../../constants'; import CAPTCHA_TYPES from '../../../utils/captchaTypes'; export const versionForType = (type: string) => { switch (type) { default: case CAPTCHA_TYPES.RECAPTCHA_V2: case CAPTCHA_TYPES.RECAPTCHA_V2C: return 0; // v2 checkbox case CAPTCHA_TYPES.RECAPTCHA_V2I: return 1; // v2 invisible case CAPTCHA_TYPES.RECAPTCHA_V3: return 2; // v3 / enterprise } }; export const requestAutoSolve = async ({ autoSolve, id, platform, url, proxy, siteKey, version, action, renderParameters }: AutoSolveProps) => { if (proxy) { return autoSolve.sendTokenRequest({ taskId: `${id}::${platform}`, url, siteKey, version, proxy, proxyRequired: platform === Platforms.Shopify, // NOTE: To prevent Cap Monster cancels action, renderParameters }); } return autoSolve.sendTokenRequest({ taskId: `${id}::${platform}`, url, siteKey, version, proxyRequired: false, action, renderParameters }); }; export const cancelAutoSolve = async ({ id, platform, autoSolve }: { id: string; platform: string; autoSolve: any; }) => { try { await autoSolve.cancelTokenRequest(`${id}::${platform}`); } catch (err) { // TODO: Proper error handling } }; type PlatformForUrl = { [url: string]: string; }; export const platformForUrl: PlatformForUrl = { 'https://checkout.shopify.com': Platforms.Shopify, 'https://www.supremenewyork.com': Platforms.Supreme, 'https://www.yeezysupply.com': Platforms.YeezySupply, 'https://geo.captcha-delivery.com/': Platforms.Footsites, 'https://geo.captcha-delivery.com?pokemon': Platforms.Pokemon }; ================================================ FILE: app/tasks/managers/captcha/captcha.ts ================================================ /* eslint-disable no-restricted-syntax */ import { session, BrowserWindow, ipcMain, IpcMainInvokeEvent, AuthenticationResponseDetails, AuthInfo, IpcMainEvent, Cookie } from 'electron'; import uuid from 'uuid'; import AutoSolve from 'autosolve-client'; import { isEmpty } from 'lodash'; import captchaTypes from '../../../utils/captchaTypes'; import { Platforms } from '../../../constants'; import { requestAutoSolve, cancelAutoSolve, versionForType } from './autoSolve'; import { loadGoogle, loadHost, sleep, intercept, setProxy, HarvestStates, WindowStates, attachRequester, detachRequester } from '../utils'; import { createCaptchaWindow, closeAll } from './windows'; import { IPCKeys } from '../../../constants/ipc'; import { Youtube } from './youtube'; import { format } from '../../../utils/proxy'; import { NotificationManager } from '..'; type AutoSolveProps = { apiKey: string; accessToken: string; }; type CaptchaDetailProps = { id: string; name: string; store: string; proxy: string; type: string; platform: string; theme: number; }; export type CaptchaProps = { id: string; // frontend ID guid: string; // backend ID name: string; // frontend name proxy?: string; // frontend proxy lastUsed: number; state: string; host: string; type: string; platform: string; theme: number; task: null | string; // currently solving task id window: BrowserWindow; }; export type InterceptProps = { listener: any; window: BrowserWindow; id: string; guid: string; platform: string; store: string; name: string; proxy?: string; type: string; theme: number; }; export type CaptchaWindow = { [id: string]: CaptchaProps; }; export type CaptchaWindows = { [platform: string]: CaptchaWindow; }; export type CaptchaRequester = { id: string; type: string; initialCid?: string; hash?: string; cid?: string; t?: string; sitekey: string; platform: string; host: string; state?: string; // state of the requester harvest: ({ token, form, body, cookies, timestamp }: { token?: string; form?: string; body?: string; cookies?: Cookie[]; timestamp?: number; }) => void; userAgent?: string; checkpoint?: boolean; // shopify proxy?: string; // currently only used for shopify s?: string; // shopify cookies?: Cookie[]; action?: string; // yeezysupply sharing?: boolean; // yeezysupply expiration?: number; // yeezysupply }; export type CaptchaRequesters = { [platform: string]: { [id: string]: CaptchaRequester; }; }; type CaptchaIntervals = { [platform: string]: number; }; type TokenQueue = { [platform: string]: { id: string; token: string; timestamp: number; }; }; export class CaptchaManager { mainWindow: BrowserWindow | null; notificationManager: NotificationManager; windows: CaptchaWindows; requesters: CaptchaRequesters; intervals: CaptchaIntervals; intervalRate: number; autoSolve: AutoSolve | null; youtubeManager: Youtube; tokens: TokenQueue; captchaSemaphore: string; constructor( mainWindow: BrowserWindow | null, notificationManager: NotificationManager ) { this.mainWindow = mainWindow; this.notificationManager = notificationManager; this.windows = {}; this.requesters = {}; this.intervals = {}; this.intervalRate = 250; // .25 of a second poll for new requesters this.autoSolve = null; this.tokens = {}; this.youtubeManager = new Youtube(mainWindow, this); this.captchaSemaphore = ''; // handlers ipcMain.handle(IPCKeys.SetupAutoSolve, this.setupAutoSolve); ipcMain.handle(IPCKeys.LaunchHarvester, this.launch); ipcMain.handle(IPCKeys.CancelLaunchHarvester, this.cancel); ipcMain.handle(IPCKeys.CloseHarvesterWindows, this.close); ipcMain.on(IPCKeys.UpdateHarvester, this.update); ipcMain.on(IPCKeys.UpdateHCaptchaToken, this.saveHcaptchaToken); ipcMain.on(IPCKeys.UpdateTheme, this.theme); ipcMain.on(IPCKeys.HarvestCaptcha, this.harvest); } /** * Sends solve requests for idle requesters to idle windows * @param platform - Platform to check solve requests for */ startInterval = (platform: string) => { return setInterval(async () => { const requesters = Object.values(this.requesters[platform]).filter( ({ state }) => state === HarvestStates.IDLE || state === HarvestStates.AUTOSOLVE ); // eslint-disable-next-line no-restricted-syntax for (const requester of requesters) { const { id, type, host, sitekey, platform, proxy, state, s, action } = requester; if (!this.windows[platform]) { this.windows[platform] = {}; } const queue = Object.values(this.windows[platform]) .filter( ({ state, task, type: _type }) => state === WindowStates.READY && !task && (platform !== Platforms.Shopify || (platform === Platforms.Shopify && ((_type === 'Checkout' && type === captchaTypes.RECAPTCHA_V2) || (_type === 'Checkpoint' && type === captchaTypes.RECAPTCHA_V2C) || (_type === 'Login' && type === captchaTypes.RECAPTCHA_V3)))) ) .sort((a, b) => (a.lastUsed > b.lastUsed ? 1 : -1)); const window = (queue || [])[0]; if (window) { window.lastUsed = Date.now(); } const version = versionForType(type); if (this.autoSolve && state !== HarvestStates.AUTOSOLVE) { requester.state = HarvestStates.AUTOSOLVE; const renderParameters = s ? { s } : {}; if (platform === Platforms.YeezySupply) { if (!this.captchaSemaphore) { this.captchaSemaphore = id; } requestAutoSolve({ autoSolve: this.autoSolve, id, platform, siteKey: sitekey, version: `${version}`, action, proxy, renderParameters, url: host }); } else { requestAutoSolve({ autoSolve: this.autoSolve, id, platform, siteKey: sitekey, version: `${version}`, action, proxy, renderParameters, url: host }); } } if (window) { window.state = WindowStates.SOLVING; requester.state = HarvestStates.START; attachRequester({ entry: window, version, requester, window: window.window, isCheckpoint: window.type === 'Checkpoint', needsFocus: this.mainWindow?.isFocused() || false, remove: this.remove }); } } }, this.intervalRate); }; stopInterval = (platform: string) => { clearInterval(this.intervals[platform]); delete this.intervals[platform]; }; /** * Attaches event listeners to the browser object to * help provide basic harvester functionality. * * @param window - BrowserWindow to attach to */ attachHandlers = (guid: string, window: BrowserWindow, platform: string) => { window.hide(); // remove the window from list of windows before closing window.on('close', () => { this.windows[platform][guid].state = WindowStates.CLOSE; const { task } = this.windows[platform][guid]; // if we had a requester while closing, unassign it from the window if (task) { const requester = this.requesters[platform][task]; if (requester) { requester.state = HarvestStates.IDLE; } } delete this.windows[platform][guid]; }); const authHandler = ( event: Event, _: AuthenticationResponseDetails, authInfo: AuthInfo, callback: Function ) => { event.preventDefault(); const { proxy } = this.windows[platform][guid]; if (authInfo.isProxy && proxy) { const formatted = format(proxy); if (formatted) { const [, , username, password] = formatted; callback(username, password); } } }; // handle proxy authentication for this window window.webContents.on('login', authHandler); }; /** * Inserts a requester into it's respective mapping by platform * * Note: This method also kicks off the requester interval if it * wasn't start already. */ insert = async ({ id, type, initialCid, hash, cid, sitekey, platform, harvest, host, proxy, t, s, userAgent, cookies, action, sharing, expiration, state = HarvestStates.IDLE }: CaptchaRequester) => { // if we're sharing, return the token if (sharing && this.tokens[platform]) { const { timestamp, token } = this.tokens[platform]; return harvest({ token, timestamp }); } // or if the stored token corresponds to the same task, return it if (this.tokens[platform] && this.tokens[platform].id === id) { const { timestamp, token } = this.tokens[platform]; return harvest({ token, timestamp }); } if (this.autoSolve) { this.notificationManager.insert({ id, message: `Task ${id}: Sent to AutoSolve`, variant: 'info' }); } this.notificationManager.insert({ id, message: `Task ${id}: ${type}`, variant: 'warning', type: 'HEADS_UP' }); if (!this.requesters[platform]) { this.requesters[platform] = {}; } if (this.requesters[platform][id]) { return; } this.requesters[platform][id] = { id, type, initialCid, hash, cid, t, sitekey, platform, harvest, host, state, proxy, cookies, userAgent, s, action, sharing, expiration }; if (!this.intervals[platform]) { this.intervals[platform] = this.startInterval(platform); } return null; }; /** * Removes a requester from the mapping of requesters and assignment * to any captcha windows * * Note: This method will also stop the requester interval if there * aren't anymore requesters for that platform. */ remove = async ({ id, platform }: { id: string; platform: string }) => { if (!this.requesters[platform]) { return; } delete this.requesters[platform][id]; if (this.autoSolve) { cancelAutoSolve({ id, platform, autoSolve: this.autoSolve }); } const windows = Object.values(this.windows[platform]); // eslint-disable-next-line no-restricted-syntax for (const entry of windows) { if (entry.task === id) { detachRequester({ requesters: this.requesters, platform, entry }); break; } } // focus next window const nextToFocus = windows.find(({ task }) => task); if (nextToFocus && !nextToFocus.window.isFocused()) { nextToFocus.window.show(); nextToFocus.window.focus(); } if (isEmpty(this.requesters[platform])) { this.stopInterval(platform); } }; /** * Handles harvesting the captcha token and sending it to the proper task */ harvest = async ( _: IpcMainEvent | null, // null if called from AutoSolve { id, platform, token, timestamp }: { id: string; platform: string; token: string; timestamp: number; } ) => { if (!this.requesters[platform]) { return; } const { sharing } = this.requesters[platform][id]; // store the token in the token cache if (platform === Platforms.YeezySupply) { this.tokens[platform] = { id, token, timestamp }; // remove the token in (expiration || 115s) setTimeout(() => { if (this.captchaSemaphore) { this.captchaSemaphore = ''; } delete this.tokens[platform]; }, 110000); } // if we're sharing tokens, let's harvest for all shared requesters.. if (sharing) { return Promise.all( Object.values(this.requesters[platform]).map( (requester: CaptchaRequester) => { if (requester.sharing) { requester.harvest({ token, timestamp }); if (this.autoSolve && requester.id === this.captchaSemaphore) { return this.remove({ id: requester.id, platform }); } return this.remove({ id: requester.id, platform }); } return null; } ) ); } const requester = this.requesters[platform][id]; requester.harvest({ token, timestamp }); return this.remove({ id, platform }); }; cancel = async (_: IpcMainInvokeEvent, { id }: CaptchaDetailProps) => { for (const platform of Object.keys(this.windows)) { if (!this.windows[platform]) { this.windows[platform] = {}; } for (const window of Object.values(this.windows[platform])) { const { id: _id, guid, window: _window, state } = window; if (_id === id && state === WindowStates.LOAD) { _window.close(); delete this.windows[platform][guid]; } } } return true; }; /** * Launches a captcha window and stores it in the map of windows */ launch = async ( _: IpcMainInvokeEvent, { id, name, store, proxy, type, platform, theme }: CaptchaDetailProps ) => { if (!this.windows[platform]) { this.windows[platform] = {}; } // make sure we close the youtube window associated with the harvester const youtube = this.youtubeManager.windows[id]; if (youtube) { youtube.window.close(); } const guid = uuid(); const window = createCaptchaWindow(id, theme, type); this.windows[platform][guid] = { id, guid, name, host: store, platform, lastUsed: 0, state: WindowStates.LOAD, task: null, theme, type, window, proxy }; this.attachHandlers(guid, window, platform); await setProxy({ window, proxy }); const listener = (_: any, __: any, errorDescription: string) => { this.notificationManager.notify({ message: `Failed to load harvester [${errorDescription}]`, variant: 'error', force: true }); return window.close(); }; window.webContents.once('did-fail-load', listener); await loadGoogle(window); await sleep(Math.floor(Math.random() * 1750) + 1250); return this.intercept({ listener, window, id, guid, platform, store, name, proxy, type, theme }); }; intercept = async ({ listener, window, guid, platform, store, name, type, theme }: InterceptProps) => { // intercept and load the proper host finally.. intercept(window); await loadHost(window, store); window.webContents.send(IPCKeys.HarvesterData, { name, type, platform, theme }); window.show(); window.webContents.removeListener('did-fail-load', listener); this.windows[platform][guid].state = WindowStates.READY; return true; }; /** * Invoked by a user deleting the harvester from the frontend. * This method will close either A) the harvester window, or B) * the Youtube window (whichever is open at the time). */ close = async (_: IpcMainInvokeEvent, { id }: CaptchaDetailProps) => { const youtube = this.youtubeManager.windows[id]; if (youtube) { const { window } = youtube; window.close(); } for (const platform of Object.keys(this.windows)) { if (!this.windows[platform]) { this.windows[platform] = {}; } for (const window of Object.values(this.windows[platform])) { if (window.id === id) { window.window.close(); } } } return true; }; closeAllWindows = () => { return Promise.all( Object.values(this.windows).map(windows => closeAll(Object.values(windows)) ) ); }; theme = async (_: IpcMainEvent, { theme }: CaptchaDetailProps) => { for (const platform of Object.keys(this.windows)) { if (!this.windows[platform]) { this.windows[platform] = {}; } for (const window of Object.values(this.windows[platform])) { window.window.webContents.send(IPCKeys.HarvesterData, { theme }); } } return true; }; /** * Updates a previously launched harvester with new information */ update = async ( _: IpcMainEvent, { id, name, proxy, type, theme }: CaptchaDetailProps ) => { for (const platform of Object.keys(this.windows)) { if (!this.windows[platform]) { this.windows[platform] = {}; } for (const window of Object.values(this.windows[platform])) { if (window.id === id) { if (window.proxy !== proxy) { // eslint-disable-next-line no-await-in-loop await setProxy({ window: window.window, proxy }); } this.windows[platform][id] = { ...window, id, name, proxy, lastUsed: Date.now(), type }; window.window.webContents.send(IPCKeys.HarvesterData, { name, type, theme }); } } } return true; }; saveHcaptchaToken = async ( _: IpcMainEvent, { id, token }: { id: string; token: string } ) => { const sess = session.fromPartition(`persist:${id}`); if (!token) { return sess.cookies.remove( 'https://www.hcaptcha.com', 'hc_accessibility' ); } return sess.cookies.set({ url: 'https://www.hcaptcha.com', domain: '.hcaptcha.com', name: 'hc_accessibility', value: token, secure: true }); }; /** * Allows you to swap to and from autosolve / bot solvers */ unassignRequesters = (autoSolve = false) => { // eslint-disable-next-line no-restricted-syntax for (const platform of Object.keys(this.requesters)) { // eslint-disable-next-line no-restricted-syntax for (const requester of Object.values(this.requesters[platform])) { if (!autoSolve) { if (requester.state === HarvestStates.AUTOSOLVE) { requester.state = HarvestStates.IDLE; } } else if (requester.state === HarvestStates.START) { requester.state = HarvestStates.IDLE; const window = Object.values(this.windows[requester.platform]).find( ({ task }) => task === requester.id ); if (window) { detachRequester({ requesters: this.requesters, platform, entry: window }); } } } } }; setupAutoSolve = async ( _: IpcMainInvokeEvent, { apiKey, accessToken }: AutoSolveProps ) => { if (!apiKey || !accessToken) { if (this.autoSolve) { this.autoSolve.cancelAllRequests(); this.autoSolve = null; } this.unassignRequesters(); return { success: false }; } this.autoSolve = AutoSolve.getInstance({ accessToken, apiKey, clientKey: '', shouldAlertOnCancel: true, debug: true }); if (!this.autoSolve) { this.autoSolve = null; return { success: false }; } return this.autoSolve .init() .then(() => { if (this.autoSolve) { this.unassignRequesters(true); this.autoSolve.ee.on( `AutoSolveResponse`, async (data: AutoSolveResponseProps) => { const { taskId, token, createdAt } = JSON.parse(data); const [id, platform] = taskId.split('::'); this.harvest(null, { id, platform, timestamp: Math.floor((createdAt * 1000 + 120000) / 1000), token }); } ); } return { success: true }; }) .catch((error: string) => { this.autoSolve = null; if (typeof error === 'string') { return { error }; } return { error: 'Unknown AutoSolve Error' }; }); }; } ================================================ FILE: app/tasks/managers/captcha/windows.ts ================================================ import { BrowserWindow, session } from 'electron'; import { join } from 'path'; import { IS_DEV } from '../../../constants/env'; import { PATHS } from '../../../utils/paths'; export const createYoutubeWindow = (id: string) => { return new BrowserWindow({ center: true, transparent: false, fullscreenable: false, movable: true, show: false, width: 450, height: 600, frame: true, resizable: true, webPreferences: { webSecurity: true, devTools: IS_DEV, nodeIntegration: false, backgroundThrottling: true, session: session.fromPartition(`persist:${id}`) } }); }; export const createCaptchaWindow = ( id: string, theme: number, type: string ) => { const preload = IS_DEV ? join(PATHS.preload, 'harvester.js') : join(__dirname, 'dist/harvester.prod.js'); return new BrowserWindow({ backgroundColor: theme === 0 ? '#f4f4f4' : '#202126', width: 400, height: 555, minWidth: 400, minHeight: 555, resizable: true, fullscreenable: false, useContentSize: true, show: false, transparent: type !== 'Checkpoint', frame: type === 'Checkpoint', titleBarStyle: 'default', acceptFirstMouse: true, webPreferences: { contextIsolation: false, backgroundThrottling: true, devTools: IS_DEV, webSecurity: false, plugins: true, session: session.fromPartition(`persist:${id}`), preload } }); }; export const closeAll = async (windows: any[]) => { return Promise.all(windows.map(({ window }) => window?.close())); }; ================================================ FILE: app/tasks/managers/captcha/youtube.ts ================================================ import { BrowserWindow, IpcMainInvokeEvent, ipcMain, AuthInfo, AuthenticationResponseDetails } from 'electron'; import { IPCKeys } from '../../../constants/ipc'; import { createYoutubeWindow, closeAll } from './windows'; import { setProxy, loadHost } from '../utils'; import { format } from '../../../utils/proxy'; import { CaptchaManager } from './captcha'; export type YoutubeWindow = { id: string; // frontend ID proxy?: string; // frontend proxy window: BrowserWindow; }; export type YoutubeWindows = { [id: string]: YoutubeWindow; }; type LaunchProps = { id: string; platform: string; proxy?: string; }; export class Youtube { mainWindow: BrowserWindow | null; captchaManager: CaptchaManager; windows: YoutubeWindows; constructor( mainWindow: BrowserWindow | null, captchaManager: CaptchaManager ) { this.mainWindow = mainWindow; this.captchaManager = captchaManager; this.windows = {}; ipcMain.handle(IPCKeys.LaunchYoutube, this.launch); ipcMain.handle(IPCKeys.CancelLaunchYouTube, this.cancel); } /** * Attaches event listeners to the browser object to * help provide basic harvester functionality. * * @param window - BrowserWindow to attach to */ attachHandlers = (id: string, window: BrowserWindow) => { // remove the window from list of windows before closing window.on('close', () => { delete this.windows[id]; }); const authHandler = ( event: Event, _: AuthenticationResponseDetails, authInfo: AuthInfo, callback: Function ) => { event.preventDefault(); const { proxy } = this.windows[id]; if (authInfo.isProxy && proxy) { const formatted = format(proxy); if (formatted) { const [, , username, password] = formatted; callback(username, password); } } }; // handle proxy authentication for this window window.webContents.on('login', authHandler); }; closeAllWindows = () => closeAll(Object.values(this.windows)); cancel = async (_: IpcMainInvokeEvent, { id }: LaunchProps) => { const launched = this.windows[id]; if (launched) { launched.window.close(); delete this.windows[id]; } return true; }; launch = async ( _: IpcMainInvokeEvent, { id, platform, proxy }: LaunchProps ) => { if (this.captchaManager.windows[platform]) { const captcha = this.captchaManager.windows[platform][id]; if (captcha) { const { window } = captcha; window.close(); } } const launched = this.windows[id]; if (launched) { launched.window.show(); launched.window.focus(); return; } const window = createYoutubeWindow(id); this.windows[id] = { id, proxy, window }; this.attachHandlers(id, window); await setProxy({ window, proxy }); const listener = (_: any, __: any, errorDescription: string) => { this.captchaManager.notificationManager.notify({ message: `Failed to load YouTube [${errorDescription}]`, variant: 'error', force: true }); return window.close(); }; window.webContents.once('did-fail-load', listener); await loadHost( window, 'https://accounts.google.com/signin/v2/identifier?service=youtube&uilel=3&passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26hl%3Den%26next%3D%252F&hl=en&ec=65620&flowName=GlifWebSignIn&flowEntry=ServiceLogin', { userAgent: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0' } ); window.webContents.removeListener('did-fail-load', listener); window.show(); return true; }; } ================================================ FILE: app/tasks/managers/checkout.ts ================================================ import { emitEvent } from '../common/utils'; /* eslint-disable no-restricted-syntax */ export type DataRequester = { context: any; abort?: () => void; }; export class CheckoutManager { requesters: { [url: string]: { [taskId: string]: DataRequester; }; }; constructor() { this.requesters = {}; } check = ({ context }: DataRequester) => { const { id, task: { profile: { id: profileId }, store: { url } } } = context; if (!this.requesters[url]) { this.requesters[url] = {}; } for (const { context: _context, abort } of Object.values( this.requesters[url] )) { const { id: _id, task: { profile: { id: _profileId } } } = _context; // TODO: Refine this more... if (profileId === _profileId && _id !== id) { emitEvent(_context, [_id], { message: 'Profile already used' }); if (abort) { abort(); } } } }; insert = ({ context, abort }: DataRequester) => { const { id, task: { store: { url } } } = context; if (!this.requesters[url]) { this.requesters[url] = {}; } this.requesters[url][id] = { context, abort }; }; remove = ({ context }: DataRequester) => { const { id, task: { store: { url } } } = context; if (!this.requesters[url]) { this.requesters[url] = {}; } if (this.requesters[url][id]) { delete this.requesters[url][id]; } }; } ================================================ FILE: app/tasks/managers/checkpoint/index.tsx ================================================ import { session, Session } from 'electron'; import { Utils } from '../../common'; const { userAgent, request } = Utils; export class CheckpointManager { interval: number | null; intervalRate: number; requesters: { [url: string]: number; }; session: Session; cache: any; constructor() { this.interval = null; this.intervalRate = 5000; this.requesters = {}; this.session = session.fromPartition('persist:checkpoint'); this.cache = {}; } check = async (url: string) => { try { const { body } = await request(this.session, { url: `${url}/checkpoint`, headers: { accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', pragma: 'no-cache', 'user-agent': userAgent, origin: url } }); // return true; return /content_checkpoint/i.test(body); } catch (err) { return false; } }; isLive = (url: string) => this.cache[url] || false; start = async (url: string) => { if (typeof this.cache[url] === 'undefined') { this.cache[url] = false; } if (typeof this.requesters[url] === 'undefined') { this.requesters[url] = 1; } if (!this.interval) { this.interval = setInterval(async () => { const isLive = await this.check(url); this.cache[url] = isLive; }, this.intervalRate); const isLive = await this.check(url); this.cache[url] = isLive; } }; stop = (url: string) => { this.requesters[url] -= 1; if (this.requesters[url] < 0) { this.requesters[url] = 0; } if (this.requesters[url] === 0) { if (this.interval) { clearInterval(this.interval); this.interval = null; } } }; } ================================================ FILE: app/tasks/managers/geetest/index.ts ================================================ import { DatadomeData } from '../../footsites/classes/types'; export type SessionRequester = { id: string; url: string; data: DatadomeData; release: (cookie?: string) => void; active: boolean; }; export class GeetestManager { requesters: { [url: string]: { [id: string]: SessionRequester; }; }; constructor() { this.requesters = {}; } count = (url: string) => { if (!this.requesters[url]) { this.requesters[url] = {}; } return Object.values(this.requesters[url]).filter(({ active }) => active) .length; }; insert = ({ id, url, data, release, active }: SessionRequester) => { if (!this.requesters[url]) { this.requesters[url] = {}; } this.requesters[url][id] = { id, url, data, release, active }; }; get = (url: string, id: string) => { if (!this.requesters[url]) { this.requesters[url] = {}; } return this.requesters[url][id]; }; // swaps an active solver with an inactive solver swap = (url: string, id: string) => { if (!this.requesters[url]) { this.requesters[url] = {}; } this.requesters[url][id].active = false; // eslint-disable-next-line no-restricted-syntax for (const { id: _id, data, release } of Object.values( this.requesters[url] )) { if (id !== _id && data.t !== 'bv') { // set old requester to inactive this.requesters[url][_id].active = true; release(); break; } } }; spread = (url: string, cookie: string) => { if (!this.requesters[url]) { this.requesters[url] = {}; } // eslint-disable-next-line no-restricted-syntax for (const { id: _id, release } of Object.values(this.requesters[url])) { if (!this.requesters[url]) { this.requesters[url] = {}; } delete this.requesters[url][_id]; release(cookie); } }; remove = ({ id, url }: { id: string; url: string }) => { if (!this.requesters[url]) { this.requesters[url] = {}; } delete this.requesters[url][id]; }; } ================================================ FILE: app/tasks/managers/index.ts ================================================ import { TaskManager } from './tasks'; import { CaptchaManager } from './captcha/captcha'; import { ProxyManager } from './proxy'; import { WebhookManager } from './webhook'; import { ProfileManager } from './profile/profile'; import { NotificationManager } from './notification'; import { RestartManager } from './restart'; import { CheckoutManager } from './checkout'; import { GeetestManager } from './geetest'; import { AnalyticsManager } from './analytics'; import { BrowserManager } from './browser'; import { InterceptionManager } from './interception'; import { CheckpointManager } from './checkpoint'; import { QueueManager } from './queue'; export { TaskManager, CaptchaManager, ProxyManager, WebhookManager, ProfileManager, NotificationManager, RestartManager, CheckoutManager, AnalyticsManager, BrowserManager, QueueManager, GeetestManager, InterceptionManager, CheckpointManager }; ================================================ FILE: app/tasks/managers/interception/index.ts ================================================ import { BrowserWindow, session } from 'electron'; import { isEmpty } from 'lodash'; import { intercept, loadHost } from '../utils'; import { createInterceptionWindow } from './window'; export class InterceptionManager { requesters: any; window: BrowserWindow; active: boolean; constructor() { this.requesters = {}; this.window = null; this.active = false; } insert({ id, html }: { id: string; html: string }) { this.requesters[id] = { id, html }; if (!this.window) { this.window = createInterceptionWindow(id); } if (!this.active) { // lets render the html, hopefully we dont need to use iframe this.active = true; } } intercept = async ({ window, url }: { window: BrowserWindow; url: string; }) => { // intercept and load the proper host finally.. intercept(window); return loadHost(window, new URL(url).hostname); }; /** * * @param id task id * @param urlFilter array of urls to match. see webRequest.onCompleted options for more details. */ async interceptResponse({ id, urlFilter }: { id: string; urlFilter: string[]; }) { if (!this.window) { return undefined; } const windowSession = session.fromPartition(`persist:${id}`); return new Promise(resolve => { windowSession.webRequest.onCompleted( { urls: urlFilter }, ({ statusCode, responseHeaders }) => { resolve({ statusCode, responseHeaders }); } ); }); } // make sure to call remove in task after the desired intercepted response is achieved remove(id: string) { if (this.requesters[id]) { delete this.requesters[id]; } // lets remove the html if (isEmpty(this.requesters)) { this.window.close(); this.window = null; this.active = false; } } } ================================================ FILE: app/tasks/managers/interception/window.ts ================================================ import { BrowserWindow } from 'electron'; export const createInterceptionWindow = (id: string) => { return new BrowserWindow({ center: true, fullscreenable: false, movable: true, show: false, width: 600, height: 800, frame: false, transparent: true, resizable: true, webPreferences: { nodeIntegration: false, backgroundThrottling: true, webSecurity: true, partition: `persist:${id}` } }); }; ================================================ FILE: app/tasks/managers/notification.ts ================================================ import { BrowserWindow } from 'electron'; import { isEmpty } from 'lodash'; import { IPCKeys } from '../../constants/ipc'; type Notification = { id?: string; message: string; variant: string; type?: string; force?: boolean; }; export class NotificationManager { notification: any; mainWindow: BrowserWindow | null; constructor(mainWindow: BrowserWindow | null) { this.notification = {}; this.mainWindow = mainWindow; } notify = (notification: Notification) => { // send IPC to frontend to make the notification if (this.mainWindow) { this.mainWindow.webContents.send(IPCKeys.Notification, notification); } }; insert = (notification: Notification) => { // if we already have a notification in the queue, return if (!isEmpty(this.notification)) { return; } this.notification = notification; // remove the notification in 2.5 seconds setTimeout(() => { this.notification = {}; }, 1250); // send IPC to frontend to make the notification if (this.mainWindow) { this.mainWindow.webContents.send(IPCKeys.Notification, this.notification); } }; clear = () => { this.notification = {}; }; } ================================================ FILE: app/tasks/managers/profile/profile.ts ================================================ import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; import { Profiles, Profile } from './typings'; import { IPCKeys } from '../../../constants/ipc'; export class ProfileManager { mainWindow: BrowserWindow | null; profiles: Profiles; constructor(mainWindow: BrowserWindow | null) { this.mainWindow = mainWindow; this.profiles = {}; ipcMain.on(IPCKeys.AddProfiles, this.registerAll); ipcMain.on(IPCKeys.RemoveProfiles, this.deregisterAll); } /** * Registers a profile to the main process * @param id - profile id * @param profile - profile */ register = async (profile: Profile) => { if (!profile?.id) { return; } const { id } = profile; this.profiles[id] = profile; }; /** * Deregisters a previously registered profile from the main process * @param id - profile id */ deregister = async ({ id }: Profile) => { delete this.profiles[id]; }; /** * Retrieves a profile given an id * @param id - profile id */ retrieve = (id: string) => { return this.profiles[id]; }; /** * Registers a list or singular profile on the main process * @param proxies - Proxy/Proxies group(s) to register */ registerAll = (_: IpcMainEvent, profiles: Profile[] | Profile) => { if (Array.isArray(profiles)) { return Promise.all(profiles.map(profile => this.register(profile))); } return this.register(profiles); }; /** * Deregisters a previously loaded profile (or profiles) from the main process * @param profiles - profile(s) */ deregisterAll = (_: IpcMainEvent, profiles: Profile[] | Profile) => { if (Array.isArray(profiles)) { return Promise.all(profiles.map(profile => this.deregister(profile))); } return this.deregister(profiles); }; } ================================================ FILE: app/tasks/managers/profile/typings.d.ts ================================================ export type Profile = { id: string; name: string; matches: boolean; shipping: Location; billing: Location; payment: Payment; }; export type Location = { name: string; address: string; apt: string; city: string; province: { value: string; label: string; }; zip: string; country: { label: string; value: string; }; phone: string; email: string; }; export type Payment = { holder: string; card: string; exp: string; cvv: string; type: string; }; export type Profiles = { [id: string]: Profile; }; ================================================ FILE: app/tasks/managers/proxy.ts ================================================ import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; import { RegisterProps, DeregisterProps, ReserveProps, SwapProps, ProxyGroup, Proxy } from './typings'; import { IPCKeys } from '../../constants/ipc'; import { format, Queue } from '../common/utils'; import { IS_DEV } from '../../constants/env'; type Proxies = { [key: string]: Queue; }; export class ProxyManager { mainWindow: BrowserWindow | null; proxies: Proxies; constructor(mainWindow: BrowserWindow | null) { this.mainWindow = mainWindow; this.proxies = {}; ipcMain.on(IPCKeys.AddProxies, this.registerAll); ipcMain.on(IPCKeys.RemoveProxies, this.deregisterAll); } /** * Registers a proxy group on the main process * @param id - proxy group id * @param proxies - list of proxies */ register = async ({ id, proxies }: RegisterProps) => { const queue = new Queue(); this.proxies[id] = queue; // prevent multiple logs happening over and over again let logged = false; return Promise.all( proxies.map((p: Proxy) => { if (!IS_DEV && /^127/.test(p.ip) && !logged) { logged = true; return null; } const proxy = format(p.ip); if (!proxy) { return null; } return queue.enqueue({ ...p, proxy }); }) ); }; /** * Deregisters a previously registered group from the main process * @param id - proxy group id */ deregister = async ({ id }: DeregisterProps) => { const group = this.proxies[id]; if (!group) { return; } delete this.proxies[id]; }; /** * * @param id - proxy group id * @param taskId - task id */ reserve = ({ id, taskId }: ReserveProps) => { const queue = this.proxies[id]; if (!queue) { return null; } const proxy = queue.dequeue(); // we're out of proxies, return null if (!proxy) { return null; } if (this.mainWindow) { this.mainWindow.webContents.send( IPCKeys.ProxyStatus, taskId, id, proxy.ip, true ); } // push the reserved proxy back to the end of the queue queue.enqueue(proxy); return proxy; }; swap = async ({ id, proxy, group }: SwapProps) => { const swapped = this.reserve({ id: group, taskId: id }); if (!swapped) { return null; } // send an event to the frontend to change inUse status of old proxy if (this.mainWindow && proxy) { this.mainWindow.webContents.send( IPCKeys.ProxyStatus, id, group, proxy.ip, false ); } return swapped; }; /** * Registers a list or singular proxy group object on the main process * @param proxies - Proxy/Proxies group(s) to register */ registerAll = (_: IpcMainEvent, proxies: ProxyGroup[] | ProxyGroup) => { if (Array.isArray(proxies)) { return Promise.all(proxies.map(p => this.register(p))); } return this.register(proxies); }; /** * Deregisters a previously loaded proxy group (or groups) from the main process * @param proxies - Proxy/Proxies group(s) to deregister */ deregisterAll = (_: IpcMainEvent, proxies: ProxyGroup[] | ProxyGroup) => { if (Array.isArray(proxies)) { return Promise.all(proxies.map(p => this.deregister(p))); } return this.deregister(proxies); }; } ================================================ FILE: app/tasks/managers/queue/index.ts ================================================ import { Cookie } from 'electron'; export class QueueManager { bank: { [url: string]: { id: string; cookies: Cookie[]; }[]; }; constructor() { this.bank = {}; } get = (url: string) => { if (!this.bank[url]) { return null; } const store = this.bank[url]; const cycled = store.shift(); if (!cycled) { return null; } this.bank[url].push({ ...cycled }); return cycled; }; add = ({ id, url, cookies }: any) => { if (!this.bank[url]) { this.bank[url] = []; } this.bank[url].push({ id, cookies }); }; remove = ({ id, url }: { id: string; url: string }) => { if (!this.bank[url]) { return; } const index = this.bank[url].findIndex(c => c.id === id); if (index === -1) { return; } this.bank[url].splice(index, 1); }; } ================================================ FILE: app/tasks/managers/restart.ts ================================================ import { BrowserWindow, ipcMain } from 'electron'; import { settingsStorage } from '../../utils/storageHelper'; import { IPCKeys } from '../../constants/ipc'; export class RestartManager { autoRestart: boolean; mainWindow: BrowserWindow | null; constructor(mainWindow: BrowserWindow | null) { this.autoRestart = settingsStorage.getItem('enableAutoRestart') || false; this.mainWindow = mainWindow; ipcMain.on(IPCKeys.ToggleAutoRestart, (_, value: boolean) => { this.autoRestart = value; }); } restart = () => this.autoRestart; } ================================================ FILE: app/tasks/managers/tasks/choose.ts ================================================ import { BrowserWindow, Session } from 'electron'; import { Task, Monitor, Platforms } from '../../common/constants'; import { Parse } from '../../shopify/utils'; // Shopify includes import { ShopifyMonitor, RateFetcher, chooseShopifyTask } from '../../shopify'; // YeezySupply includes import { chooseYeezySupplyTask } from '../../yeezysupply'; // Footsites includes import { chooseFootsiteTask } from '../../footsites'; // Pokemon includes import { choosePokemonTask } from '../../pokemon'; import { ShopifyContext, YeezySupplyContext, FootsiteContext, PokemonContext } from '../../common/contexts'; import { Proxy } from '../typings'; import { ProxyManager, CaptchaManager, WebhookManager, ProfileManager, NotificationManager, RestartManager, CheckoutManager, BrowserManager, GeetestManager, QueueManager, InterceptionManager, CheckpointManager } from '..'; import { AnalyticsManager } from '../analytics'; const { getParseType } = Parse; const { ParseType } = Monitor; const { Types } = Task; type CreateProps = { task: any; group: string; taskSession: Session; monitorSession: Session; proxy: Proxy; type: string; logger: any; // todo mainWindow: BrowserWindow | null; relayMessage: any; captchaManager: CaptchaManager; proxyManager: ProxyManager; webhookManager: WebhookManager; profileManager: ProfileManager; notificationManager: NotificationManager; restartManager: RestartManager; checkoutManager: CheckoutManager; analyticsManager: AnalyticsManager; browserManager: BrowserManager; geetestManager: GeetestManager; queueManager: QueueManager; checkpointManager: CheckpointManager; interceptionManager: InterceptionManager; }; export const createTask = ({ task, group, taskSession, monitorSession, proxy, type, logger, mainWindow, relayMessage, captchaManager, proxyManager, webhookManager, profileManager, notificationManager, restartManager, checkoutManager, analyticsManager, browserManager, geetestManager, queueManager, checkpointManager, interceptionManager }: CreateProps) => { let _task; let _monitor; const { platform, id } = task; switch (platform) { case Platforms.Shopify: { const parseType = getParseType(task.product); const context = new ShopifyContext({ id, task, group, type, parseType, taskSession, monitorSession, proxy, logger, relayMessage, captchaManager, proxyManager, webhookManager, profileManager, notificationManager, restartManager, checkoutManager, analyticsManager, checkpointManager }); if (type === Types.Normal) { _monitor = new ShopifyMonitor(context); const ShopifyTask = chooseShopifyTask(task.mode); _task = ShopifyTask(context); } else if (type === Types.Rates) { _task = new RateFetcher(context, mainWindow); } break; } case Platforms.YeezySupply: { const context = new YeezySupplyContext({ id, task, group, parseType: ParseType.Variant, taskSession, monitorSession, proxy, logger, relayMessage, captchaManager, proxyManager, webhookManager, profileManager, notificationManager, restartManager, checkoutManager, browserManager, analyticsManager, interceptionManager }); const YeezySupplyTask = chooseYeezySupplyTask(); _task = YeezySupplyTask(context); break; } case Platforms.Pokemon: { const context = new PokemonContext({ id, task, group, parseType: ParseType.Variant, taskSession, monitorSession, proxy, logger, relayMessage, captchaManager, proxyManager, webhookManager, profileManager, notificationManager, restartManager, checkoutManager, analyticsManager, geetestManager }); const PokemonTask = choosePokemonTask(); _task = PokemonTask(context); break; } case Platforms.Footsites: { const context = new FootsiteContext({ id, task, group, parseType: ParseType.Variant, taskSession, monitorSession, proxy, logger, relayMessage, captchaManager, proxyManager, webhookManager, profileManager, notificationManager, restartManager, checkoutManager, analyticsManager, queueManager }); const FootsiteTask = chooseFootsiteTask(); _task = FootsiteTask(context); break; } default: break; } return { _task, _monitor }; }; ================================================ FILE: app/tasks/managers/tasks/index.ts ================================================ /* eslint-disable no-restricted-syntax */ import { isEmpty } from 'lodash'; import { session, BrowserWindow, Session } from 'electron'; import { IPCKeys } from '../../../constants/ipc'; // Shared includes import { LoggerService, StaggeredQueue } from '../../common/utils'; import { CaptchaManager, ProxyManager, WebhookManager, ProfileManager, NotificationManager, RestartManager, CheckoutManager, BrowserManager, AnalyticsManager, CheckpointManager, GeetestManager, QueueManager, InterceptionManager } from '..'; import { Task } from '../../common/constants'; // Shopify includes import { Parse } from '../../shopify/utils'; import { Tasks, Monitors, Sessions, Intervals, Messages } from '../typings'; import { createTask } from './choose'; const { getParseType } = Parse; const { Types } = Task; export class TaskManager { logPath: string; mainWindow: BrowserWindow | null; startQueue: StaggeredQueue; startWorker: Worker | undefined; tasks: Tasks; monitors: Monitors; taskSessions: Sessions; monitorSessions: Sessions; messages: Messages; messageBatchDelay: number; messageIntervals: Intervals; proxyManager: ProxyManager; webhookManager: WebhookManager; captchaManager: CaptchaManager; profileManager: ProfileManager; notificationManager: NotificationManager; restartManager: RestartManager; checkoutManager: CheckoutManager; browserManager: BrowserManager; analyticsManager: AnalyticsManager; geetestManager: GeetestManager; checkpointManager: CheckpointManager; queueManager: QueueManager; ticketManager: any; interceptionManager: InterceptionManager; constructor(logPath: string, mainWindow: BrowserWindow | null) { // Logger file path this.logPath = logPath; this.mainWindow = mainWindow; // Tasks Map this.tasks = {}; // Monitors Map this.monitors = {}; // Sessions Map this.taskSessions = {}; this.monitorSessions = {}; this.messages = {}; this.messageIntervals = {}; this.messageBatchDelay = 150; // proxy related IPC this.proxyManager = new ProxyManager(mainWindow); // webhook related IPC this.webhookManager = new WebhookManager(); this.notificationManager = new NotificationManager(mainWindow); // captcha related IPC this.captchaManager = new CaptchaManager( mainWindow, this.notificationManager ); this.profileManager = new ProfileManager(mainWindow); this.restartManager = new RestartManager(mainWindow); this.checkoutManager = new CheckoutManager(); this.browserManager = new BrowserManager(); this.analyticsManager = new AnalyticsManager(); this.geetestManager = new GeetestManager(); this.queueManager = new QueueManager(); this.checkpointManager = new CheckpointManager(); this.startQueue = new StaggeredQueue(); this.interceptionManager = new InterceptionManager(); } process = ({ group, id }: any) => { if (this.mainWindow) { const task = this.tasks[group][id]; const monitor = this.monitors[group][id]; try { if (monitor) { monitor.run(); } task.run(); } catch (error) { this.stop({ id, group }); } } }; setupMessageInterval = async (group: string) => { if (!this.messages[group]) { this.messages[group] = {}; } if (!this.messageIntervals[group]) { try { this.messageIntervals[group] = setInterval(() => { if (!this.mainWindow?.webContents) { return; } if (isEmpty(this.messages[group])) { return; } this.mainWindow.webContents.send( IPCKeys.TaskStatus, group, this.messages[group] ); this.messages[group] = {}; }, this.messageBatchDelay); } catch (e) { // fail silently... } } }; stopMessageInterval = (group: string) => { // clear out messages before stopping... if (this.mainWindow && !isEmpty(this.messages[group])) { this.mainWindow.webContents.send( IPCKeys.TaskStatus, group, this.messages[group] ); this.messages[group] = {}; } if (this.messageIntervals[group]) { clearInterval(this.messageIntervals[group]); delete this.messageIntervals[group]; } }; stopMessageIntervals = async () => { return Promise.all( Object.keys(this.messageIntervals).map((group: any) => this.stopMessageInterval(group) ) ).catch(() => {}); }; /** * Callback for Task Events * * This method is registered as a callback for all running tasks. The method * is used to merge all task events into a single stream, so only one * event handler is needed to consume all task events. * * @param {String} id the id of the task/monitor that emitted the event * @param {String} message the status message * @param {Task.Event} event the type of event that was emitted */ mergeStatusUpdates = async ( group: string, taskIds: string[], message: any ) => { if (this.mainWindow) { Promise.all( // eslint-disable-next-line array-callback-return [...taskIds].map(id => { this.messages[group] = { ...this.messages[group], [id]: { ...this.messages[group][id], ...message } }; }) ).catch(() => {}); } }; /** * Changes a delay for a group of tasks * @param delay {Number} - numerical value to change the delay to * @param type {DelayType} - either a) monitor, or b) retry * @param tasks {List} - */ changeDelay = async ({ delay, type, group, tasks = [] }: { delay: number; type: string; group: string; tasks?: any[]; }) => { // exit early if we don't have any task groups or tasks in the group if (isEmpty(this.tasks) || isEmpty(this.tasks[group])) { return; } Promise.all( tasks.map((t: any) => { const task = this.tasks[group][t.id]; const monitor = this.monitors[group][t.id]; if (task) { task.context.task[type] = delay; if (task.delayer) { task.delayer.clear(); } } if (monitor) { monitor.context.task[type] = delay; if (monitor.delayer) { monitor.delayer.clear(); } } return t; }) ).catch(() => {}); }; /** * Start a task * * This method starts a given task if it has not already been started. The * requisite data is generated (id, open proxy if it is available, etc.) and * starts the task asynchronously. * * If the given task has already started, this method does nothing. * @param {Task} task * @param {object} options Options to customize the task: * - type - The task type to start */ start = async ({ group, task, type = Types.Normal }: { group: string; task: any; type?: string; }) => { const { id, proxies } = task; this.startQueue.removeJob(group, id); if (!this.tasks[group]) { this.tasks[group] = {}; } if (!this.monitors[group]) { this.monitors[group] = {}; } setTimeout(async () => { let proxy: any = null; if (proxies) { proxy = this.proxyManager.reserve({ id: proxies.id, taskId: id }); } const taskSession: Session = session.fromPartition(`${id}-task`, { cache: false }); await taskSession.closeAllConnections(); const monitorSession: Session = session.fromPartition(`${id}-monitor`, { cache: false }); await monitorSession.closeAllConnections(); this.taskSessions[id] = taskSession; this.monitorSessions[id] = monitorSession; await taskSession.clearAuthCache(); await monitorSession.clearAuthCache(); await taskSession.clearStorageData(); await monitorSession.clearStorageData(); await taskSession.clearCache(); await monitorSession.clearCache(); await taskSession.clearHostResolverCache(); await monitorSession.clearHostResolverCache(); if (proxy?.ip) { const [ip, port] = proxy.ip.split(':'); await taskSession.setProxy({ mode: 'fixed_servers', proxyRules: `http=${ip}:${port};https=${ip}:${port};` }); await monitorSession.setProxy({ mode: 'fixed_servers', proxyRules: `http=${ip}:${port};https=${ip}:${port};` }); } else { await taskSession.setProxy({ mode: 'fixed_servers', proxyRules: '' }); await monitorSession.setProxy({ mode: 'fixed_servers', proxyRules: '' }); } await taskSession.forceReloadProxyConfig(); await monitorSession.forceReloadProxyConfig(); setTimeout(() => { const { _task, _monitor } = createTask({ task, group, taskSession, monitorSession, proxy, type, logger: LoggerService, mainWindow: this.mainWindow, relayMessage: this.mergeStatusUpdates, captchaManager: this.captchaManager, proxyManager: this.proxyManager, webhookManager: this.webhookManager, profileManager: this.profileManager, notificationManager: this.notificationManager, restartManager: this.restartManager, checkoutManager: this.checkoutManager, analyticsManager: this.analyticsManager, browserManager: this.browserManager, geetestManager: this.geetestManager, queueManager: this.queueManager, checkpointManager: this.checkpointManager, interceptionManager: this.interceptionManager }); if (!_task) { return; } if (_monitor) { this.monitors[group][id] = _monitor; } this.tasks[group][id] = _task; this.setupMessageInterval(group); return this.startQueue.add(group, id, this.process); }, 500); }, 0); }; /** * Start multiple tasks * * This method is a convenience method to start multiple tasks * with a single call. The `start()` method is called for all * tasks in the given list. * * @param {List} tasks list of tasks to start * @param {object} options Options to customize the task: * - type - The task type to start */ startAll = async ({ group, tasks }: { group: string; tasks: any[] }) => { const promises = tasks.map(task => this.start({ group, task })); return Promise.allSettled(promises).catch(() => {}); }; /** * Stop a task * * This method stops a given task if it is running. This is done by sending * an abort signal to force the task to stop and cleanup anything it needs * to. * * This method does nothing if the given task has already stopped or * if it was never started. * * @param {Task} task the task to stop */ stop = async ({ id, group }: { id: string; group: string }) => { this.startQueue.removeJob(group, id); const task = this.tasks[group][id]; if (!task) { return null; } delete this.tasks[group][id]; const taskSession = this.taskSessions[id]; if (taskSession) { await Promise.allSettled([ taskSession.clearCache(), taskSession.clearStorageData() ]).catch(console.error); delete this.taskSessions[id]; } const monitorSession = this.monitorSessions[id]; if (monitorSession) { await Promise.allSettled([ monitorSession.clearCache(), monitorSession.clearStorageData() ]).catch(console.error); delete this.monitorSessions[id]; } try { task.abort(); } catch (e) { // fail silently... } // if we're just stopping a rate task, return early.. if (id === 'RATE_FETCHER') { return true; } // emit an event to set the task's proxy to !inUse if (this.mainWindow && task.context?.proxy) { this.mainWindow.webContents.send( IPCKeys.ProxyStatus, id, task.context.task.proxies.id, task.context.proxy.ip, false ); } const monitor: any = Object.values(this.monitors[group]).find((m: any) => m.context.hasId(id) ); try { monitor.stop(id); } catch (e) { // fail silently... } if (isEmpty(this.tasks[group])) { this.stopMessageInterval(group); } return this._cleanup(group, task, monitor); }; /** * Stop multiple tasks * * This method is a convenience method to stop multiple tasks * with a single call. The `stop()` method is called for all * tasks in the given list. * * @param {List} tasks list of tasks to stop * @param {Map} options options associated with stopping tasks */ async stopAll({ group, tasks }: { group: string; tasks: any[] }) { const promises = tasks.map(({ id }: { id: string }) => this.stop({ id, group }) ); return Promise.allSettled(promises).catch(() => {}); } async update(group: string, task: any) { const { id } = task; const taskGroup = this.tasks[group]; const monitorGroup = this.monitors[group]; if (!taskGroup || !monitorGroup) { return; } const oldTask = taskGroup[id]; const monitor = monitorGroup[id]; // we need to hard restart the task in these cases if (oldTask?.context?.task?.store?.url !== task.store.url) { this.stop({ group, id }); return this.start({ group, task }); } if (!monitor) { return; } const parseType = getParseType(task.product); oldTask.context.task = task; oldTask.context.parseType = parseType; monitor.context.task = task; monitor.context.parseType = parseType; if (monitor.delayer) { monitor.delayer.clear(); } } async updateAll(group: string, tasks: any[]) { return Promise.all([...tasks].map(t => this.update(group, t))); } async _cleanup(group: string, task: any, monitor: any) { const { context: taskContext } = task; if (monitor) { const { context: monitorContext } = monitor; if (!monitorContext.ids.length) { delete this.monitors[group][monitorContext.id]; } } return taskContext.id; } } ================================================ FILE: app/tasks/managers/typings.d.ts ================================================ import { WebhookClient } from 'discord.js'; import { Session } from 'electron'; import { AycdClient } from './webhook/aycd'; import { AsyncQueue, Queue } from '../common/utils'; import { ShopifyMonitor, RateFetcher } from '../shopify'; import { FastTask } from '../shopify/classes/tasks/fast'; import { SafeTask } from '../shopify/classes/tasks/safe'; import { PreloadTask } from '../shopify/classes/tasks/preload'; import { BaseFootsiteTask } from '../footsites/classes/tasks/base'; import { YeezySupplyTask } from '../yeezysupply/classes/tasks/base'; type Proxies = { [key: string]: Queue; }; type ProxyGroup = { id: string; selected: boolean; name: string; proxies: Proxy[]; }; type Proxy = { ip: string; proxy?: string; selected: boolean; speed: null | number; inUse: boolean; }; type RegisterProps = { id: string; proxies: Proxy[]; }; type DeregisterProps = { id: string; }; type ReserveProps = { id: string; taskId: string; }; type SwapProps = { id: string; proxy: Proxy | null; group: string; }; type TestProps = { id: string; type: string; }; type Webhook = { client: WebhookClient | AycdClient; id: string; name: string; url: string; declines: boolean; type: string; }; type WebhookData = { idempotency?: string; success?: boolean; date?: Date | string | number; user?: string; version?: string; url?: string; mode: string; proxy?: string; quantity?: string; product: { name: string; price: string; image: string; size: string; url: string; }; store: { name: string; url: string; }; delays: { monitor: string | number; retry: string | number; checkout?: string | number; }; profile: { name: string; type: string; }; }; type Webhooks = { [key: string]: Webhook; }; type CaptchaQueues = { [key: string]: AsyncQueue; }; type Sessions = { [key: string]: Session; }; type Messages = { [group: string]: { [id: string]: { [prop: string]: string; }; }; }; type Intervals = { [group: string]: number; }; type Tasks = { [group: string]: { [id: string]: | FastTask | SafeTask | PreloadTask | SafeTask | FastTask | YeezySupplyTask | BaseFootsiteTask | RateFetcher; }; }; type Monitors = { [group: string]: { [id: string]: ShopifyMonitor; }; }; ================================================ FILE: app/tasks/managers/utils.ts ================================================ /* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-param-reassign */ import { BrowserWindow, Cookie } from 'electron'; import { readFileSync } from 'fs'; import { parse } from 'url'; import { Platforms } from '../../constants'; import { IS_DEV, DEBUG_PROD } from '../../constants/env'; import { PATHS } from '../../utils/paths'; import { format } from '../../utils/proxy'; import { CaptchaRequester, CaptchaRequesters, CaptchaProps } from './captcha/captcha'; import { IPCKeys } from '../../constants/ipc'; export const HarvestStates = { IDLE: 'idle', START: 'start', AUTOSOLVE: 'autosolve' }; export const WindowStates = { LOAD: 'load', READY: 'ready', SOLVING: 'solving', CLOSE: 'close' }; export const loadGoogle = async (window: BrowserWindow, options?: object) => { return window.loadURL('https://www.google.com', options); }; export const loadLogin = async (window: BrowserWindow) => { return window.loadURL( 'https://accounts.google.com/signin/v2/identifier?hl=en&service=youtube&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Ffeature%3Dsign_in_button%26hl%3Den%26app%3Ddesktop%26next%3D%252F%26action_handle_signin%3Dtrue&passive=true&uilel=3&flowName=GlifWebSignIn&flowEntry=ServiceLogin', { userAgent: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0' } ); }; export const loadHost = async ( window: BrowserWindow, host: string, options?: object ): Promise => { return window.loadURL(host, options); }; export const sleep = (time: string | number) => new Promise(resolve => setTimeout(resolve, Number(time))); export const intercept = (window: BrowserWindow) => { window.webContents.session.protocol.interceptBufferProtocol( 'https', (req: any, callback: any) => { const { host } = parse(req.url); if (!/signify|paypal|datadome|google|gstatic|monorail/i.test(host)) { const html = readFileSync(PATHS.captchaUrlPath, 'utf8'); window.webContents.session.protocol.uninterceptProtocol('https'); return callback({ mimeType: 'text/html', data: Buffer.from(html) }); } } ); }; type SetProxyProps = { window: BrowserWindow; proxy?: string; }; type SetCookieProps = { window: BrowserWindow; url: string; cookies: Cookie[]; }; type AttachRequesterProps = { entry: CaptchaProps; version: 0 | 1 | 2; requester: CaptchaRequester; window: BrowserWindow; isCheckpoint: boolean; remove: ({ id, platform }: { id: string; platform: string }) => void; needsFocus: boolean; }; type DetachRequesterProps = { requesters: CaptchaRequesters; platform: string; entry: CaptchaProps; }; export const setProxy = async ({ window, proxy }: SetProxyProps) => { const formatted = format(proxy); if (formatted) { const [ip, port] = formatted; await window.webContents.session.setProxy({ mode: 'fixed_servers', proxyRules: `http=${ip}:${port};https=${ip}:${port};` }); } else { await window.webContents.session.setProxy({ mode: 'fixed_servers', proxyRules: '' }); } if (IS_DEV || DEBUG_PROD) { const proxy = await window.webContents.session.resolveProxy( 'https://google.com' ); console.info('Using proxy: %j', proxy); window.webContents.session.cookies .get({}) .then((cookies: any) => { console.info('Session cookies: %j', cookies?.length || 0); return null; }) .catch((error: any) => { console.info('Error retrieving session cookies: %j', error); }); } }; export const setCookies = async ({ window, url, cookies }: SetCookieProps): Promise => { try { for (const cookie of cookies) { try { // eslint-disable-next-line no-await-in-loop await window.webContents.session.cookies.set({ url, ...cookie }); } catch (e) { console.error( '[HARVESTER]: Failed to set cookie: ', cookie.name, cookie.value ); // noop.. } } } catch (e) { console.error('[HARVESTER]: Failed to set cookies: ', e); } }; type CheckpointProps = { window: BrowserWindow; url: string; }; export const removeCookies = async ({ window, url }: CheckpointProps) => { const cookies = await window.webContents.session.cookies.get({ url }); for (const cookie of cookies) { try { if (cookie.name !== 'hc_accessibility') { await window.webContents.session.cookies.remove(url, cookie.name); } } catch (e) { console.error( '[HARVESTER]: Failed to remove cookie: ', cookie.name, cookie.value ); } } }; export const blockPopups = (window: BrowserWindow) => { window.webContents.session.webRequest.onBeforeRequest((details, callback) => { const testUrl = details.url; if (/cookiebot.com|listrakbi.com|klaviyo.com/i.test(testUrl)) { callback({ cancel: true }); } else { callback({ cancel: false }); } }); }; const getCaptchaForm = ` (function() { return new Promise((resolve, reject) => { const interval = setInterval(() => { if ((document.querySelector('[name="g-recaptcha-response"]') && document.querySelector('[name="g-recaptcha-response"]').value && document.querySelector('[name="g-recaptcha-response"]').value.length > 0) || (document.querySelector('[name="h-captcha-response"]') && document.querySelector('[name="h-captcha-response"]').value && document.querySelector('[name="h-captcha-response"]').value.length > 0)) { clearInterval(interval) resolve({ form: Object.fromEntries(Array.from(new FormData(document.querySelector('form[action="/checkpoint"]')))), body: document.documentElement.innerHTML }); }}, 100); }) })();`; export const attachRequester = async ({ entry, version, requester, window, isCheckpoint, remove, needsFocus }: AttachRequesterProps) => { const { id, host, proxy, cookies, platform, harvest } = requester; entry.task = id; // assign task id to window requester.state = HarvestStates.START; // change requester state to start // no matter what we want to show the window if it isn't already shown.. if (needsFocus) { window.show(); window.focus(); } if (requester.userAgent) { window.webContents.userAgent = requester.userAgent; } // eslint-disable-next-line @typescript-eslint/no-unused-vars const { harvest: _harvest, ...data } = requester; switch (platform) { case Platforms.Shopify: { entry.proxy = proxy; await setProxy({ window, proxy }); if (isCheckpoint) { const { host: _host } = new URL(window.webContents.getURL()); const { origin: url } = new URL(host); if (cookies?.length) { try { await removeCookies({ window, url }); } catch (e) { console.error(e); } await setCookies({ window, url, cookies }); } if (host !== _host) { await loadHost(window, host); } blockPopups(window); return window.webContents .executeJavaScript(getCaptchaForm) .then(async ({ form, body }) => { const cookies = await window.webContents.session.cookies.get({ url }); // harvest the token harvest({ form, body, cookies }); // remove set-cookies await removeCookies({ window, url }); // reomve the requesters return remove({ id, platform }); }) .catch(err => { console.error(`[CAPTCHA]: V2 Error: `, err); }); } break; } case Platforms.Pokemon: { if (proxy) { entry.proxy = proxy; await setProxy({ window, proxy }); } break; } default: break; } // all other cases other than checkpoint, just send the data across IPC window.webContents.send(IPCKeys.StartHarvest, { ...data, version }); }; export const detachRequester = async ({ requesters, platform, entry }: DetachRequesterProps) => { entry.task = null; const { window, type, name, theme } = entry; // special case for stopping a checkpoint harvester // since it's not our html file const isCheckpoint = type === 'Checkpoint'; if (isCheckpoint) { // might be expensive, let's weigh the cost of it.. const hasNextRequester = () => Object.values(requesters[platform]).some( requester => requester.checkpoint && requester.state === HarvestStates.IDLE ); if (!hasNextRequester()) { entry.state = WindowStates.LOAD; window.webContents.once('did-finish-load', () => { window.webContents.send(IPCKeys.HarvesterData, { name, type, platform, theme }); entry.state = WindowStates.READY; }); intercept(window); // eslint-disable-next-line no-await-in-loop return loadHost(window, 'https://checkout.shopify.com'); } entry.state = WindowStates.READY; return; } if (platform === Platforms.Footsites) { const hasNextRequester = () => Object.values(requesters[platform]).some( requester => requester.platform === Platforms.Footsites && requester.state === HarvestStates.IDLE ); if (!hasNextRequester()) { entry.state = WindowStates.LOAD; window.webContents.once('did-finish-load', () => { window.webContents.send(IPCKeys.HarvesterData, { name, type, platform, theme }); entry.state = WindowStates.READY; }); intercept(window); // eslint-disable-next-line no-await-in-loop return loadHost(window, 'https://geo.captcha-delivery.com'); } entry.state = WindowStates.READY; return; } window.webContents.send(IPCKeys.StopHarvest); entry.state = WindowStates.READY; }; export const removeNonAuthCookies = async (window: BrowserWindow) => { // get all cookies not related to google or youtube const cookies: Cookie[] = ( await window.webContents.session.cookies.get({}) ).filter(({ domain }) => domain && !/youtube|google/i.test(domain)); if (cookies?.length) { await Promise.all( cookies.map(cookie => cookie.domain ? window.webContents.session.cookies.remove( cookie.domain, cookie.name ) : null ) ); } }; ================================================ FILE: app/tasks/managers/webhook/aycd.ts ================================================ /* eslint-disable camelcase */ import { app, session } from 'electron'; import { request } from '../../common/utils/request'; import { WebhookData } from '../typings'; import { getColor, validURL } from '.'; export const build = ({ success = false, url, date = new Date(), mode, product, store, delays, proxy, profile, quantity }: WebhookData) => { const color = getColor(success, true) as number; const embed: any = { color, timestamp: (date as any).toISOString(), footer: { text: `Nebulabots v${app.getVersion()} © ${(date as any).getFullYear()}`, icon_url: 'https://nebulabots.s3.amazonaws.com/nebula-logo.png' }, url: store.url, fields: [] }; const { name: storeName } = store; if (success) { embed.title = `Checkout Success - ${storeName} (${mode})`; } else { embed.title = `Checkout Failed - ${storeName} (${mode})`; } if (url) { embed.url = url; } const { name: productName, url: productUrl, image, size, price } = product; if (validURL(image)) { embed.thumbnail = { url: image }; } if (productName && productUrl) { embed.fields.push({ name: 'Product', value: `[${productName}](${productUrl})`, inline: false }); } else if (productName) { embed.fields.push({ name: 'Product', value: productName, inline: false }); } else { embed.fields.push({ name: 'Product', value: `N/A`, inline: false }); } if (price && Number.isNaN(price)) { embed.fields.push({ name: 'Price', value: `N/A`, inline: true }); } else if (price) { embed.fields.push({ name: 'Price', value: price, inline: true }); } if (quantity) { embed.fields.push({ name: 'Quantity', value: `${quantity}`, inline: true }); } if (size) { embed.fields.push({ name: 'Size', value: size, inline: true }); } else { embed.fields.push({ name: 'Size', value: 'N/A', inline: true }); } if (profile) { const { name: profileName } = profile; embed.fields.push({ name: 'Profile', value: `||${profileName}||`, inline: true }); } if (proxy) { embed.fields.push({ name: 'Proxy', value: `||${proxy}||`, inline: true }); } else { embed.fields.push({ name: 'Proxy', value: 'None', inline: true }); } if (delays) { const { monitor, retry, checkout } = delays; let delay = ''; if (monitor || monitor === 0) { delay += `M: ${monitor}`; } if (retry || retry === 0) { delay += ` / R: ${retry}`; } if (checkout || checkout === 0) { delay += ` / C: ${checkout}`; } embed.fields.push({ name: 'Delays', value: `||${delay}||`, inline: true }); } return embed; }; export class AycdClient { url: string; constructor(url: string) { this.url = url; } send = async (embed: any) => request(session.defaultSession, { url: this.url, method: 'POST', headers: { 'content-type': 'application/json' }, json: { embeds: [embed] } }); } ================================================ FILE: app/tasks/managers/webhook/discord.ts ================================================ import { app } from 'electron'; import { MessageEmbed } from 'discord.js'; import { WebhookData } from '../typings'; import { getColor, validURL } from '.'; export const build = ({ success = false, url, date = new Date(), mode, product, store, delays, proxy, profile, quantity }: WebhookData) => { const color = getColor(success); const embed = new MessageEmbed() .setColor(color) .setTimestamp((date as any).valueOf()) .setFooter( `Nebulabots v${app.getVersion()} © ${(date as any).getFullYear()}`, 'https://nebulabots.s3.amazonaws.com/nebula-logo.png' ); const { name: storeName, url: storeUrl } = store; if (success) { embed.setTitle(`Checkout Success - ${storeName} (${mode})`); } else { embed.setTitle(`Checkout Failed - ${storeName} (${mode})`); } if (url) { embed.setURL(url); } else { embed.setURL(storeUrl); } const { name: productName, url: productUrl, image, size, price } = product; if (validURL(image)) { embed.setThumbnail(image); } if (productName && productUrl) { embed.addField('Product', `[${productName}](${productUrl})`, false); } else if (productName) { embed.addField('Product', productName, false); } else { embed.addField('Product', 'Unknown', false); } if (price && /nan/i.test(price)) { embed.addField('Price', 'Unknown', true); } else if (price) { embed.addField('Price', price, true); } if (quantity) { embed.addField('Quantity', `${quantity}`, true); } if (size) { embed.addField('Size', size, true); } else { embed.addField('Size', 'N/A', true); } if (profile) { const { name: profileName } = profile; embed.addField('Profile', `||${profileName}||`, true); } if (proxy) { embed.addField('Proxy', `||${proxy}||`, true); } else { embed.addField('Proxy', 'None', true); } if (delays) { const { monitor, retry, checkout } = delays; let delay = ''; if (monitor || monitor === 0) { delay += `M: ${monitor}`; } if (retry || retry === 0) { delay += ` / R: ${retry}`; } if (checkout || checkout === 0) { delay += ` / C: ${checkout}`; } embed.addField('Delays', `||${delay}||`, true); } return embed; }; ================================================ FILE: app/tasks/managers/webhook/index.ts ================================================ import { app, ipcMain, IpcMainEvent } from 'electron'; import { WebhookClient, MessageEmbed } from 'discord.js'; import SlackWebhook from 'slack-webhook'; import { AycdClient, build as buildAycd } from './aycd'; import { build as buildDiscord } from './discord'; import { build as buildSlack } from './slack'; import { Webhook, Webhooks, WebhookData, TestProps } from '../typings'; import { IPCKeys } from '../../../constants/ipc'; export const validURL = (str: string) => { const pattern = new RegExp( '^(https?:\\/\\/)?' + // protocol '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address '(\\:\\d+)?(\\/[-a-z\\d%_.,~+]*)*' + // port and path '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string '(\\#[-a-z\\d_]*)?$', 'i' ); return !!pattern.test(str); }; export const getColor = (success: boolean, asNumber = false) => { if (success) { if (asNumber) { return 9339892; } return '#8E83F4'; } if (asNumber) { return 15679838; } return '#EF415E'; }; export class WebhookManager { webhooks: Webhooks; constructor() { this.webhooks = {}; ipcMain.on(IPCKeys.TestWebhook, this.test); ipcMain.on(IPCKeys.AddWebhooks, this.registerAll); ipcMain.on(IPCKeys.RemoveWebhooks, this.deregisterAll); } /** * Registers a webhook object on the main process * @param webhook - webhook object */ register = (webhook: Webhook) => { if (webhook.type === 'discord') { const [, , , , , id, token] = webhook.url.split('/'); this.webhooks[webhook.id] = { ...webhook, client: new WebhookClient({ id, token }) }; } else if (webhook.type === 'aycd') { this.webhooks[webhook.id] = { ...webhook, client: new AycdClient(webhook.url) }; } else if (webhook.type === 'slack') { this.webhooks[webhook.id] = { ...webhook, client: new SlackWebhook(webhook.url) }; } }; /** * Deregisters a webhook object from the main process * @param id - webhook id to deregister */ deregister = ({ id }: Webhook) => { if (!this.webhooks[id]) { return; } delete this.webhooks[id]; }; /** * Registers a webhook (or list of) on the main process * @param webhooks list of webhooks (or webhook) to register */ registerAll = (_: IpcMainEvent, webhooks: Webhook | Webhook[]) => { if (Array.isArray(webhooks)) { return Promise.allSettled(webhooks.map(w => this.register(w))); } return this.register(webhooks); }; /** * Deregisters a webhook (or list of) from the main process * @param webhooks list of webhooks (or webhook) to deregister */ deregisterAll = (_: IpcMainEvent, webhooks: Webhook | Webhook[]) => { if (Array.isArray(webhooks)) { return Promise.allSettled(webhooks.map(w => this.deregister(w))); } return this.deregister(webhooks); }; build(data: WebhookData, type: string) { switch (type) { default: case 'discord': return buildDiscord(data); case 'slack': return buildSlack(data); case 'aycd': return buildAycd(data); } } log(data: WebhookData) { const { success } = data; Object.values(this.webhooks).forEach(({ client, declines, type }) => { const embed = this.build({ ...data, date: new Date() }, type); // if success, or !!success and we want declines sent if (success || (!success && (declines || declines === undefined))) { client.send({ embeds: [embed] }); } }); } test = (_: IpcMainEvent, { id }: TestProps) => { const webhook = this.webhooks[id]; if (!webhook) { return; } const date = new Date(); const { client, type } = webhook; let embed = {}; switch (type) { default: case 'discord': { embed = new MessageEmbed() .setColor(9339892) .setTitle("Yup! You're connected and ready to go. 😎") .setDescription( 'Please be aware that Discord rate limits webhook\n messages to 5 messages per 10 seconds.' ) .setTimestamp(date.valueOf()) .setFooter( `Nebulabots v${app.getVersion()} © ${date.getFullYear()}`, 'https://nebulabots.s3.amazonaws.com/nebula-logo.png' ); break; } case 'aycd': { embed = { color: 9339892, title: "Yup! You're connected and ready to go. 😎", description: 'Please be aware that Discord rate limits webhook\n messages to 5 messages per 10 seconds.', timestamp: date.toISOString(), footer: { text: `Nebulabots v${app.getVersion()} © ${date.getFullYear()}`, icon_url: 'https://nebulabots.s3.amazonaws.com/nebula-logo.png' } }; break; } case 'slack': { embed = { attachments: [ { title: "Yup! You're connected and ready to go. 😎", text: 'Please be aware that Discord rate limits webhook\n messages to 5 messages per 10 seconds.', color: 9339892, fields: [], footer: `Nebulabots v${app.getVersion()} @ ${date.getFullYear()}`, footer_icon: 'https://nebulabots.s3.amazonaws.com/nebula-logo.png', ts: date.valueOf() } ] }; break; } } return client.send({ embeds: [embed] }); }; } ================================================ FILE: app/tasks/managers/webhook/slack.ts ================================================ import { app } from 'electron'; import SlackWebhook from 'slack-webhook'; import { WebhookData } from '../typings'; import { getColor, validURL } from '.'; export const connect = (url: string) => new SlackWebhook(url); export const buildTitle = ( success: boolean, storeName: string, mode: string, url?: string ) => { if (url) { if (success) { return `<${url}|Checkout Success - ${storeName} (${mode})>`; } return `<${url}|Checkout Failed - ${storeName} (${mode})>`; } if (success) { return `Checkout Success - ${storeName} (${mode})`; } return `Checkout Failed - ${storeName} (${mode})`; }; export const build = ({ success = false, url, date = new Date(), mode, proxy, product, store, delays, profile, quantity }: WebhookData) => { const embed: any = { attachments: [ { title: buildTitle(success, store.name, mode, url), color: getColor(success), fields: [], footer: `Nebulabots v${app.getVersion()} @ ${(date as any).getFullYear()}`, footer_icon: 'https://nebulabots.s3.amazonaws.com/nebula-logo.png', ts: (date as any).valueOf() } ] }; // product const { name: productName, url: productUrl, image, size, price } = product; if (validURL(image)) { embed.attachments[0].thumb_url = image; } embed.attachments[0].fields.push({ title: 'Product', value: `<${productUrl}|${productName}>`, short: false }); embed.attachments[0].fields.push({ title: 'Price', value: price, short: true }); embed.attachments[0].fields.push({ title: 'Size', value: size, short: true }); if (quantity) { embed.attachments[0].fields.push({ title: 'Quantity', value: `${quantity}`, short: true }); } if (profile) { const { name: profileName } = profile; embed.attachments[0].fields.push({ title: 'Profile', value: profileName, short: false }); } if (proxy) { embed.attachments[0].fields.push({ title: 'Proxy', value: proxy, short: false }); } else { embed.attachments[0].fields.push({ title: 'Proxy', value: 'None', short: false }); } if (delays) { const { monitor, retry, checkout } = delays; let delay = ''; if (monitor || monitor === 0) { delay += `M: ${monitor}`; } if (retry || retry === 0) { delay += `/ R: ${retry}`; } if (checkout || checkout === 0) { delay += `/ C: ${checkout}`; } embed.attachments[0].fields.push({ title: 'Delays', value: delay, short: false }); } return embed; }; ================================================ FILE: app/tasks/pokemon/classes/functions/captcha.ts ================================================ import { Task } from '../../constants'; import { DatadomeData } from '../../types'; const { States } = Task; export const submitCaptcha = ({ handler, url, data, userAgent, captchaToken }: { handler: Function; url: string; data: DatadomeData; userAgent: string; captchaToken: string; }) => { const { host, cid, initialCid, hsh, referer: _referer, s, t } = data; const referer = `https://${host}/captcha/?initialCid=${initialCid}&hash=${hsh}&t=${t}&s=${s}&referer=${encodeURIComponent( _referer )}&cid=${cid}`; const endpoint = `https://${host}/captcha/check?cid=${encodeURIComponent( cid )}&icid=${encodeURIComponent( initialCid )}&ccid=null&g-recaptcha-response=${encodeURIComponent( captchaToken )}&hash=${hsh}&ua=${encodeURIComponent( userAgent )}&referer=${encodeURIComponent( `${url}/` )}&parent_url=&x-forwarded-for=&captchaChallenge=12439015&s=${s}`; return handler({ endpoint, options: { method: 'GET', json: true, headers: { accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', dnt: '1', referer } }, message: 'Submitting captcha', from: States.SUBMIT_CAPTCHA }); }; ================================================ FILE: app/tasks/pokemon/classes/functions/cart.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const addToCart = ({ handler, quantity, atcUri }: { handler: Function; quantity: string; atcUri: string; }) => handler({ endpoint: `/tpci-ecommweb-api/cart?type=product&format=nodatalinks`, options: { method: 'POST', headers: { 'content-type': 'application/json', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' }, json: { productURI: atcUri, quantity: quantity || 1, configuration: {} } }, message: 'Adding to cart', from: States.ADD_TO_CART }); export const clearCart = ({ handler, cartUri }: { handler: Function; cartUri: string; }) => handler({ endpoint: `/tpci-ecommweb-api/cart?type=product&format=zoom.nodatalinks`, options: { method: 'POST', headers: { 'content-type': 'application/json', referer: 'https://www.pokemoncenter.com/cart', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' }, json: { productURI: cartUri, quantity: 0 } }, message: 'Clearing cart', from: States.CLEAR_CART }); ================================================ FILE: app/tasks/pokemon/classes/functions/checkout.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const submitCheckout = async ({ handler, storeUrl, purchaseUri }: { handler: Function; storeUrl: string; purchaseUri: string; }) => { return handler({ endpoint: `/tpci-ecommweb-api/order?format=nodatalinks`, options: { method: 'POST', headers: { 'content-type': 'application/json', referer: `${storeUrl}/checkout/summary` }, json: { purchaseForm: purchaseUri } }, message: 'Submitting checkout', from: States.SUBMIT_CHECKOUT }); }; ================================================ FILE: app/tasks/pokemon/classes/functions/datadome.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const getCookie = ({ handler, userAgent, responsePage, jsType, ddv, cid }: { handler: Function; userAgent: string; responsePage: string; jsType: string; ddv: string; cid: string; }) => { return handler({ endpoint: `https://dd.pokemoncenter.com/js/`, options: { method: 'POST', json: true, headers: { accept: '*/*', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/x-www-form-urlencoded', origin: 'https://www.pokemoncenter.com' }, form: `jsData=%7B%22ttst%22%3A22.999999046325684%2C%22ifov%22%3Afalse%2C%22wdifts%22%3Atrue%2C%22wdifrm%22%3Afalse%2C%22wdif%22%3Afalse%2C%22br_h%22%3A984%2C%22br_w%22%3A893%2C%22br_oh%22%3A1095%2C%22br_ow%22%3A893%2C%22nddc%22%3A1%2C%22rs_h%22%3A1120%2C%22rs_w%22%3A1792%2C%22rs_cd%22%3A30%2C%22phe%22%3Afalse%2C%22nm%22%3Afalse%2C%22jsf%22%3Afalse%2C%22ua%22%3A%22${encodeURIComponent( userAgent )}%22%2C%22lg%22%3A%22en-US%22%2C%22pr%22%3A2%2C%22hc%22%3A16%2C%22ars_h%22%3A1095%2C%22ars_w%22%3A1741%2C%22tz%22%3A360%2C%22str_ss%22%3Atrue%2C%22str_ls%22%3Atrue%2C%22str_idb%22%3Atrue%2C%22str_odb%22%3Atrue%2C%22plgod%22%3Afalse%2C%22plg%22%3A5%2C%22plgne%22%3A%22NA%22%2C%22plgre%22%3A%22NA%22%2C%22plgof%22%3A%22NA%22%2C%22plggt%22%3A%22NA%22%2C%22pltod%22%3Afalse%2C%22hcovdr%22%3Afalse%2C%22plovdr%22%3Afalse%2C%22ftsovdr%22%3Afalse%2C%22hcovdr2%22%3Afalse%2C%22plovdr2%22%3Afalse%2C%22ftsovdr2%22%3Afalse%2C%22lb%22%3Afalse%2C%22eva%22%3A33%2C%22lo%22%3Afalse%2C%22ts_mtp%22%3A0%2C%22ts_tec%22%3Afalse%2C%22ts_tsa%22%3Afalse%2C%22vnd%22%3A%22Google%20Inc.%22%2C%22bid%22%3A%22NA%22%2C%22mmt%22%3A%22application%2Fpdf%2Ctext%2Fpdf%22%2C%22plu%22%3A%22PDF%20Viewer%2CChrome%20PDF%20Viewer%2CChromium%20PDF%20Viewer%2CMicrosoft%20Edge%20PDF%20Viewer%2CWebKit%20built-in%20PDF%22%2C%22hdn%22%3Afalse%2C%22awe%22%3Afalse%2C%22geb%22%3Afalse%2C%22dat%22%3Afalse%2C%22med%22%3A%22defined%22%2C%22aco%22%3A%22probably%22%2C%22acots%22%3Afalse%2C%22acmp%22%3A%22probably%22%2C%22acmpts%22%3Atrue%2C%22acw%22%3A%22probably%22%2C%22acwts%22%3Afalse%2C%22acma%22%3A%22maybe%22%2C%22acmats%22%3Afalse%2C%22acaa%22%3A%22probably%22%2C%22acaats%22%3Atrue%2C%22ac3%22%3A%22%22%2C%22ac3ts%22%3Afalse%2C%22acf%22%3A%22probably%22%2C%22acfts%22%3Afalse%2C%22acmp4%22%3A%22maybe%22%2C%22acmp4ts%22%3Afalse%2C%22acmp3%22%3A%22probably%22%2C%22acmp3ts%22%3Afalse%2C%22acwm%22%3A%22maybe%22%2C%22acwmts%22%3Afalse%2C%22ocpt%22%3Afalse%2C%22vco%22%3A%22probably%22%2C%22vcots%22%3Afalse%2C%22vch%22%3A%22probably%22%2C%22vchts%22%3Atrue%2C%22vcw%22%3A%22probably%22%2C%22vcwts%22%3Atrue%2C%22vc3%22%3A%22maybe%22%2C%22vc3ts%22%3Afalse%2C%22vcmp%22%3A%22%22%2C%22vcmpts%22%3Afalse%2C%22vcq%22%3A%22%22%2C%22vcqts%22%3Afalse%2C%22vc1%22%3A%22probably%22%2C%22vc1ts%22%3Afalse%2C%22dvm%22%3A8%2C%22sqt%22%3Afalse%2C%22so%22%3A%22landscape-primary%22%2C%22wbd%22%3Afalse%2C%22wbdm%22%3Atrue%2C%22wdw%22%3Atrue%2C%22cokys%22%3A%22bG9hZFRpbWVzY3NpYXBwcnVudGltZQ%3D%3DL%3D%22%2C%22ecpc%22%3Afalse%2C%22lgs%22%3Atrue%2C%22lgsod%22%3Afalse%2C%22bcda%22%3Atrue%2C%22idn%22%3Atrue%2C%22capi%22%3Afalse%2C%22svde%22%3Afalse%2C%22vpbq%22%3Atrue%2C%22xr%22%3Atrue%2C%22bgav%22%3Atrue%2C%22rri%22%3Atrue%2C%22idfr%22%3Atrue%2C%22ancs%22%3Atrue%2C%22inlc%22%3Atrue%2C%22cgca%22%3Atrue%2C%22inlf%22%3Atrue%2C%22tecd%22%3Atrue%2C%22sbct%22%3Atrue%2C%22aflt%22%3Atrue%2C%22rgp%22%3Atrue%2C%22bint%22%3Atrue%2C%22spwn%22%3Afalse%2C%22emt%22%3Afalse%2C%22bfr%22%3Afalse%2C%22dbov%22%3Afalse%2C%22glvd%22%3A%22Intel%20Inc.%22%2C%22glrd%22%3A%22Intel(R)%20UHD%20Graphics%20630%22%2C%22tagpu%22%3A7.3999998569488525%2C%22prm%22%3Atrue%2C%22tzp%22%3A%22America%2FDenver%22%2C%22cvs%22%3Atrue%2C%22usb%22%3A%22defined%22%7D&events=%5B%5D&eventCounters=%5B%5D&jsType=${jsType}&cid=${cid}&ddk=5B45875B653A484CC79E57036CE9FC&Referer=https%253A%252F%252Fwww.pokemoncenter.com%252F&request=%252F&responsePage=${responsePage}&ddv=${ddv}` }, message: 'Initializing', from: States.GET_COOKIE }); }; ================================================ FILE: app/tasks/pokemon/classes/functions/email.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const submitEmail = ({ handler, storeUrl, json }: { handler: Function; storeUrl: string; json: any; }) => { return handler({ endpoint: `/tpci-ecommweb-api/email?format=zoom.nodatalinks`, options: { method: 'POST', headers: { 'content-type': 'application/json', referer: `${storeUrl}/checkout/address`, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' }, json }, from: States.SUBMIT_INFORMATION }); }; ================================================ FILE: app/tasks/pokemon/classes/functions/encrypt.ts ================================================ import decode from 'jwt-decode'; import { Task } from '../../constants'; import { KeyId } from '../../utils/encrypt'; import { encryptCard } from '../../utils'; const { States } = Task; export const createEncryption = async ({ handler, keyId, microform, payment }: { handler: Function; keyId: string; microform: string; payment: any; }) => { const decoded: KeyId = decode(keyId); const { kid } = decoded.flx.jwk; const encryptedKeyId = await encryptCard(keyId, payment); return handler({ endpoint: 'https://flex.cybersource.com/flex/v2/tokens', options: { method: 'POST', headers: { accept: '*/*', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/jwt; charset=UTF-8', origin: 'https://flex.cybersource.com', referer: `https://flex.cybersource.com/cybersource/assets/microform/${microform}/iframe.html?keyId=${kid}`, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' }, body: encryptedKeyId }, includeHeaders: false, message: 'Encrypting session', from: States.CREATE_ENCRYPT }); }; ================================================ FILE: app/tasks/pokemon/classes/functions/index.ts ================================================ import { getCookie } from './datadome'; import { getSession } from './session'; import { getProduct, getProducts } from './product'; import { addToCart, clearCart } from './cart'; import { submitEmail } from './email'; import { submitInformation } from './information'; import { createEncryption } from './encrypt'; import { getKeyId, submitPayment } from './payment'; import { submitCheckout } from './checkout'; import { submitCaptcha } from './captcha'; export { getCookie, getSession, getProduct, getProducts, addToCart, submitEmail, submitInformation, clearCart, getKeyId, createEncryption, submitPayment, submitCheckout, submitCaptcha }; ================================================ FILE: app/tasks/pokemon/classes/functions/information.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const submitInformation = ({ handler, storeUrl, json }: { handler: Function; storeUrl: string; json: any; }) => handler({ endpoint: `/tpci-ecommweb-api/address?format=zoom.nodatalinks`, options: { method: 'POST', headers: { 'content-type': 'application/json', referer: `${storeUrl}/checkout/address`, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' }, json }, message: 'Submitting information', from: States.SUBMIT_INFORMATION }); ================================================ FILE: app/tasks/pokemon/classes/functions/payment.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const getKeyId = ({ handler, storeName, storeUrl }: { handler: Function; storeName: string; storeUrl: string; }) => { let endpoint = `/tpci-ecommweb-api/payment/key?microform=true`; if (/ca/i.test(storeName)) { endpoint += `&locale=en-CA`; } else { endpoint += `&locale=en-US`; } let referer = `${storeUrl}`; if (/ca/i.test(storeName)) { referer += `/en-ca/checkout/address`; } else { endpoint += `/checkout/address`; } return handler({ endpoint, options: { json: true, headers: { referer, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' } }, message: 'Generating token', from: States.GET_KEY_ID }); }; export const submitPayment = ({ handler, storeUrl, json }: { handler: Function; storeUrl: string; json: any; }) => { return handler({ endpoint: '/tpci-ecommweb-api/payment?microform=true&format=nodatalinks', options: { method: 'POST', headers: { 'content-type': 'application/json', referer: `${storeUrl}/checkout/payment`, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' }, json }, message: 'Submitting payment', from: States.SUBMIT_PAYMENT }); }; ================================================ FILE: app/tasks/pokemon/classes/functions/product.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const getProduct = ({ handler, url, sku }: { handler: Function; url: string; sku: string; }) => { return handler({ endpoint: `/tpci-ecommweb-api/product/${sku}?format=zoom.nodatalinks`, options: { json: true, headers: { 'content-type': 'application/json', referer: `${url}/product/${sku}`, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' } }, message: 'Visiting product', from: States.GET_PRODUCT }); }; export const getProducts = ({ handler }: { handler: Function }) => handler({ endpoint: `/tpci-ecommweb-api/search?ref_url=&rows=100&start=0&url=www.pokemoncenter.com&fl=pid&search_type=keyword`, options: { json: true, headers: { pragma: 'no-cache', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin' } }, message: 'Visiting products', from: States.GET_PRODUCTS }); ================================================ FILE: app/tasks/pokemon/classes/functions/session.ts ================================================ import { Task } from '../../constants'; const { States } = Task; export const getSession = ({ handler }: { handler: Function }) => { return handler({ endpoint: '/tpci-ecommweb-api/order?type=status&format=zoom.nodatalinks', message: 'Visiting homepage', from: States.GET_SESSION }); }; ================================================ FILE: app/tasks/pokemon/classes/tasks/base.ts ================================================ /* eslint-disable no-restricted-syntax */ import decode from 'jwt-decode'; import qs from 'query-string'; import { isEmpty } from 'lodash'; import isHtml from 'is-html'; import { load } from 'cheerio'; import uuid from 'uuid'; import { emitEvent, waitForDelay, isNetworkError, isImproperStatusCode, toTitleCase, isTimeout, request } from '../../../common/utils'; import { BaseTask } from '../../../common/classes'; import { Platforms, SiteKeyForPlatform } from '../../../common/constants'; import { PokemonContext } from '../../../common/contexts'; import { getCookie, getSession, getProduct, getProducts, addToCart, submitEmail, submitInformation, clearCart, getKeyId, createEncryption, submitPayment, submitCheckout, submitCaptcha } from '../functions'; import { getHeaders, getCardType, pickVariant, userAgents } from '../../utils'; import { email, information } from '../../utils/forms'; import { Task } from '../../constants'; import { Product, Products, Cart, Information, Key, Payment, Success, DatadomeData, DatadomeCookie } from '../../types'; import { JsonWebTokenInfo } from '../../utils/encrypt'; import CAPTCHA_TYPES from '../../../../utils/captchaTypes'; const { States, Modes } = Task; export class BasePokemonTask extends BaseTask { context: PokemonContext; active: 0 | 1; microform: string; currency: string; formatter: string; preloadUri: string; preloadSku: string; atcUri: string; cartUri: string; purchaseUri: string; keyId: string; paymentKey: string; paymentToken: string; product: any; authToken: string; proceedTo: string | null; swapped: boolean; data: DatadomeData; cid: string; attempts: number; preloaded: boolean; added: boolean; submitted: boolean; products: string[]; waitForCookieTimeout: number; captchaCookie: string; retries: number; agent: any; purgeCookie: boolean; email: boolean; needsCookie: boolean; responsePage: string; jsType: string; ddv: string; constructor(context: PokemonContext, platform = Platforms.Pokemon) { super(context, States.GET_SESSION, platform); this.active = 0; this.microform = '0.11.3'; this.currency = 'USD'; this.formatter = 'en-US'; this.context = context; this.preloadUri = ''; this.preloadSku = ''; this.cartUri = ''; this.atcUri = ''; this.purchaseUri = ''; this.keyId = ''; this.paymentKey = ''; this.paymentToken = ''; this.product = {}; this.authToken = ''; this.proceedTo = null; this.cid = ''; this.preloaded = this.context.task.mode !== Modes.PRELOAD; this.added = false; this.needsCookie = true; this.submitted = false; this.swapped = false; this.attempts = 0; this.data = {} as DatadomeData; this.products = []; this.waitForCookieTimeout = 0; this.captchaCookie = ''; this.retries = 0; this.purgeCookie = false; this.email = false; this.responsePage = 'origin'; this.jsType = ''; this.ddv = '4.1.67'; this.setInitialCookies(); } setInitialCookies = () => { const { task: { store: { name, url } }, taskSession } = this.context; const correlationId = uuid(); const cookies = [ { name: 'correlationId', value: correlationId }, { name: 'nmstat', value: uuid() }, { name: 'complianceCookie', value: 'true' }, { name: 'amp_logged_in_status', value: 'N' }, { name: 'nmstat', value: uuid() }, { name: 'amp_correlationID', value: correlationId }, { name: 'amp_storefront', value: /ca/i.test(name) ? 'CA' : 'US' } ]; return Promise.allSettled( cookies.map(cookie => taskSession.cookies.set({ url, ...cookie })) ); }; inject = async (data: any) => { if (!isEmpty(data)) { Object.entries(data).map(([key, value]: [string, any]) => { if (key) { (this as any)[key] = value; } return null; }); } }; release = async (cookie: string) => { const { id, logger } = this.context; logger.log({ id, level: 'info', message: `Received datadome cookie: ${cookie}` }); this.captchaCookie = cookie; }; async injectRequester(data: DatadomeData) { const { id, captchaManager, task: { store: { sitekey } }, proxy } = this.context; this.context.setCaptchaToken(''); emitEvent(this.context, [id], { message: 'Waiting for captcha' }); captchaManager.insert({ id, type: CAPTCHA_TYPES.RECAPTCHA_V2, harvest: this.harvest as any, host: data.host, platform: Platforms.Pokemon, userAgent: userAgents[this.active], sitekey: sitekey || SiteKeyForPlatform[Platforms.Pokemon], proxy: proxy ? proxy.ip : undefined }); } extractDatadomeData(url: string): DatadomeData { const data = qs.parse( (url || '').replace('https://geo.captcha-delivery.com/captcha/', '') ); return { ...data, // @ts-ignore hsh: data?.hash, host: 'geo.captcha-delivery.com' }; } async removeDatadome(headers: any) { const { taskSession, task: { store: { url: storeUrl } } } = this.context; const setCookies = headers['set-cookie']; const dd = setCookies.find((c: string) => /datadome/i.test(c)); const ddVal = dd.split('datadome=')[1].split(';')[0]; const cooks = await taskSession.cookies.get({}); for (const cookie of cooks) { if (/datadome/i.test(cookie.name)) { // eslint-disable-next-line no-await-in-loop await taskSession.cookies.remove(storeUrl, 'datadome'); } } await taskSession.cookies.set({ url: storeUrl, domain: '.pokemoncenter.com', name: 'datadome', value: ddVal }); } async handleDatadome(body: any, headers: any) { const { id, proxy, task: { retry, store: { url: storeUrl } }, taskSession } = this.context; if (/Generated by cloudfront/i.test(body)) { emitEvent(this.context, [id], { message: 'Cloudfront error' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; const next = this.proceedTo; this.proceedTo = null; return next; } await this.removeDatadome(headers); if (this.retries > 2) { const cookies = await taskSession.cookies.get({}); for (const cookie of cookies) { if (/datadome/i.test(cookie.name)) { // eslint-disable-next-line no-await-in-loop await taskSession.cookies.remove(storeUrl, 'datadome'); } } this.needsCookie = true; this.retries = 0; return States.SWAP; } this.retries += 1; let url; try { if (typeof body === 'string') { if (isHtml(body)) { const $ = load(body, { normalizeWhitespace: true, xmlMode: false }); let ret = {} as DatadomeData; $('script').each((_, el) => { const text = $(el).html(); try { if (/var\sdd/i.test(text!)) { const match = /dd=(.*)/.exec(text!); const [, data] = match!; ret = JSON.parse(data.replace(/'/g, '"')); } } catch (e) { // noop... } }); const { host, cid, hsh, s, t } = ret; url = `https://${host}/captcha/?initialCid=${cid}&hash=${hsh}&t=${t}&s=${s}&referer=https://www.pokemoncenter.com/`; } ({ url } = JSON.parse(body)); } else { ({ url } = body); } } catch (err) { // noop.. } if (!url) { const next = this.proceedTo; this.proceedTo = null; return next; } const ddd = this.extractDatadomeData(url); if (ddd.t === 'bv') { emitEvent(this.context, [id], { message: 'Proxy banned, swapping...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; const cookies = await taskSession.cookies.get({}); for (const cookie of cookies) { if (/datadome/i.test(cookie.name)) { // eslint-disable-next-line no-await-in-loop await taskSession.cookies.remove(storeUrl, 'datadome'); } } this.needsCookie = true; this.retries = 0; return States.SWAP; } let cid = ''; const cookies = await taskSession.cookies.get({}); const cookie = cookies.find(cookie => cookie.name === 'datadome'); if (cookie) { cid = cookie.value; } const data = { ...ddd, cid, ccid: null, 'x-forwarded-for': proxy ? proxy.ip : undefined, parent_url: `${storeUrl}/`, referer: ddd?.referer || '/' }; this.data = data; this.purgeCookie = false; await this.injectRequester({ ...data }); return States.CAPTCHA; } async waitForCookie() { const { aborted, task: { store: { url } }, taskSession } = this.context; if (aborted) { return States.ABORT; } if (this.swapped) { this.swapped = false; return States.CAPTCHA; } // if we are waiting longer than a minute for a cookie, go back to atc if (this.waitForCookieTimeout > 120) { this.waitForCookieTimeout = 0; return States.ADD_TO_CART; } if (!this.captchaCookie) { this.delayer = waitForDelay(500, this.aborter.signal); await this.delayer; this.waitForCookieTimeout += 1; return States.WAIT_FOR_COOKIE; } await taskSession.cookies.remove(url, 'datadome'); await taskSession.cookies.set({ url, domain: '.pokemoncenter.com', name: 'datadome', value: this.captchaCookie }); this.captchaCookie = ''; this.waitForCookieTimeout = 0; if (this.proceedTo) { const next = this.proceedTo; this.proceedTo = null; return next; } return States.GET_SESSION; } async waitForCaptcha() { const { aborted } = this.context; if (aborted) { return States.ABORT; } if (!this.context.captchaToken) { this.delayer = waitForDelay(500, this.aborter.signal); await this.delayer; return States.CAPTCHA; } return States.SUBMIT_CAPTCHA; } async submitCaptcha() { const { id, logger, task: { retry, store: { url } }, captchaToken } = this.context; const { nextState, data } = await submitCaptcha({ handler: this.handler, url, data: this.data, userAgent: userAgents[this.active], captchaToken }); if (nextState) { logger.log({ id, level: 'error', message: `${States.SUBMIT_CAPTCHA} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.SUBMIT_CAPTCHA} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { this.attempts += 1; if (this.attempts > 5) { this.attempts = 0; return States.WAIT_FOR_COOKIE; } emitEvent(this.context, [id], { message: `Error submitting captcha [${statusCode}]` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.SUBMIT_CAPTCHA; } await this.extractDatadome(body); if (this.proceedTo) { const next = this.proceedTo; this.proceedTo = null; return next; } } async extractDatadome({ cookie }: DatadomeCookie) { const { task: { store: { url } }, taskSession } = this.context; const [, raw] = cookie.split('='); const [value] = raw.split(';'); await taskSession.cookies.remove(url, 'datadome'); await taskSession.cookies.set({ url, domain: '.pokemoncenter.com', name: 'datadome', value }); } async extractAuthToken(headers: any) { const { id, task: { retry } } = this.context; const cookies = headers['set-cookie']; if (!cookies) { emitEvent(this.context, [id], { message: 'Invalid auth token, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_SESSION; } for (const cookie of cookies) { const match = /{"access_token":"(.*?)",/i.exec(cookie); if (match) { const [, authToken] = match; this.authToken = authToken; break; } } if (!this.authToken) { emitEvent(this.context, [id], { message: 'Invalid auth token, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_SESSION; } if (!this.preloaded) { return States.GET_PRODUCTS; } return States.GET_PRODUCT; } async extractProduct({ _items, _definition, _availability, images }: Product) { const { id, logger, task: { retry, monitor, sizes, store: { url }, product: { variant } } } = this.context; try { const [{ _element }] = _items; const [{ thumbnail }] = images; const [item] = _definition; if (!_element) { emitEvent(this.context, [id], { message: 'Out of stock' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.GET_PRODUCT; } if (!this.preloaded) { const [{ state }] = _availability; if (state !== 'AVAILABLE') { const nextInLine = this.products.pop(); if (!nextInLine) { this.preloaded = true; return States.GET_PRODUCT; } this.preloadSku = nextInLine; return States.GET_PRODUCT; } } const chosen = pickVariant({ variants: _element, sizes, logger, id }); if (!chosen) { emitEvent(this.context, [id], { message: 'No variation matched, retrying...' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.GET_PRODUCT; } const { id: _id, uri, price, size } = chosen; this.product.id = _id; this.atcUri = uri; this.product.price = price; this.product.size = size; this.product.name = item['display-name'] || 'Unknown'; this.product.image = thumbnail; this.context.product.url = `${url}/product/${variant}`; emitEvent(this.context, [id], { productImage: `${thumbnail}`.startsWith('http') ? thumbnail : `${url}/${thumbnail}`, productImageHi: `${thumbnail}`.startsWith('http') ? thumbnail : `${url}/${thumbnail}`, productName: this.product.name, chosenSize: size }); } catch (e) { emitEvent(this.context, [id], { message: 'Error extracting product, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_PRODUCT; } return States.ADD_TO_CART; } async extractProducts({ response }: Products) { if (!response) { this.preloaded = true; return States.GET_PRODUCT; } const { docs } = response; this.products = docs.map(({ pid }) => pid); if (!docs) { this.preloaded = true; return States.GET_PRODUCT; } const [{ pid }] = docs; if (!pid) { this.preloaded = true; return States.GET_PRODUCT; } this.preloadSku = pid; return States.GET_PRODUCT; } async extractCartResponse({ messages, self }: Cart) { const { id, task: { mode, retry } } = this.context; if (messages.length !== 0) { emitEvent(this.context, [id], { message: 'Error adding to cart, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.ADD_TO_CART; } if (self?.uri) { this.cartUri = self.uri; } if (mode === Modes.PRELOAD && this.preloaded) { return States.CREATE_ENCRYPT; } this.added = true; return States.SUBMIT_INFORMATION; } async extractInformationResponse({ billing, shipping }: Information) { const { id, task: { retry } } = this.context; if (billing.messages.length !== 0 || shipping.messages.length !== 0) { emitEvent(this.context, [id], { message: 'Error submitting information, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.SUBMIT_INFORMATION; } this.submitted = true; return States.GET_KEY_ID; } async extractKeyId({ keyId }: Key) { const { id, task: { mode, retry } } = this.context; if (!keyId) { emitEvent(this.context, [id], { message: 'Invalid jwt key, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_KEY_ID; } this.keyId = keyId; if (mode === Modes.PRELOAD && !this.preloaded) { return States.CLEAR_CART; } return States.CREATE_ENCRYPT; } async extractPaymentKey(body: string) { const { id, task: { retry } } = this.context; if (!body) { emitEvent(this.context, [id], { message: 'Invalid payment key, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_KEY_ID; } this.paymentKey = body; const { jti }: JsonWebTokenInfo = decode(body); this.paymentToken = jti; return States.SUBMIT_PAYMENT; } async extractPurchaseForm({ self }: Payment) { const { id, task: { retry } } = this.context; const { uri } = self; if (!uri) { emitEvent(this.context, [id], { message: 'Invalid payment form, retrying...' }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.SUBMIT_PAYMENT; } this.purchaseUri = `${uri.replace('paymentmethods', 'purchases')}/form`; return States.SUBMIT_CHECKOUT; } async sendWebhook(success: boolean, body = {} as Success) { const { id, proxy, task: { oneCheckout, store: { url, name }, monitor, retry, mode, quantity }, product: { url: productUrl }, taskSession, checkoutManager, webhookManager, notificationManager, analyticsManager } = this.context; const { image, name: productName, size, price } = this.product; const usedProxy = proxy ? proxy.proxy : 'None'; let profileName; let cardType; const profile = this.retrieveProfile(); if (!profile) { profileName = 'Unknown'; cardType = 'Unknown'; } else { profileName = profile.name; cardType = profile.payment.type; } if (!success) { analyticsManager.log({ success: false, store: name, date: new Date().toUTCString(), order: 'N/A', product: toTitleCase(productName), price: `${price}`, card: profile.payment.card, email: profile.shipping.email, name: profile.shipping.name, proxy: usedProxy || 'None', size: size ? `${size}` : 'N/A' }); if (!this.webhookSent) { this.webhookSent = true; webhookManager.log({ mode: mode || Modes.NORMAL, proxy: proxy ? proxy.proxy : undefined, product: { name: toTitleCase(productName), price: `${this.product.price}`, image: `${image}`.startsWith('http') ? image : `${url}/${image}`, size: size ? `${size}` : 'N/A', url: productUrl }, store: { name, url }, delays: { monitor, retry }, profile: { name: profileName, type: cardType }, quantity }); } emitEvent(this.context, [id], { message: 'Payment failed, retrying...' }); this.context.setCaptchaToken(''); await taskSession.clearStorageData({}); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.CREATE_ENCRYPT; } const order = body['purchase-number']; if (body?.['monetary-total']) { this.product.price = body?.['monetary-total'][0].display; } analyticsManager.log({ success: true, store: name, date: new Date().toUTCString(), order: order || 'Unknown', product: toTitleCase(productName), price: `${this.product.price}`, card: profile.payment.card, email: profile.shipping.email, name: profile.shipping.name, proxy: usedProxy || 'None', size: size ? `${size}` : 'N/A' }); emitEvent(this.context, [id], { message: 'Check email!' }); notificationManager.insert({ id, message: `Task ${id}: Check email!`, variant: 'success', type: 'DONE' }); if (oneCheckout) { checkoutManager.check({ context: this.context }); } webhookManager.log({ success: true, mode: mode || Modes.NORMAL, proxy: proxy ? proxy.proxy : undefined, product: { name: toTitleCase(productName), price: `${this.product.price}`, image: `${image}`.startsWith('http') ? image : `${url}/${image}`, size: size ? `${size}` : 'N/A', url: productUrl }, store: { name, url }, delays: { monitor, retry }, profile: { name: profileName, type: cardType }, quantity }); return States.DONE; } handler = async ({ endpoint, options = {}, message = '', from = this.prevState, timeout = 12500, includeHeaders = true }: { endpoint: string; options?: any; message?: string; from?: string; timeout?: number; includeHeaders?: boolean; }) => { const { id, aborted, logger, proxy, task: { store: { url, name } }, taskSession } = this.context; if (aborted) { return { nextState: States.ABORT }; } if (message) { emitEvent(this.context, [id], { message }); } const baseOptions = { proxy: proxy ? proxy.proxy : undefined, followAllRedirects: false, followRedirect: false, timeout }; const requestHeaders = includeHeaders ? { ...getHeaders(name), ...options.headers } : { ...options.headers }; requestHeaders['user-agent'] = userAgents[this.active]; const toRequest = // eslint-disable-next-line no-nested-ternary endpoint.indexOf('http') > -1 ? endpoint : endpoint.startsWith('/') ? `${url}${endpoint}` : `${url}/${endpoint}`; const opts = { ...baseOptions, ...options, url: toRequest, headers: requestHeaders }; try { const res = await request(taskSession, opts); let redirect; if (opts.followRedirect) { redirect = res?.request?.uri?.href; } else { redirect = res?.headers?.location; } logger.log({ id, level: 'silly', message: `${from} REDIRECT: ${redirect}` }); if (!redirect) { return { data: res }; } return { data: res, redirect }; } catch (error) { logger.log({ id, level: 'error', message: `${from} error: ${(error as any)?.message || 'Unknown'}` }); if (isTimeout(error)) { emitEvent(this.context, [id], { message: `Error ${message.toLowerCase()} [TIMEOUT]` }); return { data: {}, nextState: from }; } if (isNetworkError(error)) { emitEvent(this.context, [id], { message: `Error ${message.toLowerCase()} [NETWORK]` }); return { data: {}, nextState: from }; } emitEvent(this.context, [id], { message: `Error ${message.toLowerCase()} [UNKNOWN]` }); return { data: {}, nextState: from }; } }; async getCookie() { const { id, logger, task: { retry } } = this.context; const { nextState, data } = await getCookie({ handler: this.handler, userAgent: userAgents[this.active], ddv: this.ddv, cid: 'null', jsType: 'ch', responsePage: this.responsePage }); const { statusCode, body, headers } = data; if (nextState) { logger.log({ id, level: 'error', message: `${States.GET_COOKIE} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.GET_COOKIE} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { emitEvent(this.context, [id], { message: `Error retrieving cookie [${statusCode}]` }); if (statusCode === 403) { this.proceedTo = States.GET_COOKIE; return this.handleDatadome(body, headers); } this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_COOKIE; } this.retries = 0; await this.extractDatadome(body); this.needsCookie = false; if (this.proceedTo) { const next = this.proceedTo; this.proceedTo = null; return next; } return States.GET_SESSION; } async getSession() { const { id, logger, task: { retry } } = this.context; if (this.needsCookie) { this.proceedTo = States.GET_SESSION; return States.GET_COOKIE; } const { nextState, data } = await getSession({ handler: this.handler }); const { statusCode, body, headers } = data; if (nextState) { logger.log({ id, level: 'error', message: `${States.GET_SESSION} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.GET_SESSION} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { emitEvent(this.context, [id], { message: `Error visiting homepage [${statusCode}]` }); if (statusCode === 403) { this.proceedTo = States.GET_SESSION; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_SESSION; } this.retries = 0; return this.extractAuthToken(headers); } async getProducts() { const { id, logger, task: { retry } } = this.context; if (this.needsCookie) { this.proceedTo = States.GET_PRODUCTS; return States.GET_COOKIE; } const { nextState, data } = await getProducts({ handler: this.handler }); if (nextState) { logger.log({ id, level: 'error', message: `${States.GET_PRODUCTS} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.GET_PRODUCTS} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { emitEvent(this.context, [id], { message: `Error visiting products [${statusCode}]` }); if (statusCode === 403) { this.proceedTo = States.GET_PRODUCTS; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_PRODUCTS; } this.retries = 0; return this.extractProducts(body); } async getProduct() { const { id, logger, task: { retry, store: { url }, product: { variant } } } = this.context; if (this.needsCookie) { this.proceedTo = States.GET_PRODUCT; return States.GET_COOKIE; } const { nextState, data } = await getProduct({ handler: this.handler, url, sku: !this.preloaded ? this.preloadSku : variant }); if (nextState) { logger.log({ id, level: 'error', message: `${States.GET_PRODUCT} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.GET_PRODUCT} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { emitEvent(this.context, [id], { message: `Error visiting product [${statusCode}]` }); if (statusCode === 403) { this.proceedTo = States.GET_PRODUCT; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_PRODUCT; } this.retries = 0; return this.extractProduct(body); } async addToCart() { const { id, logger, task: { retry, monitor, quantity } } = this.context; if (this.needsCookie) { this.proceedTo = States.ADD_TO_CART; return States.GET_COOKIE; } const { nextState, data } = await addToCart({ handler: this.handler, quantity, atcUri: this.atcUri }); if (nextState) { logger.log({ id, level: 'error', message: `${States.ADD_TO_CART} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.ADD_TO_CART} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { if (statusCode === 400) { emitEvent(this.context, [id], { message: 'Out of stock' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.ADD_TO_CART; } if (statusCode === 403) { this.proceedTo = States.ADD_TO_CART; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } emitEvent(this.context, [id], { message: `Error adding to cart [${statusCode}]` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.ADD_TO_CART; } this.retries = 0; return this.extractCartResponse(body); } async submitInformation() { const { id, logger, task: { retry, store: { url } } } = this.context; if (this.needsCookie) { this.proceedTo = States.SUBMIT_INFORMATION; return States.GET_COOKIE; } const profile = this.retrieveProfile(); if (!profile) { emitEvent(this.context, [id], { message: `Profile not found` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.SUBMIT_INFORMATION; } const { matches, shipping, billing } = profile; if (!this.email) { await submitEmail({ handler: this.handler, storeUrl: url, json: email(billing) }); } this.email = true; const { nextState, data } = await submitInformation({ handler: this.handler, storeUrl: url, json: information(matches, shipping, billing) }); if (nextState) { logger.log({ id, level: 'error', message: `${States.SUBMIT_INFORMATION} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.SUBMIT_INFORMATION} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { emitEvent(this.context, [id], { message: `Error submitting information [${statusCode}]` }); if (statusCode === 403) { this.proceedTo = States.SUBMIT_INFORMATION; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.SUBMIT_INFORMATION; } this.retries = 0; return this.extractInformationResponse(body); } async clearCart() { const { id, logger, task: { retry } } = this.context; if (this.needsCookie) { this.proceedTo = States.CLEAR_CART; return States.GET_COOKIE; } const { nextState, data } = await clearCart({ handler: this.handler, cartUri: this.cartUri }); if (nextState) { logger.log({ id, level: 'error', message: `${States.CLEAR_CART} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.CLEAR_CART} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { if (statusCode === 403) { this.proceedTo = States.CLEAR_CART; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } emitEvent(this.context, [id], { message: `Error clearing cart [${statusCode}]` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.CLEAR_CART; } if (statusCode === 204) { this.retries = 0; this.preloaded = true; return States.GET_PRODUCT; } emitEvent(this.context, [id], { message: `Error clearing cart [${statusCode}]` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.CLEAR_CART; } async getKeyId() { const { id, logger, task: { retry, store: { name, url } } } = this.context; if (this.needsCookie) { this.proceedTo = States.GET_KEY_ID; return States.GET_COOKIE; } const { nextState, data } = await getKeyId({ handler: this.handler, storeName: name, storeUrl: url }); if (nextState) { logger.log({ id, level: 'error', message: `${States.GET_KEY_ID} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.GET_KEY_ID} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { emitEvent(this.context, [id], { message: `Error generating token [${statusCode}]` }); if (statusCode === 403) { this.proceedTo = States.GET_KEY_ID; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.GET_KEY_ID; } this.retries = 0; return this.extractKeyId(body); } async createEncryption() { const { id, logger, task: { retry } } = this.context; const profile = this.retrieveProfile(); if (!profile) { emitEvent(this.context, [id], { message: `Profile not found` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.CREATE_ENCRYPT; } const { payment } = profile; const { nextState, data } = await createEncryption({ handler: this.handler, keyId: this.keyId, microform: this.microform, payment }); if (nextState) { logger.log({ id, level: 'error', message: `${States.CREATE_ENCRYPT} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.CREATE_ENCRYPT} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { if (statusCode === 429) { return this.sendWebhook(false); } emitEvent(this.context, [id], { message: `Error encrypting session [${statusCode}]` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.CREATE_ENCRYPT; } this.retries = 0; return this.extractPaymentKey(body); } async submitPayment() { const { id, logger, task: { retry, store: { url } } } = this.context; if (this.needsCookie) { this.proceedTo = States.SUBMIT_PAYMENT; return States.GET_COOKIE; } const profile = this.retrieveProfile(); if (!profile) { emitEvent(this.context, [id], { message: `Profile not found` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.SUBMIT_PAYMENT; } const { payment } = profile; const { exp, type } = payment; const [month, year] = exp.split('/'); const { nextState, data } = await submitPayment({ handler: this.handler, storeUrl: url, json: { paymentDisplay: `${getCardType(type)} ${month}/20${year}`, paymentKey: this.keyId, paymentToken: this.paymentToken } }); if (nextState) { logger.log({ id, level: 'error', message: `${States.SUBMIT_PAYMENT} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; logger.log({ id, level: statusCode >= 400 ? 'error' : 'info', message: `${States.SUBMIT_PAYMENT} statusCode: ${statusCode}` }); if (isImproperStatusCode(statusCode)) { emitEvent(this.context, [id], { message: `Error submitting payment [${statusCode}]` }); if (statusCode === 403) { this.proceedTo = States.SUBMIT_PAYMENT; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return States.SUBMIT_PAYMENT; } this.retries = 0; return this.extractPurchaseForm(body); } async submitCheckout() { const { id, logger, task: { retry, store: { url } } } = this.context; if (this.needsCookie) { this.proceedTo = States.SUBMIT_CHECKOUT; return States.GET_COOKIE; } const { nextState, data } = await submitCheckout({ handler: this.handler, storeUrl: url, purchaseUri: this.purchaseUri }); if (nextState) { logger.log({ id, level: 'error', message: `${States.SUBMIT_CHECKOUT} nextState: ${nextState}` }); this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, body, headers } = data; if (isImproperStatusCode(statusCode)) { if (statusCode === 403) { this.proceedTo = States.SUBMIT_CHECKOUT; if (this.responsePage === 'origin') { this.responsePage = 'blocked-page'; await this.removeDatadome(headers); return States.GET_COOKIE; } return this.handleDatadome(body, headers); } return this.sendWebhook(false); } return this.sendWebhook(true, body); } async handleStepLogic(currentState: string) { const { id, logger } = this.context; if (this.anticrack && currentState === States.SUBMIT_CHECKOUT) { // eslint-disable-next-line no-param-reassign currentState = States.NOOP; } const stepMap = { [States.GET_COOKIE]: this.getCookie, [States.WAIT_FOR_COOKIE]: this.waitForCookie, [States.CAPTCHA]: this.waitForCaptcha, [States.SUBMIT_CAPTCHA]: this.submitCaptcha, [States.GET_SESSION]: this.getSession, [States.GET_PRODUCT]: this.getProduct, [States.GET_PRODUCTS]: this.getProducts, [States.ADD_TO_CART]: this.addToCart, [States.SUBMIT_INFORMATION]: this.submitInformation, [States.CLEAR_CART]: this.clearCart, [States.GET_KEY_ID]: this.getKeyId, [States.CREATE_ENCRYPT]: this.createEncryption, [States.SUBMIT_PAYMENT]: this.submitPayment, [States.SUBMIT_CHECKOUT]: this.submitCheckout, [States.WAIT_FOR_COOKIE]: this.waitForCookie, [States.NOOP]: this.noop, [States.SWAP]: this.swap, [States.DONE]: () => States.DONE, [States.ERROR]: () => States.DONE, [States.ABORT]: () => States.DONE }; // filter out captcha state... if ( currentState !== States.CAPTCHA && currentState !== States.WAIT_FOR_COOKIE ) { logger.log({ id, level: 'silly', message: `Handling task state: ${currentState}` }); } const defaultHandler = () => { const handler: any = stepMap[this.prevState]; if (!handler) { throw new Error('Reached unknown state!'); } return handler.call(this); }; const handler: any = stepMap[currentState] || defaultHandler; return handler.call(this); } } ================================================ FILE: app/tasks/pokemon/classes/tasks/index.ts ================================================ import { BasePokemonTask } from './base'; import { PokemonContext } from '../../../common/contexts'; export const choosePokemonTask = () => { return (context: PokemonContext) => new BasePokemonTask(context); }; ================================================ FILE: app/tasks/pokemon/constants/index.ts ================================================ import { PokemonTypes } from '../../../constants'; import { Task as TaskConstants } from '../../common/constants'; const CheckoutStates = { ...TaskConstants.States, GET_COOKIE: 'GET_COOKIE', WAIT_FOR_COOKIE: 'WAIT_FOR_COOKIE', GET_CAPTCHA: 'GET_CAPTCHA', SOLVE_CAPTCHA: 'SOLVE_CAPTCHA', SUBMIT_CAPTCHA: 'SUBMIT_CAPTCHA', GET_SESSION: 'GET_SESSION', GET_PRODUCTS: 'GET_PRODUCTS', GET_PRODUCT: 'GET_PRODUCT', GET_STOCK: 'GET_STOCK', ADD_TO_CART: 'ADD_TO_CART', CLEAR_CART: 'CLEAR_CART', SUBMIT_INFORMATION: 'SUBMIT_INFORMATION', GET_CHECKOUT: 'GET_CHECKOUT', GET_KEY_ID: 'GET_KEY_ID', CREATE_ENCRYPT: 'CREATE_ENCRYPT', SUBMIT_PAYMENT: 'SUBMIT_PAYMENT', SUBMIT_CHECKOUT: 'SUBMIT_CHECKOUT' }; const Task = { States: CheckoutStates, Modes: PokemonTypes }; export { Task }; ================================================ FILE: app/tasks/pokemon/index.ts ================================================ import { choosePokemonTask } from './classes/tasks'; export { choosePokemonTask }; ================================================ FILE: app/tasks/pokemon/types/cart.d.ts ================================================ type link = { rel: string; rev: string; type: string; uri: string; href: string; }; export type Cart = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; configuration: any; 'is-freegiftitem': boolean; quantity: number; }; ================================================ FILE: app/tasks/pokemon/types/checkout.d.ts ================================================ type link = { rel: string; rev: string; type: string; uri: string; href: string; }; type message = { type: string; id: string; 'debug-message': string; data: { casue: string; }; }; export type Checkout = { messages: message[]; links: link[]; }; ================================================ FILE: app/tasks/pokemon/types/datadome.d.ts ================================================ export type DatadomeData = { initialCid: string; cid: string; hsh: string; t: string; s: string; host: string; referer: string; site: string; }; export type DatadomeCookie = { cookie: string; }; ================================================ FILE: app/tasks/pokemon/types/geetest.d.ts ================================================ export interface Geetest { geetestResponseChallenge: string; geetestResponseSeccode: string; geetestResponseValidate: string; captchaInScriptChallenge: number; ip: string; tts: number; } ================================================ FILE: app/tasks/pokemon/types/index.d.ts ================================================ import { Product } from './product'; import { Products, ProductsDocument } from './products'; import { Cart } from './cart'; import { Information } from './information'; import { Key } from './key'; import { Order } from './order'; import { Token } from './token'; import { Payment } from './payment'; import { Checkout } from './checkout'; import { Success } from './success'; import { Geetest } from './geetest'; import { DatadomeData, DatadomeCookie } from './datadome'; export { Product, Products, ProductsDocument, Cart, Information, Key, Order, Token, Payment, Checkout, Success, Geetest, DatadomeData, DatadomeCookie }; ================================================ FILE: app/tasks/pokemon/types/information.d.ts ================================================ type link = { rel: string; rev: string; type: string; uri: string; href: string; }; type address = { 'country-name': 'US'; 'extended-address': null; locality: string; organization: null; 'phone-number': string; 'postal-code': string; region: string; 'street-address': string; }; type name = { 'family-name': string; 'given-name': string; }; export type Information = { billing: { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; address: address; name: name; }; shipping: { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; address: address; name: name; }; }; ================================================ FILE: app/tasks/pokemon/types/key.d.ts ================================================ export type Key = { keyId: string; }; ================================================ FILE: app/tasks/pokemon/types/order.d.ts ================================================ type link = { rel: string; rev: string; type: string; uri: string; href: string; }; type self = { type: string; uri: string; href: string; }; type address = { 'country-name': 'US'; 'extended-address': null; locality: string; organization: null; 'phone-number': string; 'postal-code': string; region: string; 'street-address': string; }; type name = { 'family-name': string; 'given-name': string; }; type cost = { amount: number; currency: string; display: string; }; type billingaddress = { self: self; messages: any[]; links: link[]; address: address; name: name; }; type billingaddressinfo = { _billingaddress: billingaddress[]; }; type destination = { self: self; messages: any[]; links: link[]; address: address; name: name; }; type destinationinfo = { _destination: destination[]; _shippingoptioninfo: shippingoptioninfo[]; }; type deliveries = { _element: destinationinfo[]; }; type choice = { self: self; messages: any[]; links: link[]; _description: [ { self: self; messages: any[]; links: link[]; carrier: string; cost: cost[]; 'display-name': string; name: string; } ]; }; type selector = { _choice: choice[]; }; type shippingoption = { self: self; messages: any[]; links: link[]; carrier: string; cost: cost[]; 'display-name': string; name: string; }; type shippingoptioninfo = { _selector: selector[]; _shippingoption: shippingoption[]; }; type paymentmethod = { self: self; messages: any[]; links: link[]; 'display-name': string; token: string; }; type paymentmethodinfo = { _paymentmethod: paymentmethod[]; }; type subtotal = { self: self; messages: any[]; links: link[]; cost: cost[]; discount: cost[]; }; type tax = { self: self; messages: any[]; links: link[]; cost: cost[]; total: cost; }; type total = { self: self; messages: any[]; links: link[]; cost: cost[]; }; type order = { self: self; messages: any[]; links: link[]; _billingaddressinfo: billingaddressinfo[]; _deliveries: deliveries[]; _paymentmethodinfo: paymentmethodinfo[]; _subtotal: subtotal[]; _tax: tax[]; _total: total[]; }; export type Order = { self: self; messages: any[]; links: link[]; _order: order[]; 'total-quantity': number; }; ================================================ FILE: app/tasks/pokemon/types/payment.d.ts ================================================ type link = { rel: string; rev: string; type: string; uri: string; href: string; }; export type Payment = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; 'display-name': string; token: string; }; ================================================ FILE: app/tasks/pokemon/types/product.d.ts ================================================ type link = { rel: string; rev: string; type: string; uri: string; href: string; }; type availability = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; state: 'AVAILABLE' | 'UNAVAILBLE'; }; type code = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; code: string; }; type detail = { 'display-name': string; 'display-value': string; name: string; value: string | number | boolean; }; type definition = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; details: detail[]; 'display-name': string; 'reporting-properties': { crumb: string; 'product-name': string; }; }; type addtocartform = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; configuration: any; quantity: number; }; type addtowishlistform = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; }; type availability = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; state: 'AVAILABLE' | 'UNAVAILABLE'; }; type code = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; code: string; }; type priceItem = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; 'list-price': price[]; 'purchase-price': price[]; }; type variantDefinition = { _options: { _element: { _value: { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; 'display-name': string; name: string; }[]; }[]; }[]; }; export type element = { _addtocartform: addtocartform[]; _addtowishlistform: addtowishlistform[]; _availability: availability[]; _code: code[]; _definition: variantDefinition[]; _price: priceItem[]; }; type item = { _element: element[]; }; type price = { amount: number; currency: 'USD'; // add more once we see them display: string; }; type priceRange = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; 'list-price-range': { 'from-price': price[]; 'to-price': price[]; }; 'purchase-price-range': { 'from-price': price[]; 'to-price': price[]; }; }; type image = { high: string; original: string; thumbnail: string; }; export type Product = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; _availability: availability[]; _code: code[]; _definition: definition[]; _items: item[]; _pricerange: priceRange[]; images: image[]; }; ================================================ FILE: app/tasks/pokemon/types/products.d.ts ================================================ /* eslint-disable camelcase */ export interface Products { response: Response; facet_counts: FacetCounts; category_map: CategoryMap; } export interface Response { numFound: number; start: number; docs: ProductsDocument[]; } export interface ProductsDocument { sale_price: number; price: number; launch_date: string; pid: string; currency: string; reporting_product_name: string; thumb_image: string; PRF: string[]; title: string; description: string; display_price: string; brand: string; sale_price_range: number[]; price_range: number[]; display_sale_price: string; url: string; reporting_crumb: string; best_seller: number; variants: Variant[]; } export interface Variant { reporting_crumb: string[]; reporting_product_name: string[]; sku_swatch_images: string[]; sku_thumb_images: string[]; } export interface FacetCounts { facet_ranges: FacetRanges; facet_fields: FacetFields; facet_queries: FacetQueries; } export interface FacetRanges {} export interface FacetFields { category: Category[]; sizes: any[]; brand: Brand[]; colors: any[]; color_groups: any[]; crumbs_id: CrumbsId[]; 'Size Name': SizeName[]; catalog_code: CatalogCode[]; view_id: ViewId[]; PreorderReAuthDelta: PreorderReAuthDelum[]; OFFER_TYPE: OfferType[]; Buyable: Buyable[]; 'Item Category 2': ItemCategory2[]; 'Item Category 1': ItemCategory1[]; 'Item Category 3': ItemCategory3[]; 'Purchase Quantity Limit': PurchaseQuantityLimit[]; 'Is Pre-Order Item': IsPreOrderItem[]; 'Pre-Order Inventory Limit': PreOrderInventoryLimit[]; NOT_SOLD_SEPARATELY: NotSoldSeparately[]; 'Recommended Age': RecommendedAge[]; MSRP: Msrp[]; 'Authorized to Sell': AuthorizedToSell[]; ITEM_TYPE: ItemType[]; Displayable: Displayable[]; MINIMUM_ORDER_QUANTITY: MinimumOrderQuantity[]; } export interface Category { count: number; crumb: string; cat_name: string; parent: string; cat_id: string; tree_path: string; } export interface Brand { count: number; name: string; } export interface CrumbsId { count: number; name: string; } export interface SizeName { count: number; name: string; } export interface CatalogCode { count: number; name: string; } export interface ViewId { count: number; name: string; } export interface PreorderReAuthDelum { count: number; name: string; } export interface OfferType { count: number; name: string; } export interface Buyable { count: number; name: string; } export interface ItemCategory2 { count: number; name: string; } export interface ItemCategory1 { count: number; name: string; } export interface ItemCategory3 { count: number; name: string; } export interface PurchaseQuantityLimit { count: number; name: string; } export interface IsPreOrderItem { count: number; name: string; } export interface PreOrderInventoryLimit { count: number; name: string; } export interface NotSoldSeparately { count: number; name: string; } export interface RecommendedAge { count: number; name: string; } export interface Msrp { count: number; name: string; } export interface AuthorizedToSell { count: number; name: string; } export interface ItemType { count: number; name: string; } export interface Displayable { count: number; name: string; } export interface MinimumOrderQuantity { count: number; name: string; } export interface FacetQueries {} export interface CategoryMap { 'S0106-0002-0000': string; 'Extraordinary-Gifts': string; 'eevee-sweet-choices': string; 'pokemon-sports': string; 'outdoors-collection': string; starters: string; 'sword-shield': string; 'galar-legendary': string; 'S0102-0000-0000': string; 'S0103-0001-0002': string; 'S0102-0005-0002': string; 'S0102-0005-0003': string; 'S0102-0005-0000': string; 'S0102-0005-0001': string; 'S0103-0001-0001': string; 'S0102-0005-0005': string; 'S0101-0001-0002': string; 'grass-type-collection': string; 'ghost-type': string; 'pokemon-go': string; 'dragon-majesty': string; ludicolo: string; 'S0105-0000-0000': string; 'detective-pikachu': string; 'cosmic-eclipse': string; 'S0108-0001-0000': string; 'S0108-0001-0001': string; 'S0108-0001-0002': string; 'S0108-0001-0003': string; 'S0108-0001-0004': string; 'Gifts-Under-25': string; 'ground-type': string; 'S0103-0007-0000': string; galar: string; 'S0102-0009-0000': string; 'S0105-0001-0001': string; 'S0105-0001-0000': string; 'S0105-0001-0003': string; 'S0105-0001-0002': string; 'S0105-0001-0005': string; 'S0105-0001-0004': string; 'S0105-0001-0007': string; 'S0105-0001-0006': string; 'highest-rated': string; 'S0102-0006-0000': string; 'S0103-0000-0000': string; jewelry: string; 'Squishy Plush': string; Figma: string; 'S0101-0001-0005': string; 'S0101-0001-0004': string; 'S0101-0001-0006': string; 'S0101-0001-0001': string; 'S0101-0001-0000': string; 'S0101-0001-0003': string; 'S0103-0001-0000': string; 'S0103-0002-0002': string; 'S0103-0002-0003': string; 'S0103-0002-0000': string; 'S0103-0002-0001': string; Funko: string; johto: string; 'Ultra Prism': string; 'S0103-0003-0003': string; 'S0102-0003-0000': string; 'S0105-0004-0000': string; 'steel-type': string; kitchen: string; 'Detective Pikachu TCG': string; 'psychic-type': string; 'unbroken-bonds': string; valentines: string; graduation: string; 'Detective Pikachu Plush': string; 'Cuddly Plush': string; 'rebel-clash': string; 'darkness-ablaze': string; jackets: string; 'hidden-fates': string; Holiday: string; 'S0101-0003-0000': string; 'pokemon-sunset': string; 'pokemon-accents': string; 'grass-type-first-partners': string; 'cozy-gifts': string; 'pokeball-classics': string; kanto: string; CharizardFury: string; 'S0108-0002-0001': string; 'S0108-0002-0000': string; 'S0108-0002-0003': string; 'school-essentials': string; 'S0108-0002-0005': string; 'S0108-0002-0004': string; 'vivid-voltage': string; hoenn: string; 'eevee-pixel': string; 'S0101-0002-0006': string; 'sliding-pins': string; 'S0101-0002-0000': string; 'S0101-0002-0001': string; 'S0101-0002-0002': string; 'S0101-0002-0003': string; 'S0103-0003-0001': string; 'S0103-0003-0000': string; 'top-character-gifts': string; 'S0103-0003-0002': string; 'Stocking-Stuffers': string; Ties: string; 'S0106-0000-0000': string; alola: string; 'relax-with-eevee': string; 'Home-Decoration': string; 'S0101-0005-0009': string; 'pikachu-classics': string; unova: string; 'S0108-0002-0002': string; 'S0101-0000-0000': string; 'S0102-0002-0000': string; 'S0101-0005-0001': string; 'S0101-0005-0000': string; 'S0101-0005-0003': string; 'S0101-0005-0002': string; 'S0101-0005-0005': string; 'S0101-0005-0004': string; 'S0101-0005-0007': string; 'S0101-0005-0006': string; 'S0102-0004-0000': string; books: string; loungewear: string; outdoors: string; 'S0103-0008-0000': string; 'S0101-0004-0000': string; 'Galar Collection': string; 'S0105-0002-0004': string; 'celestial-storm': string; 'champions-path': string; 'S0105-0002-0000': string; 'S0105-0002-0001': string; 'S0105-0002-0002': string; 'S0105-0002-0003': string; kalos: string; 'team-up': string; 'dragon-type': string; COLLECTIONS: string; KantoRegion: string; sinnoh: string; 'bear-walker': string; 'S0102-0010-0000': string; 'S0101-0005-0010': string; 'S0101-0005-0011': string; 'S0102-0001-0004': string; 'S0102-0001-0002': string; 'S0102-0001-0003': string; 'S0102-0001-0000': string; 'S0102-0001-0001': string; 'Shining Legends': string; 'eevee-cant-wait': string; 'Forbidden Light': string; 'S0105-0003-0000': string; 'Charizard Firestorm': string; shirts: string; 'lost-thunder': string; 'S0101-0002-0004': string; 'Halloween-Collection': string; 'S0101-0002-0005': string; 'S0102-0008-0000': string; 'Team Rocket': string; 'S0102-0011-0000': string; 'S0108-0000-0000': string; 'S0106-0001-0000': string; activewear: string; 'Legendary Pins': string; 'mystery-dungeon-plush': string; } ================================================ FILE: app/tasks/pokemon/types/success.d.ts ================================================ type link = { rel: string; rev: string; type: string; uri: string; href: string; }; type address = { 'country-name': 'US'; 'extended-address': null; locality: string; organization: null; 'phone-number': string; 'postal-code': string; region: string; 'street-address': string; }; type name = { 'family-name': string; 'given-name': string; }; type price = { amount: number; currency: 'USD'; // add more once we see them display: string; }; type tax = { amount: number; currency: 'USD'; // add more once we see them title: string; }; export type Success = { self: { type: string; uri: string; href: string; }; messages: any[]; links: link[]; 'billing-address': { address: address; name: name; }; 'monetary-total': price[]; 'payment-means': 'CREDITCARD'; 'payment-name': string; 'purchase-date': { 'display-value': string; value: number; }; 'purchase-number': string; 'shipping-destinations': { address: address; name: name }[]; 'shipping-options': { carrier: string; cost: price[]; 'display-name': string; name: string; }[]; status: string; 'tax-total': price; taxes: tax[]; }; ================================================ FILE: app/tasks/pokemon/types/token.d.ts ================================================ export type Token = { token: string; timestamp: number; }; ================================================ FILE: app/tasks/pokemon/utils/cards.ts ================================================ export const typeForProvider = (provider: string) => { if (/visa/i.test(provider)) { return '001'; } if (/master/i.test(provider)) { return '002'; } if (/amex|american/i.test(provider)) { return '003'; } if (/maestro/i.test(provider)) { return '042'; } if (/discover/i.test(provider)) { return '004'; } if (/diners/i.test(provider)) { return '005'; } if (/jcb/i.test(provider)) { return '007'; } if (/china|cup/i.test(provider)) { return '062'; } // default to visa? return '001'; }; ================================================ FILE: app/tasks/pokemon/utils/decode.ts ================================================ import { encrypt } from 'cs2-encryption'; import { typeForProvider } from './cards'; export const encryptCard = async (keyId: string, payment: any) => { const { card, exp, cvv, type: provider } = payment; const type = typeForProvider(provider); const [month, year] = exp.split('/'); return encrypt( { securityCode: cvv, number: card, type, expirationMonth: month, expirationYear: `20${year}` }, keyId ); }; ================================================ FILE: app/tasks/pokemon/utils/encrypt.ts ================================================ import { Crypto } from '@peculiar/webcrypto'; import btoa from 'btoa'; export type Card = { securityCode: string; number: string; type: string; expirationMonth: string; expirationYear: string; }; export type KeyId = { flx: { path: string; data: string; origin: string; jwk: { kty: string; e: string; use: string; n: string; kid: string; }; }; ctx: { data: { targetOrigins: string[]; mfOrigin: string; }; type: string; }[]; iss: string; exp: number; iat: number; jti: string; }; export type CardType = '001' | '002' | '042' | '004' | '005' | '007' | '062'; export type JsonWebTokenInfo = { data: { expirationYear: string; number: string; expirationMonth: string; type: CardType; }; iss: 'Flex/04'; exp: number; type: 'mf-0.11.0'; iat: number; jti: string; }; type Payload = { data: Card; context: string; index: number; }; type Header = { kid: string; alg: string; enc: string; }; type InitializationVector = Uint8Array; const arrayBufferToString = (buf: ArrayBuffer) => String.fromCharCode.apply(null, new Uint8Array(buf) as any); const stringToArrayBuffer = (str: string) => { const buffer = new ArrayBuffer(str.length); const array = new Uint8Array(buffer); const { length } = str; for (let r = 0; r < length; r += 1) { array[r] = str.charCodeAt(r); } return buffer; }; const replace = (str: string) => btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const generateKey = (crypto: Crypto): Promise => crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, ['encrypt'] ); const encrypt = async ( crypto: Crypto, payload: Payload, key: CryptoKey, header: Header, iv: InitializationVector ) => { const algorithm = { name: 'AES-GCM', iv, additionalData: stringToArrayBuffer(replace(JSON.stringify(header))), tagLength: 128 }; const buffer = await crypto.subtle.encrypt( algorithm, key, stringToArrayBuffer(JSON.stringify(payload)) ); return [buffer, key]; }; function importKey(crypto: Crypto, jsonWebKey: JsonWebKey) { return crypto.subtle.importKey( 'jwk', jsonWebKey, { name: 'RSA-OAEP', hash: { name: 'SHA-1' } }, false, ['wrapKey'] ); } async function wrapKey(crypto: Crypto, key: CryptoKey, jsonWebKey: JsonWebKey) { const wrappedKey = await importKey(crypto, jsonWebKey); return crypto.subtle.wrapKey('raw', key, wrappedKey, { name: 'RSA-OAEP', hash: { name: 'SHA-1' } }); } async function build( crypto: Crypto, buffer: ArrayBuffer, key: CryptoKey, iv: InitializationVector, header: Header, jsonWebKey: JsonWebKey ) { // eslint-disable-next-line no-bitwise const u = buffer.byteLength - ((128 + 7) >> 3); const keyBuffer = await wrapKey(crypto, key, jsonWebKey); return [ replace(JSON.stringify(header)), replace(arrayBufferToString(keyBuffer)), replace(arrayBufferToString(iv)), replace(arrayBufferToString(buffer.slice(0, u))), replace(arrayBufferToString(buffer.slice(u))) ].join('.'); } export const run = async ( card: Card, keyId: KeyId, token: string, radius = 0 ) => { const crypto = new Crypto(); const header: Header = { kid: keyId.flx.jwk.kid || '', alg: 'RSA-OAEP', enc: 'A256GCM' }; const payload: Payload = { data: card, context: token, index: radius }; const iv: InitializationVector = crypto.getRandomValues(new Uint8Array(12)); return generateKey(crypto) .then(key => encrypt(crypto, payload, key, header, iv)) .then(data => { const [buffer, key] = data; return build(crypto, buffer, key, iv, header, keyId.flx.jwk); }); }; ================================================ FILE: app/tasks/pokemon/utils/forms.ts ================================================ import formatter from 'phone-formatter'; export const information = (matches: boolean, shipping: any, billing: any) => { const { name: sName, country: sCountry, address: sAddress, city: sCity, phone: sPhone, province: sProvince, zip: sZip } = shipping; const { name: bName, country: bCountry, address: bAddress, city: bCity, phone: bPhone, province: bProvince, zip: bZip } = billing; const [sFirstName, sLastName] = sName.split(' '); const [bFirstName, bLastName] = bName.split(' '); if (matches) { return { billing: { familyName: sLastName, givenName: sFirstName, countryName: sCountry.value, locality: sCity, phoneNumber: formatter.format(sPhone, '(NNN) NNN-NNNN'), postalCode: sZip, region: sProvince ? sProvince.value : '', streetAddress: sAddress }, shipping: { familyName: sLastName, givenName: sFirstName, countryName: sCountry.value, locality: sCity, phoneNumber: formatter.format(sPhone, '(NNN) NNN-NNNN'), postalCode: sZip, region: sProvince ? sProvince.value : '', streetAddress: sAddress } }; } return { billing: { familyName: bLastName, givenName: bFirstName, countryName: bCountry.value, locality: bCity, phoneNumber: formatter.format(bPhone, '(NNN) NNN-NNNN'), postalCode: bZip, region: bProvince ? bProvince.value : '', streetAddress: bAddress }, shipping: { familyName: sLastName, givenName: sFirstName, countryName: sCountry.value, locality: sCity, phoneNumber: formatter.format(sPhone, '(NNN) NNN-NNNN'), postalCode: sZip, region: sProvince ? sProvince.value : '', streetAddress: sAddress } }; }; export const email = ({ email }: any) => ({ email }); ================================================ FILE: app/tasks/pokemon/utils/index.ts ================================================ import { userAgent, toTitleCase } from '../../common/utils'; import { pickVariant } from './pickVariant'; import { encryptCard } from './decode'; type UserAgents = { [index: number]: string; }; export const userAgents: UserAgents = { 0: userAgent, 1: `Mozilla/5.0 (compatible; MJ12bot/v1.3.${ Math.floor(Math.random() * 8) + 1 }; http://mj12bot.com/)` }; export const getHeaders = (name: string) => ({ Accept: '*/*', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': /ca/i.test(name) ? 'en-ca' : 'en-us', origin: 'https://www.pokemoncenter.com', 'x-store-scope': /ca/i.test(name) ? 'pokemon-ca' : 'pokemon' }); export const getCardType = (type: string) => { if (/visa/i.test(type)) { return 'Visa'; } if (/master/i.test(type)) { return 'MasterCard'; } if (/amex|american/i.test(type)) { return 'American Express'; } if (/discover/i.test(type)) { return 'Discover'; } if (/diners/i.test(type)) { return 'Diners Club'; } if (/jcb/i.test(type)) { return 'JCB'; } if (/union|cup/i.test(type)) { return 'China UnionPay'; } return toTitleCase(type); }; export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); export { pickVariant, encryptCard }; ================================================ FILE: app/tasks/pokemon/utils/pickVariant.ts ================================================ /* eslint-disable camelcase */ /* eslint-disable no-restricted-syntax */ import { shuffle } from 'lodash'; import { getRandomIntInclusive } from '../../common/utils'; import { element } from '../types/product'; export const sizeMatcherShoe = (sizes: string[]) => (value: string) => { for (const size of sizes) { const pattern = `^${size}(?!.)`; const re = new RegExp(pattern, 'i'); const parsed = `${value}`.replace(/^[^0-9]+/g, ''); if (re.test(parsed)) { return true; } } return false; }; type PickVariant = { variants: element[]; sizes: string[]; logger: any; id: string; }; export type PokemonVariant = { id: string; uri: string; price: string; size: string; available: boolean; }; export const pickVariant = ({ variants = [], sizes = [], logger, id }: PickVariant) => { const matches: PokemonVariant[] = []; const variantGroup: PokemonVariant[] = [...variants] .map(({ _addtocartform, _availability, _price, _code, _definition }) => { let id = '-'; let uri = '-'; let price = 'N/A'; let size = 'N/A'; let available = false; try { id = _code[0].code; ({ uri } = _addtocartform[0].self); price = _price[0]['purchase-price'][0].display || `${_price[0]['purchase-price'][0].amount}`; size = _definition[0]._options[0]._element[0]._value[0]['display-name']; available = _availability[0].state === 'AVAILABLE'; } catch (e) { // noop.. } return { id, uri, price, size, available }; }) .filter(Boolean); // let's just choose a random in stock variant if they just chose random sizing // this will at least guarantee they checkout if there is a size available if (sizes.length === 1 && sizes.some(size => /random/i.test(size))) { const inStockOptions = variantGroup.filter(({ available }) => available); // no variations in stock, let's return a random size.. if (!inStockOptions.length) { const rand = getRandomIntInclusive(0, variantGroup.length - 1); const variant = variantGroup[rand]; return variant; } // let's return a random option from the instock list const rand = getRandomIntInclusive(0, inStockOptions.length - 1); const variant = inStockOptions[rand]; return variant; } for (const variant of variantGroup) { // Determine if we are checking for shoe sizes or not let sizeMatcher; if (sizes.some(size => /[0-9]+/.test(size))) { // We are matching a shoe size sizeMatcher = sizeMatcherShoe(sizes); } else { // We are matching a garment size sizeMatcher = (s: string) => sizes.some(size => new RegExp(`^${size}`, 'i').test(`${s}`.trim())); } if (sizeMatcher(variant.size)) { logger.log({ id, level: 'debug', message: `Matched variant: ${variant.size}` }); matches.push(variant); } } // if we can't match a size at all and we don't want random, return null if (!matches.length && !sizes.some(size => /random/i.test(size))) { return null; } // if we only matched one variant, just use that no matter what if (matches.length === 1) { return matches[0]; } // otherwise, let's do some 'pseudo-random' shuffling const options = shuffle([...matches]); const inStockOptions = options.filter(({ available }) => available); if (!inStockOptions.length) { const rand = getRandomIntInclusive(0, matches.length - 1); const variant = matches[rand]; return variant; } const rand = getRandomIntInclusive(0, inStockOptions.length - 1); const variant = inStockOptions[rand]; return variant; }; ================================================ FILE: app/tasks/shopify/classes/functions/account.ts ================================================ import { Task as TaskConstants } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = TaskConstants; export const getAccount = ({ handler, context, current, aborter, delayer, storeUrl }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; storeUrl: string; }) => { return handler({ context, current, aborter, delayer, endpoint: '/account/login', options: { followRedirect: true, followAllRedirects: true, headers: { 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', dnt: '1', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', referer: `${storeUrl}/`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' } }, includeHeaders: false, message: 'Visiting account', from: States.GET_ACCOUNT }); }; export const submitAccount = ({ handler, context, current, aborter, delayer, storeUrl }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; storeUrl: string; }) => { const { captchaToken } = context; const { username, password } = context.task.account; return handler({ context, current, aborter, delayer, endpoint: '/account/login', options: { method: 'POST', followRedirect: false, followAllRedirects: false, headers: { 'cache-control': 'max-age=0', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', origin: storeUrl, 'upgrade-insecure-requests': '1', dnt: '1', 'content-type': 'application/x-www-form-urlencoded', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', referer: `${storeUrl}/account/login?return_url=%2Faccount`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }, form: { form_type: 'customer_login', utf8: '✓', 'customer[email]': username, 'customer[password]': password, 'recaptcha-v3-token': captchaToken } }, includeHeaders: false, message: 'Logging in', from: States.SUBMIT_ACCOUNT }); }; ================================================ FILE: app/tasks/shopify/classes/functions/cart.ts ================================================ import { Task as TaskConstants } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = TaskConstants; export const getCart = ({ handler, context, current, aborter, delayer, endpoint = '/cart', message = 'Visiting cart', options = {} }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; endpoint?: string; message?: string; options?: any; }) => { return handler({ context, current, aborter, delayer, endpoint, options: { followRedirect: false, followAllRedirects: false, ...options }, message, from: States.GET_CART }); }; export const submitCart = ({ handler, context, current, aborter, delayer, storeUrl, productUrl, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; storeUrl: string; productUrl: string; form: any; }) => { let json = true; let endpoint = `/cart/add.js`; if (/mattel/i.test(storeUrl)) { json = false; endpoint = `/cart/add`; } return handler({ context, current, aborter, delayer, endpoint, options: { method: 'POST', json, followRedirect: false, followAllRedirects: false, headers: { 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', accept: 'application/json, text/javascript, */*; q=0.01', dnt: '1', 'sec-ch-ua-mobile': '?0', 'user-agent': userAgent, 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', origin: storeUrl, 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', referer: productUrl, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }, form }, includeHeaders: false, message: 'Adding to cart', from: States.SUBMIT_CART }); }; export const clearCart = ({ handler, context, current, aborter, delayer }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; }) => { return handler({ context, current, aborter, delayer, endpoint: '/cart/clear.js', options: { method: 'POST', followRedirect: false, followAllRedirects: false, form: JSON.stringify({}) }, message: 'Clearing cart', from: States.CLEAR_CART }); }; ================================================ FILE: app/tasks/shopify/classes/functions/challenge.ts ================================================ import { Task as TaskConstants } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = TaskConstants; export const getChallenge = ({ handler, context, current, aborter, delayer, url }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; url: string; }) => { return handler({ context, current, aborter, delayer, endpoint: '/challenge', options: { followRedirect: false, followAllRedirects: false, headers: { 'content-type': 'application/x-www-form-urlencoded', referer: `${url}/account/login` } }, message: 'Visiting challenge', from: States.GET_CHALLENGE }); }; export const submitChallenge = ({ handler, context, current, aborter, delayer, authToken, captchaToken, url }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; authToken: string; captchaToken: string; url: string; }) => { return handler({ context, current, aborter, delayer, endpoint: '/account/login', options: { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', referer: `${url}/challenge` }, form: { authenticity_token: authToken, 'g-recaptcha-response': captchaToken } }, message: 'Submitting challenge', from: States.SUBMIT_CHALLENGE }); }; ================================================ FILE: app/tasks/shopify/classes/functions/checkout.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = Task; export const initialize = ({ handler, context, current, aborter, delayer, storeUrl, productUrl, follow = false, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; storeUrl: string; productUrl: string; follow?: boolean; form: string; }) => { let params = form; if (!params) { params = 'updates%5B%5D=1&attributes%5Bcheckout_clicked%5D=true&checkout='; } return handler({ context, current, aborter, delayer, endpoint: '/cart', options: { method: 'POST', followRedirect: follow, followAllRedirects: follow, headers: { 'cache-control': 'max-age=0', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', origin: storeUrl, 'upgrade-insecure-requests': '1', dnt: '1', 'content-type': 'application/x-www-form-urlencoded', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', referer: productUrl, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }, form: params }, message: 'Initializing checkout', from: States.INIT_CHECKOUT }); }; export const getTotalPrice = ({ handler, context, current, aborter, delayer, accessToken, hash }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; accessToken: string; hash: string; }) => { return handler({ context, current, aborter, delayer, endpoint: `/api/checkouts/${hash}.json`, options: { method: 'GET', followRedirect: false, followAllRedirects: false, json: true, headers: { 'X-Shopify-Storefront-Access-Token': accessToken, 'X-Shopify-Checkout-Version': '2016-09-06', 'accept-encoding': 'gzip, deflate', accept: 'application/json', connection: 'close' } }, includeHeaders: false, message: 'Calculating taxes', from: States.GET_PRICE }); }; ================================================ FILE: app/tasks/shopify/classes/functions/checkpoint.ts ================================================ /* eslint-disable camelcase */ import { Task as TaskConstants } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = TaskConstants; export const submitCheckpoint = ({ handler, context, current, aborter, delayer, url, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; url: string; form: any; }) => { return handler({ context, current, aborter, delayer, endpoint: '/checkpoint', options: { method: 'POST', json: false, followRedirect: false, followAllRedirects: false, headers: { 'content-type': 'application/x-www-form-urlencoded', Dnt: '1', referer: `${url}/checkpoint?return_to=${encodeURIComponent( `${url}/cart` )}`, 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1' }, form }, timeout: 60000, message: 'Submitting checkpoint', from: States.SUBMIT_CHECKPOINT }); }; ================================================ FILE: app/tasks/shopify/classes/functions/config.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = Task; export const getConfig = ({ handler, context, current, aborter, delayer }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; }) => { return handler({ context, current, aborter, delayer, endpoint: '/payments/config', options: { json: true, followRedirect: false, followAllRedirects: false }, message: 'Visiting config', from: States.GET_CONFIG }); }; ================================================ FILE: app/tasks/shopify/classes/functions/customer.ts ================================================ import uuid from 'uuidv4'; import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = Task; export const getCustomer = ({ handler, context, current, aborter, delayer, hash, shopId, url }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; url: string; }) => { return handler({ context, current, aborter, delayer, endpoint: `/${shopId}/checkouts/${hash}`, options: { followRedirect: true, followAllRedirects: true, headers: { 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', dnt: '1', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', 'content-type': 'application/x-www-form-urlencoded', referer: `${url}/${shopId}/checkouts/${hash}`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' } }, message: 'Visiting checkout', from: States.GET_CUSTOMER }); }; export const submitCustomer = ({ handler, context, current, aborter, delayer, hash, shopId, url, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; url: string; form: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `/${shopId}/checkouts/${hash}`, options: { method: 'POST', json: false, headers: { 'cache-control': 'max-age=0', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', origin: url, 'upgrade-insecure-requests': '1', dnt: '1', 'content-type': 'application/x-www-form-urlencoded', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', referer: `${url}/${shopId}/checkouts/${hash}?step=contact_information`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }, form }, message: 'Submitting information', from: States.SUBMIT_CUSTOMER }); }; export const submitCustomerApi = ({ handler, context, current, aborter, delayer, hash, uniqueToken = uuid(), visitToken = uuid(), accessToken, json }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; uniqueToken?: string; visitToken?: string; accessToken: string; json: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `/api/checkouts/${hash}.json`, options: { method: 'PATCH', followRedirect: true, followAllRedirects: true, headers: { authorization: `Basic ${Buffer.from(`${accessToken}::`).toString( 'base64' )}`, 'content-type': 'application/json', 'X-Shopify-Checkout-Version': '2021-01-04', 'X-Shopify-UniqueToken': uniqueToken, 'X-Shopify-VisitToken': visitToken }, json }, message: 'Preloading checkout', from: States.PATCH_CHECKOUT }); }; ================================================ FILE: app/tasks/shopify/classes/functions/discount.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = Task; export const submitDiscount = ({ handler, context, current, aborter, delayer, hash, shopId, url, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; url: string; form: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `/${shopId}/checkouts/${hash}`, options: { method: 'POST', followAllRedirects: false, followRedirect: false, headers: { 'content-type': 'application/x-www-form-urlencoded', referer: `${url}/${shopId}/checkouts/${hash}`, 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin', 'sec-fetch-user': '?1' }, form }, message: 'Submitting discount', from: States.SUBMIT_DISCOUNT }); }; ================================================ FILE: app/tasks/shopify/classes/functions/homepage.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = Task; export const getHomepage = ({ handler, context, current, aborter, delayer }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; }) => { return handler({ context, current, aborter, delayer, endpoint: '/', options: { json: false, followRedirect: false, followAllRedirects: false }, message: 'Visiting homepage', from: States.GET_HOMEPAGE }); }; ================================================ FILE: app/tasks/shopify/classes/functions/index.ts ================================================ import { getHomepage } from './homepage'; import { getConfig } from './config'; import { getPassword, submitPassword } from './password'; import { getProduct, getProducts, getPreloadProduct, getDetails, getProductUrl } from './product'; import { getAccount, submitAccount } from './account'; import { getChallenge, submitChallenge } from './challenge'; import { submitCheckpoint } from './checkpoint'; import { getCart, clearCart, submitCart } from './cart'; import { initialize, getTotalPrice } from './checkout'; import { enterQueue, waitInQueue, waitInNextQueue, passedQueue } from './queue'; import { getCustomer, submitCustomer, submitCustomerApi } from './customer'; import { getShipping, getShippingApi, getCartRates, submitShipping, submitShippingApi } from './shipping'; import { getSession } from './session'; import { submitDiscount } from './discount'; import { getPayment, submitPayment } from './payment'; import { createGuest, approveGuest, getCallbackUrl } from './paypal'; import { getReview, submitReview } from './review'; import { getOrder } from './order'; export { getHomepage, getConfig, getPassword, submitPassword, getProduct, getProducts, getPreloadProduct, getDetails, getProductUrl, getAccount, submitAccount, getChallenge, submitChallenge, submitCheckpoint, getCart, clearCart, submitCart, initialize, getTotalPrice, enterQueue, waitInQueue, waitInNextQueue, passedQueue, getCustomer, submitCustomer, submitCustomerApi, getShipping, getShippingApi, getCartRates, submitShipping, submitShippingApi, getSession, submitDiscount, getPayment, submitPayment, createGuest, approveGuest, getCallbackUrl, getReview, submitReview, getOrder }; ================================================ FILE: app/tasks/shopify/classes/functions/order.ts ================================================ import { Task as TaskConstants } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = TaskConstants; export const getOrder = ({ handler, context, current, aborter, delayer, hash, shopId, url, polling, message }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; url: string; polling: boolean; message: string; }) => { const endpoint = polling ? `/${shopId}/checkouts/${hash}/processing?from_processing_page=1` : `/${shopId}/checkouts/${hash}/processing`; return handler({ context, current, aborter, delayer, endpoint, options: { json: false, followRedirect: true, followAllRedirects: true, headers: { 'content-type': 'application/x-www-form-urlencoded', referer: `${url}/${shopId}/checkouts/${hash}` } }, message, from: States.GET_ORDER }); }; ================================================ FILE: app/tasks/shopify/classes/functions/password.ts ================================================ import { Task as TaskConstants } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = TaskConstants; export const getPassword = ({ handler, context, current, aborter, delayer }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; }) => { return handler({ context, current, aborter, delayer, endpoint: '/password', options: { method: 'GET', followRedirect: false, followAllRedirects: false }, message: 'Visiting password', from: States.GET_PASSWORD }); }; export const submitPassword = ({ handler, context, current, aborter, delayer, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; form: string; }) => { let params = form; if (!params) { const { password } = context.task; params = `form_type=storefront_password&utf8=✓&password=${password}`; } return handler({ context, current, aborter, delayer, endpoint: '/password', options: { method: 'POST', followRedirect: false, followAllRedirects: false, headers: { 'content-type': 'application/x-www-form-urlencoded' }, form: params }, message: 'Submitting password', from: States.SUBMIT_PASSWORD }); }; ================================================ FILE: app/tasks/shopify/classes/functions/payment.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = Task; export const getPayment = ({ handler, context, current, aborter, delayer, hash, shopId, url, polling = false }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; url: string; polling?: boolean; }) => { let endpoint = `/${shopId}/checkouts/${hash}?previous_step=shipping_method&step=payment_method`; let headers: any = { 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', dnt: '1', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', referer: `${url}/${shopId}/checkouts/${hash}?previous_step=contact_information&step=shipping_method`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }; if (polling) { endpoint = `/${shopId}/checkouts/${hash}?step=payment_method`; headers = { 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', accept: '*/*', dnt: '1', 'x-requested-with': 'XMLHttpRequest', 'sec-ch-ua-mobile': '?0', 'user-agent': userAgent, 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', referer: `${url}/${shopId}/checkouts/${hash}?previous_step=shipping_method&step=payment_method`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }; } return handler({ context, current, aborter, delayer, endpoint, options: { json: false, followRedirect: false, followAllRedirects: false, headers }, message: polling ? 'Calculating taxes' : 'Visiting payment', from: States.GET_PAYMENT }); }; export const submitPayment = ({ handler, context, current, aborter, delayer, follow = false, hash, shopId, url, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; follow?: boolean; hash: string; shopId: string; url: string; form: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `/${shopId}/checkouts/${hash}`, options: { method: 'POST', followAllRedirects: follow, followRedirect: follow, headers: { 'cache-control': 'max-age=0', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', origin: url, 'upgrade-insecure-requests': '1', dnt: '1', 'content-type': 'application/x-www-form-urlencoded', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', referer: `${url}/${shopId}/checkouts/${hash}?previous_step=shipping_method&step=payment_method`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }, form }, message: 'Submitting order', from: States.SUBMIT_PAYMENT }); }; ================================================ FILE: app/tasks/shopify/classes/functions/paypal.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = Task; export const createGuest = ({ handler, context, current, aborter, delayer, expressCheckoutToken, json }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; expressCheckoutToken: string; json: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `https://www.paypal.com/graphql?OnboardGuestMutation`, options: { method: 'POST', followRedirect: false, followAllRedirects: false, headers: { accept: '*/*', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/json', dnt: 1, 'paypal-client-metadata-id': expressCheckoutToken, 'paypal-client-context': expressCheckoutToken, referer: `https://www.paypal.com/checkoutweb/signup?version=4.0.173&locale.x=en_US&fundingSource=paypal&sessionID=&buttonSessionID=&env=production&logLevel=warn&uid=044671dc1d&token=${expressCheckoutToken}&xcomponent=1&country.x=US&locale.x=en_US&country.x=US`, 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'user-agent': userAgent, 'x-app-name': 'checkoutuinodeweb_onboarding_lite', 'x-country': 'US', 'x-locale': 'en_US' }, json }, includeHeaders: false, message: 'Creating guest', from: States.CREATE_GUEST }); }; export const approveGuest = ({ handler, context, current, aborter, delayer, expressCheckoutToken, accessToken, json }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; expressCheckoutToken: string; accessToken: string; json: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `https://www.paypal.com/graphql?ApproveOnboardPaymentMutation`, options: { method: 'POST', followRedirect: false, followAllRedirects: false, headers: { accept: '*/*', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/json', dnt: 1, 'paypal-client-metadata-id': expressCheckoutToken, 'paypal-client-context': expressCheckoutToken, referer: `https://www.paypal.com/checkoutweb/signup?version=4.0.173&locale.x=en_US&fundingSource=paypal&sessionID=&buttonSessionID=&env=production&logLevel=warn&uid=044671dc1d&token=${expressCheckoutToken}&xcomponent=1&country.x=US&locale.x=en_US&country.x=US`, 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'user-agent': userAgent, 'x-app-name': 'checkoutuinodeweb_onboarding_lite', 'x-country': 'US', 'x-locale': 'en_US', 'x-paypal-internal-euat': accessToken }, json }, includeHeaders: false, message: 'Approving guest', from: States.APPROVE_GUEST }); }; export const getCallbackUrl = ({ handler, context, current, aborter, delayer, storeUrl, returnUrl }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; storeUrl: string; returnUrl: string; }) => { return handler({ context, current, aborter, delayer, endpoint: returnUrl, options: { method: 'GET', followRedirect: false, followAllRedirects: false, headers: { accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', dnt: '1', pragma: 'no-cache', referer: storeUrl, 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin', 'upgrade-insecure-requests': '1', 'user-agent': userAgent } }, includeHeaders: false, message: 'Approving guest', from: States.APPROVE_GUEST }); }; ================================================ FILE: app/tasks/shopify/classes/functions/product.ts ================================================ import { Task as TaskConstants, Monitor as MonitorConstants } from '../../constants'; const { States: TaskStates } = TaskConstants; const { States: MonitorStates } = MonitorConstants; const getRandomNumber = (digit: number) => Math.random().toFixed(digit).split('.')[1]; export const getPreloadProduct = ({ handler }: { handler: Function }) => { return handler({ endpoint: `/products.json?limit=250&order=${getRandomNumber(7)}`, options: { json: true }, timeout: 12500, from: TaskStates.GET_PRODUCT, message: 'Preloading product' }); }; export const getProducts = ({ handler, message }: { handler: Function; message: string; }) => { return handler({ endpoint: `/products.json?limit=100&order=${getRandomNumber(7)}`, options: { json: true }, timeout: 12500, from: MonitorStates.GET_PRODUCTS, message }); }; export const getProduct = ({ handler, message, url }: { handler: Function; message: string; url: string; }) => { const [endpoint] = url.split('?'); return handler({ endpoint: `${endpoint}?order=${getRandomNumber(7)}`, from: MonitorStates.GET_PRODUCT, timeout: 12500, message }); }; export const getDetails = ({ handler, url, message }: { handler: Function; url: string; message: string; }) => { const [base] = url.split('?'); const endpoint = `${base}?format=js`; return handler({ endpoint, from: MonitorStates.GET_PRODUCT, options: { json: true }, timeout: 12500, message }); }; export const getProductUrl = ({ handler, url }: { handler: Function; url: string; }) => { const [endpoint] = url.split('?'); return handler({ endpoint, from: MonitorStates.GET_PRODUCT, timeout: 12500, message: 'Parsing product' }); }; ================================================ FILE: app/tasks/shopify/classes/functions/queue.ts ================================================ import { Task as TaskConstants } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = TaskConstants; export const enterQueue = async ({ handler, context, current, aborter, delayer, ctd, url, from }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; ctd: string; url: string; from: string; }) => { return handler({ context, current, aborter, delayer, endpoint: ctd ? `/throttle/queue?_ctd=${encodeURIComponent(ctd)}&_ctd_update=` : '/throttle/queue', options: { followRedirect: false, followAllRedirects: false, headers: { accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', referer: url } }, message: 'Entering queue', from }); }; export const waitInQueue = async ({ handler, context, current, aborter, delayer, url, ctd }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; url: string; ctd: string; }) => { return handler({ context, current, aborter, delayer, endpoint: '/checkout/poll?js_poll=1', options: { followRedirect: false, followAllRedirects: false, headers: { accept: '*/*', 'accept-language': 'en-US,en;q=0.9,fr;q=0.8,de;q=0.7', referer: `${url}/throttle/queue?_ctd=${encodeURIComponent( ctd )}&_ctd_update` } }, message: 'Waiting in queue', from: States.GET_QUEUE }); }; export const passedQueue = async ({ handler, context, current, aborter, delayer, ctd }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; ctd: string; }) => { return handler({ context, current, aborter, delayer, endpoint: `/throttle/queue?_ctd=${encodeURIComponent(ctd)}&_ctd_update`, options: { json: false, followRedirect: false, followAllRedirects: false }, from: States.GET_QUEUE }); }; export const waitInNextQueue = async ({ handler, context, current, aborter, delayer, storeUrl, eta, available, token }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; storeUrl: string; eta: string; available: string; token: string; }) => { let message = 'Waiting in queue'; if (eta) { message = `Waiting in queue [${eta}s]`; } if (available !== '') { message = `Waiting in queue [${available}]`; } return handler({ context, current, aborter, delayer, endpoint: `/queue/poll`, options: { method: 'POST', followRedirect: false, followAllRedirects: false, headers: { 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', dnt: '1', 'sec-ch-ua-mobile': '?0', 'user-agent': userAgent, 'content-type': 'application/json', accept: '*/*', origin: storeUrl, 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', referer: `${storeUrl}/throttle/queue`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }, json: { query: '\n {\n poll(token: $token) {\n token\n pollAfter\n queueEtaSeconds\n productVariantAvailability {\n id\n available\n }\n }\n }\n ', variables: { token } } }, message, from: States.GET_NEXT_QUEUE }); }; ================================================ FILE: app/tasks/shopify/classes/functions/review.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; const { States } = Task; export const getReview = ({ handler, context, current, aborter, delayer, hash, shopId }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; }) => { return handler({ context, current, aborter, delayer, endpoint: `/${shopId}/checkouts/${hash}?step=review`, options: { method: 'GET', followAllRedirects: false, followRedirect: false }, message: 'Calculating taxes', from: States.GET_REVIEW }); }; export const submitReview = ({ handler, context, current, aborter, delayer, hash, shopId, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; form: any; }) => { const { task: { store: { url } } } = context; return handler({ context, current, aborter, delayer, endpoint: `/${shopId}/checkouts/${hash}`, options: { method: 'POST', followAllRedirects: false, followRedirect: false, headers: { 'content-type': 'application/x-www-form-urlencoded', referer: `${url}/${shopId}/checkouts/${hash}`, 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'same-origin', 'sec-fetch-user': '?1' }, form }, message: 'Completing order', from: States.SUBMIT_REVIEW }); }; ================================================ FILE: app/tasks/shopify/classes/functions/session.ts ================================================ import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = Task; export const getSession = ({ handler, context, current, aborter, delayer, timeout = 15000, message = 'Creating session', json }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; message?: string; timeout?: number; json: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `https://deposit.us.shopifycs.com/sessions`, options: { method: 'POST', headers: { accept: 'application/json', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9', 'content-type': 'application/json', dnt: 1, origin: 'https://checkout.shopifycs.com', pragma: 'no-cache', referer: `https://checkout.shopifycs.com/`, 'user-agent': userAgent }, json }, timeout, includeHeaders: false, message, from: States.GET_SESSION }); }; ================================================ FILE: app/tasks/shopify/classes/functions/shipping.ts ================================================ import uuid from 'uuidv4'; import { Task } from '../../constants'; import { ShopifyContext } from '../../../common/contexts'; import { userAgent } from '../../../common/utils'; const { States } = Task; export const getShipping = ({ handler, context, current, aborter, delayer, hash, shopId, url, polling = false }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; shopId: string; url: string; polling: boolean; }) => { let endpoint = `/${shopId}/checkouts/${hash}?previous_step=contact_information&step=shipping_method`; if (polling) { endpoint = `/${shopId}/checkouts/${hash}/shipping_rates?step=shipping_method`; } return handler({ context, current, aborter, delayer, endpoint, options: { json: false, followRedirect: false, followAllRedirects: false, headers: { 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', dnt: '1', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', referer: `${url}/${shopId}/checkouts/${hash}?step=contact_information`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' } }, message: polling ? 'Polling rates' : 'Visiting rates', from: States.GET_SHIPPING }); }; export const getShippingApi = ({ handler, context, current, aborter, delayer, hash, accessToken, uniqueToken = uuid(), visitToken = uuid(), polling }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; accessToken: string; uniqueToken?: string; visitToken?: string; polling: boolean; }) => { return handler({ context, current, aborter, delayer, endpoint: `/api/checkouts/${hash}/shipping_rates.json`, options: { json: true, headers: { authorization: `Basic ${Buffer.from(`${accessToken}::`).toString( 'base64' )}`, 'content-type': 'application/json', 'X-Shopify-Checkout-Version': '2021-01-04', 'X-Shopify-UniqueToken': uniqueToken, 'X-Shopify-VisitToken': visitToken } }, message: polling ? 'Polling rates' : 'Visiting rates', from: States.GET_SHIPPING }); }; export const getCartRates = ({ handler, context, current, aborter, delayer, zip, province, country }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; zip: string; province: string; country: string; }) => { return handler({ context, current, aborter, delayer, endpoint: `/cart/shipping_rates.json?shipping_address[zip]=${zip}&shipping_address[country]=${country}&shipping_address[province]=${province}`, from: States.GET_SHIPPING, message: 'Visiting rates' }); }; export const submitShipping = ({ handler, context, current, aborter, delayer, follow = false, hash, shopId, url, form }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; follow?: boolean; shopId: string; url: string; form: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `/${shopId}/checkouts/${hash}`, options: { method: 'POST', json: false, followRedirect: follow, followAllRedirects: follow, headers: { 'cache-control': 'max-age=0', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', origin: url, 'upgrade-insecure-requests': '1', dnt: '1', 'content-type': 'application/x-www-form-urlencoded', 'user-agent': userAgent, accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'navigate', 'sec-fetch-user': '?1', 'sec-fetch-dest': 'document', referer: `${url}/${shopId}/checkouts/${hash}?previous_step=contact_information&step=shipping_method`, 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9' }, form }, message: 'Submitting rates', from: States.SUBMIT_SHIPPING }); }; export const submitShippingApi = ({ handler, context, current, aborter, delayer, hash, uniqueToken = uuid(), visitToken = uuid(), accessToken, json }: { handler: Function; context: ShopifyContext; current: any; aborter: AbortController; delayer: any; hash: string; uniqueToken?: string; visitToken?: string; accessToken: string; json: any; }) => { return handler({ context, current, aborter, delayer, endpoint: `/api/checkouts/${hash}.json`, options: { method: 'PATCH', followRedirect: false, followAllRedirects: false, headers: { authorization: `Basic ${Buffer.from(`${accessToken}::`).toString( 'base64' )}`, 'content-type': 'application/json', 'X-Shopify-Checkout-Version': '2021-01-04', 'X-Shopify-UniqueToken': uniqueToken, 'X-Shopify-VisitToken': visitToken }, json }, message: 'Submitting rates', from: States.SUBMIT_SHIPPING }); }; ================================================ FILE: app/tasks/shopify/classes/monitor.ts ================================================ import { isEmpty } from 'lodash'; import { ShopifyAnswers } from '../../common/contexts/shopify'; import { ShopifyContext } from '../../common/contexts'; import { emitEvent, request, isTimeout, isNetworkError, waitForDelay, isImproperStatusCode, ellipsis } from '../../common/utils'; import { Platforms, Monitor } from '../../common/constants'; import { BaseMonitor } from '../../common/classes'; import { Parse, getHeaders } from '../utils'; import { Monitor as MonitorConstants } from '../constants'; import { getProperties } from '../utils/parse'; import { getProducts, getProduct, getDetails } from './functions'; import { ProductMeta, Product } from '../types/product'; const { match } = Parse; const { ParseType } = Monitor; const { States } = MonitorConstants; export class ShopifyMonitor extends BaseMonitor { context: ShopifyContext; products: any; fallback: boolean; product: any; constructor(context: ShopifyContext, platform = Platforms.Shopify) { super(context, States.MONITOR, platform); this.context = context; this.fallback = false; this.products = {}; this.product = {}; } // this only needs to live on the shopify side, so attach it here answer = ({ answers }: { answers: ShopifyAnswers[] }) => { this.context.answers = answers; }; patchContext() { this.context.product = this.product; } handler = async ({ endpoint, options = {}, message = '', from = this.prevState, timeout = 10000, includeHeaders = true }: { endpoint: string; options?: any; message?: string; from?: string; timeout?: number; includeHeaders?: boolean; }) => { const { id, aborted, logger, monitorSession, monitorProxy, task: { store: { url } } } = this.context; if (aborted) { return { nextState: States.ABORT }; } if (message) { emitEvent(this.context, [id], { message }); } const baseOptions = { proxy: monitorProxy ? monitorProxy.proxy : undefined, followAllRedirects: true, followRedirect: true, timeout }; const requestHeaders = includeHeaders ? { ...getHeaders({ url }), ...options.headers } : { ...options.headers }; const toRequest = // eslint-disable-next-line no-nested-ternary endpoint.indexOf('http') > -1 ? endpoint : endpoint.startsWith('/') ? `${url}${endpoint}` : `${url}/${endpoint}`; const opts = { ...baseOptions, ...options, url: toRequest, headers: requestHeaders }; try { const res = await request(monitorSession, opts); let redirect; if (opts.followRedirect) { redirect = res?.request?.uri?.href; } else { redirect = res?.headers?.location; } logger.log({ id, level: 'debug', message: `${from} REDIRECT: ${redirect}` }); if (!redirect) { return { data: res }; } return { data: res, redirect }; } catch (error) { logger.log({ id, level: 'error', message: `${from} error: ${error?.message || 'Unknown'}` }); if (isTimeout(error)) { emitEvent(this.context, [id], { message: `Error ${message.toLowerCase()} [TIMEOUT]` }); return { data: {}, nextState: from }; } if (isNetworkError(error)) { emitEvent(this.context, [id], { message: `Error ${message.toLowerCase()} [NETWORK]` }); return { data: {}, nextState: from }; } emitEvent(this.context, [id], { message: `Error ${message.toLowerCase()} [UNKNOWN]` }); return { data: {}, nextState: from }; } }; async getProducts() { const { id, aborted, logger, task: { retry, monitor } } = this.context; if (aborted) { return States.ABORT; } const message = `Monitoring${ellipsis[this.tries]}`; this.tries += 1; if (this.tries > 2) { this.tries = 0; } const { nextState, data } = await getProducts({ handler: this.handler, message }); if (nextState) { this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, headers, body } = data; if (isImproperStatusCode(statusCode, /429|430|403/)) { emitEvent(this.context, [id], { message: `Error monitoring [${statusCode}]` }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } if (isImproperStatusCode(statusCode, /401/)) { emitEvent(this.context, [id], { message: 'Password page' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } logger.log({ id, level: 'silly', message: `Was cached? ${headers['x-cache'] !== 'miss'}` }); if (!body?.products) { emitEvent(this.context, [id], { message: 'Error monitoring [UNKNOWN]' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } const { products } = body; this.products = products; return States.MATCH_PRODUCT; } async match() { const { aborted, task: { monitor, store: { url } } } = this.context; if (aborted) { return States.ABORT; } const matched: Product = await match(this.context, this.products); if (!matched) { this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } this.product.url = `${url}/products/${matched.handle}`; this.product.name = matched.title; this.product.variants = matched.variants; return States.GET_PRODUCT; } async getProductDetails() { const { id, aborted, logger, task: { retry, monitor, store: { url } } } = this.context; if (aborted) { return States.ABORT; } const message = `Monitoring${ellipsis[this.tries]}`; this.tries += 1; if (this.tries > 2) { this.tries = 0; } const { nextState, data } = await getDetails({ handler: this.handler, message, url }); if (nextState) { this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, headers, body } = data; if (isImproperStatusCode(statusCode, /429|430|403/)) { emitEvent(this.context, [id], { message: `Error monitoring [${statusCode}]` }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } if (isImproperStatusCode(statusCode, /401/)) { emitEvent(this.context, [id], { message: 'Password page' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } logger.log({ id, level: 'silly', message: `Was cached? ${headers['x-cache'] !== 'miss'}` }); const { title, handle, variants } = body; this.product.name = title; this.product.url = `${url}/products/${handle}`; this.product.variants = variants; this.patchContext(); return States.DONE; } async getProduct() { const { id, aborted, logger, task: { retry, monitor, store: { url: storeUrl } } } = this.context; if (aborted) { return States.ABORT; } if (this.fallback) { return this.getProductDetails(); } // patch this so we don't circle back to parsing out the url of the product again if (this.context.parseType !== ParseType.Url) { this.context.parseType = ParseType.Url; } const message = `Monitoring${ellipsis[this.tries]}`; this.tries += 1; if (this.tries > 2) { this.tries = 0; } const { url } = this.product; const { nextState, data } = await getProduct({ handler: this.handler, message, url }); if (nextState) { this.delayer = waitForDelay(retry, this.aborter.signal); await this.delayer; return nextState; } const { statusCode, headers, body } = data; if (isImproperStatusCode(statusCode, /429|430|403/)) { emitEvent(this.context, [id], { message: `Error monitoring [${statusCode}]` }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } if (isImproperStatusCode(statusCode, /404/)) { emitEvent(this.context, [id], { message: 'Product not live' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } if (isImproperStatusCode(statusCode, /401/)) { emitEvent(this.context, [id], { message: 'Password page' }); this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return States.MONITOR; } logger.log({ id, level: 'silly', message: `Was cached? ${headers['x-cache'] !== 'miss'}` }); if (!body) { this.delayer = waitForDelay(monitor, this.aborter.signal); await this.delayer; return nextState; } const match = /(? ({ id: p.id, price: p.price, name: p.name, title: p.public_title || 'N/A' })) }; this.patchContext(); return States.DONE; } async monitor() { const { aborted, parseType, task: { product: { variant } } } = this.context; if (aborted) { return States.ABORT; } switch (parseType) { case ParseType.Variant: { this.context.product.variants = [{ id: variant }]; return States.DONE; } case ParseType.Url: { // patch this to avoid runtime errors this.product.url = this.context.task.product.url; return this.getProduct(); } case ParseType.Keywords: { return this.getProducts(); } default: { return States.ERROR; } } } async handleStepLogic(currentState: string) { const { id, logger } = this.context; const stepMap = { [States.MONITOR]: this.monitor, [States.GET_PRODUCT]: this.getProduct, [States.MATCH_PRODUCT]: this.match, [States.SWAP]: this.swap, [States.ERROR]: () => States.ABORT, [States.DONE]: () => States.ABORT, [States.ABORT]: () => States.ABORT }; logger.log({ id, level: 'info', message: `Handling monitor state: ${currentState}` }); const defaultHandler = () => { const handler: any = stepMap[this.prevState]; if (!handler) { throw new Error('Reached unknown state!'); } return handler.call(this); }; const handler: any = stepMap[currentState] || defaultHandler; return handler.call(this); } } ================================================ FILE: app/tasks/shopify/classes/rates.ts ================================================ import { pick } from 'lodash'; import { load } from 'cheerio'; import { ShopifyContext } from '../../common/contexts'; import { request, isImproperStatusCode } from '../../common/utils'; import { Monitor } from '../../common/constants'; import { ShopifyTask } from './tasks/base'; import { getProducts, getDetails, getProductUrl } from './functions'; import { IPCKeys } from '../../../constants/ipc'; import { Parse, getHeaders } from '../utils'; const { match } = Parse; const { ParseType } = Monitor; const States = { ABORT: 'ABORT', ERROR: 'ERROR', MONITOR: 'MONITOR', WAIT_FOR_PRODUCT: 'WAIT_FOR_PRODUCT', SUBMIT_CART: 'SUBMIT_CART', GET_SHIPPING: 'GET_SHIPPING', DONE: 'DONE' }; export class RateFetcher extends ShopifyTask { context: ShopifyContext; shippingRates: any[]; _error: string; _mainWindow: any; constructor(context: ShopifyContext, mainWindow: any) { super(context, States.MONITOR); this.context = context; this.shippingRates = []; this._error = ''; this._mainWindow = mainWindow; } retrieveProfile() { const { profileManager, task: { profile: { id } } } = this.context; return profileManager.retrieve(id); } handler = async ({ endpoint, options = {}, from = this.prevState, timeout = 10000, includeHeaders = true }: { endpoint: string; options?: any; from?: string; timeout?: number; includeHeaders?: boolean; }) => { const { id, aborted, logger, taskSession, proxy, task: { store: { url } } } = this.context; if (aborted) { return { nextState: States.ABORT }; } const baseOptions = { proxy: proxy ? proxy.proxy : undefined, followAllRedirects: true, followRedirect: true, timeout }; const requestHeaders = includeHeaders ? { ...getHeaders({ url }), ...options.headers } : { ...options.headers }; const toRequest = // eslint-disable-next-line no-nested-ternary endpoint.indexOf('http') > -1 ? endpoint : endpoint.startsWith('/') ? `${url}${endpoint}` : `${url}/${endpoint}`; const opts = { ...baseOptions, ...options, url: toRequest, headers: requestHeaders }; try { const res = await request(taskSession, opts); let redirect; if (opts.followRedirect) { redirect = res?.request?.uri?.href; } else { redirect = res?.headers?.location; } logger.log({ id, level: 'debug', message: `${from} REDIRECT: ${redirect}` }); if (!redirect) { return { data: res }; } return { data: res, redirect }; } catch (error) { logger.log({ id, level: 'error', message: `${from} error: ${error?.message || 'Unknown'}` }); return { data: {}, nextState: from }; } }; async getProducts() { const { aborted, task: { store: { url } } } = this.context; if (aborted) { return States.ABORT; } const { nextState, data } = await getProducts({ handler: this.handler, message: '' }); if (nextState) { this._error = 'UNKNOWN'; return States.ERROR; } const { statusCode, body } = data; if ( isImproperStatusCode(statusCode, /429|430|403|401/) || !body || !body?.products ) { this._error = 'NETWORK'; return States.ERROR; } const { products } = body; const matched = await match(this.context, products); if (!matched) { this._error = 'PRODUCT'; return States.ERROR; } const { id, variants, handle } = matched; this.context.product.id = id; this.context.product.variants = variants.map((v: any) => pick( v, 'id', 'product_id', 'title', 'available', 'price', 'option1', 'option2', 'option3', 'option4' ) ); if (/eflash-us/i.test(url)) { this.context.task.product.url = `${url}/products/${handle}`; return this.getProductFrontend(); } return States.WAIT_FOR_PRODUCT; } async getProductFrontend() { const { aborted, task: { product: { url } } } = this.context; if (aborted) { return States.ABORT; } const { nextState, data } = await getProductUrl({ handler: this.handler, url }); if (nextState) { this._error = 'UNKNOWN'; return States.ERROR; } const { statusCode, body } = data; if (isImproperStatusCode(statusCode, /429|430|403|401/) || !body) { this._error = 'NETWORK'; return States.ERROR; } const $ = load(body, { normalizeWhitespace: true, xmlMode: false }); const template = $('script#ProductJson-product-template') || ''; if (!template || template.attr('type') !== 'application/json') { this._error = 'UNKNOWN'; return States.ERROR; } const html = template.html(); if (!html) { this._error = 'UNKNOWN'; return States.ERROR; } const product = JSON.parse(html); // for DSM US we need to parse the `properties[_HASH]` field if (/eflash-us/i.test(url)) { const regex = /\$\(\s*atob\(\s*'PGlucHV0IHR5cGU9ImhpZGRlbiIgbmFtZT0icHJvcGVydGllc1tfSEFTSF0iIC8\+'\s*\)\s*\)\s*\.val\(\s*'(.+)'\s*\)/; const elements = regex.exec(body); if (elements) { const [, hash] = elements; this.context.product.properties = [ { name: 'properties[_HASH]', value: hash, question: false } ]; } } // for DSM UK the hash hasn't changed, so let's just hardcode it if (/eflash(?!.)/i.test(url)) { this.context.product.properties = [ { name: 'properties[_hash]', value: 'ee3e8f7a9322eaa382e04f8539a7474c11555', question: false } ]; } const { variants } = product; this.context.product.variants = variants.map((v: any) => pick( v, 'id', 'product_id', 'title', 'available', 'price', 'option1', 'option2', 'option3', 'option4' ) ); return States.WAIT_FOR_PRODUCT; } async getProduct() { const { aborted, task: { product: { url } } } = this.context; if (aborted) { return States.ABORT; } if (/eflash(?!-sg|-jp)/i.test(url)) { return this.getProductFrontend(); } const { nextState, data } = await getDetails({ handler: this.handler, url, message: '' }); if (nextState) { this._error = 'UNKNOWN'; return States.ERROR; } const { statusCode, body } = data; if (isImproperStatusCode(statusCode, /429|430|403|401/) || !body) { this._error = 'NETWORK'; return States.ERROR; } const { variants } = body; this.context.product.variants = variants.map((v: any) => pick( v, 'id', 'product_id', 'title', 'available', 'price', 'option1', 'option2', 'option3', 'option4' ) ); return States.WAIT_FOR_PRODUCT; } async monitor() { const { aborted, parseType, task: { product: { variant } } } = this.context; if (aborted) { return States.ABORT; } switch (parseType) { case ParseType.Variant: { this.context.product.variants = [{ id: variant }]; return States.WAIT_FOR_PRODUCT; } case ParseType.Url: { return this.getProduct(); } case ParseType.Keywords: { return this.getProducts(); } default: { this._error = 'PARSER'; return States.ERROR; } } } async submitCart() { const nextState = await super.submitCart(); if (nextState === States.DONE) { return States.GET_SHIPPING; } this._error = 'ERR: Cart'; return States.ERROR; } async getShipping() { const { aborted } = this.context; if (aborted) { return States.ABORT; } const profile = this.retrieveProfile(); if (!profile) { this._error = 'ERR: Profile'; return States.ERROR; } const { shipping: { country, province, zip } } = profile; const { nextState, data } = await this.handler({ endpoint: `/cart/shipping_rates.json?shipping_address[zip]=${zip.replace( /\s/g, '+' )}&shipping_address[country]=${country.value.replace( /\s/g, '+' )}&shipping_address[province]=${ province ? province.value.replace(/\s/g, '+') : '' }`, options: { json: true }, from: States.GET_SHIPPING, timeout: 15000, includeHeaders: true }); if (nextState) { this._error = 'UNKNOWN'; return nextState; } const { statusCode, body } = data; if (statusCode === 422) { this._error = 'NETWORK'; return States.ERROR; } if (!body) { this._error = 'NETWORK'; return States.ERROR; } const { shipping_rates: shippingRates } = body; if (!shippingRates || !shippingRates.length) { return States.GET_SHIPPING; } // eslint-disable-next-line no-restricted-syntax for (const { name, price, source, code } of shippingRates) { const newRate = { name, price, id: `${source}-${encodeURI(code)}-${price}` }; this.shippingRates.push(newRate); } return States.DONE; } async handleStepLogic(currentState: string) { const { id, logger } = this.context; const stepMap = { [States.MONITOR]: this.monitor, [States.WAIT_FOR_PRODUCT]: this.waitForProduct, [States.SUBMIT_CART]: this.submitCart, [States.GET_SHIPPING]: this.getShipping, [States.DONE]: () => States.DONE, [States.ERROR]: () => States.DONE, [States.ABORT]: () => States.DONE }; logger.log({ id, level: 'info', message: `Rate fetcher state: ${currentState}` }); const defaultHandler = () => { const handler: any = stepMap[this.prevState]; if (!handler) { throw new Error('Reached unknown state!'); } return handler.call(this); }; const handler: any = stepMap[currentState] || defaultHandler; return handler.call(this); } async run() { await super.run(); if (this.shippingRates.length) { this._mainWindow.send(IPCKeys.RatesTaskStatus, { success: true, store: this.context.task.store.url, rates: this.shippingRates }); return; } this._mainWindow.send(IPCKeys.RatesTaskStatus, { success: false, error: this._error }); } } ================================================ FILE: app/tasks/shopify/classes/tasks/base.ts ================================================ /* eslint-disable promise/catch-or-return */ /* eslint-disable promise/always-return */ /* eslint-disable no-restricted-syntax */ import { load } from 'cheerio'; import { isEmpty } from 'lodash'; import { Cookie } from 'electron'; import { Bases } from '../../../common'; import { Platforms, SiteKeyForPlatform } from '../../../common/constants'; import { waitForDelay, emitEvent, insertDecimal, request, isImproperStatusCode, isTimeout, isNetworkError, toTitleCase, ellipsis } from '../../../common/utils'; import { Task as TaskConstants } from '../../constants'; import { Forms, getHeaders, pickVariant, parseProtection, urlForStore } from '../../utils'; import { getHomepage, getConfig, submitPassword, submitCart, getCart, getAccount, submitAccount, getChallenge, submitChallenge, submitCheckpoint, enterQueue, waitInNextQueue, waitInQueue, passedQueue, getCustomer, submitCustomer, getShipping, submitShipping, getPayment, getTotalPrice, submitDiscount, getSession, submitPayment, createGuest, approveGuest, getCallbackUrl, getOrder, getReview } from '../functions'; import { ShopifyContext } from '../../../common/contexts'; import { AddToCartResponse, AddToCartMessage } from '../../types'; import { Property } from '../../utils/forms'; import CAPTCHA_TYPES from '../../../../utils/captchaTypes'; const { addToCart, submitCustomerForm, submitShippingForm, submitDiscountForm, submitPaymentForm, onboardGuest } = Forms; const { BaseTask } = Bases; const { States, Modes } = TaskConstants; type ShippingRate = { id: string; name: string; price: string; }; type CookieObject = { name: string; value: string; }; type Injected = { [url: string]: { [variant: string]: Property[]; }; }; export class ShopifyTask extends BaseTask { context: ShopifyContext; shippingRate: ShippingRate; checkpointInterval: any; loginCaptcha: boolean; challenge: boolean; checkpoint: boolean; question: boolean; preloading: boolean; reviewing: boolean; proceedTo: string | null; form: string; protection: any; resubmitShipping: boolean; shippingProtection: any; session: string; hash: string; key: string; ctd: string; gateway: string; authToken: string; calculated: boolean; merging: boolean; preload: any; useCompany: boolean; useTerms: boolean; useRemember: boolean; discountAuthToken: string; appliedDiscount: boolean; submittedPassword: boolean; count: number; formatter: string; currency: string; polling: boolean; product: any; // TODO: Define this model injected: Injected; checkpointUrl: string; skipShipping: boolean; solvedCheckpoint: boolean; expressCheckoutToken: string; paypalReturnUrl: string; payerId: string; useNewQueue: boolean; queueEta: string; queueAvailability: string; nextQueueToken: string; accessToken: string; restocking: boolean; rewinded: boolean; constructor( context: ShopifyContext, initState: string, platform = Platforms.Shopify ) { super(context, initState, platform); this.context = context; this.shippingRate = { id: '', name: '', price: '' }; const { rate } = this.context.task; if (rate?.id) { this.shippingRate = rate; } // checkout specific globals this.challenge = false; this.loginCaptcha = false; this.question = false; this.checkpoint = false; this.preloading = false; this.reviewing = false; this.calculated = false; this.merging = false; this.count = 0; this.solvedCheckpoint = false; this.resubmitShipping = false; this.checkpointUrl = ''; this.checkpointInterval = null; // internals this.form = ''; this.protection = []; this.shippingProtection = []; this.session = ''; this.hash = ''; this.key = ''; this.ctd = ''; this.gateway = ''; this.authToken = ''; this.discountAuthToken = ''; this.appliedDiscount = false; this.submittedPassword = false; this.formatter = 'en-US'; this.currency = 'USD'; this.skipShipping = false; this.expressCheckoutToken = ''; this.paypalReturnUrl = ''; this.payerId = ''; this.accessToken = ''; this.proceedTo = null; this.polling = false; this.restocking = false; this.rewinded = false; this.product = {}; this.injected = {}; this.useCompany = false; this.useTerms = false; this.useRemember = false; this.useNewQueue = false; this.queueEta = ''; this.queueAvailability = ''; this.nextQueueToken = ''; } async restart() { const { taskSession } = this.context; this.shippingRate = { id: '', name: '', price: '' }; this.challenge = false; this.question = false; this.checkpoint = false; this.preloading = false; this.reviewing = false; this.calculated = false; this.merging = false; this.resubmitShipping = false; this.checkpointUrl = ''; // internals this.form = ''; this.protection = []; this.shippingProtection = []; this.session = ''; this.hash = ''; this.key = ''; this.ctd = ''; this.gateway = ''; this.authToken = ''; this.discountAuthToken = ''; this.appliedDiscount = false; this.submittedPassword = false; this.formatter = 'en-US'; this.currency = 'USD'; this.skipShipping = false; this.expressCheckoutToken = ''; this.paypalReturnUrl = ''; this.payerId = ''; this.accessToken = ''; this.proceedTo = null; this.polling = false; this.restocking = false; this.rewinded = false; this.product = {}; this.injected = {}; this.useNewQueue = false; this.nextQueueToken = ''; await taskSession.clearStorageData(); return States.GET_HOMEPAGE; } inject = async (data: any) => { if (!isEmpty(data)) { Object.entries(data).map(([key, value]: [string, any]) => { if (key !== '_id') { (this as any)[key] = value; } if (key === 'properties') { const { task: { store: { url } } } = this.context; if (!this.injected[url]) { this.injected[url] = {}; } if (value[url]) { this.injected[url] = value[url]; } } return null; }); } }; harvest = async ({ token, form, body, cookies }: { token?: string; form?: any; body?: string; cookies?: Cookie[]; }) => { const { taskSession, task: { store: { url } } } = this.context; if (cookies?.length) { taskSession .clearStorageData({}) .then(async () => { try { for (const cookie of cookies) { try { // eslint-disable-next-line no-await-in-loop await taskSession.cookies.set({ url, ...cookie }); } catch (e) { console.error( '[TASK]: Failed to set cookie: ', cookie.name, cookie.value ); // noop.. } } } catch (e) { console.error('[TASK]: Failed to set cookies: ', e); } }) .catch(async () => { try { for (const cookie of cookies) { try { // eslint-disable-next-line no-await-in-loop await taskSession.cookies.set({ url, ...cookie }); } catch (e) { console.error( '[TASK]: Failed to set cookie: ', cookie.name, cookie.value ); // noop.. } } } catch (e) { console.error('[TASK]: Failed to set cookies: ', e); } }) .finally(() => { if (form) { this.captchaFinished = true; this.form = form; } if (body) { this.extractAuthToken( 'form input[name=authenticity_token]', load(body, { xmlMode: false, normalizeWhitespace: true }) ); } }); } this.context.captchaToken = token as string; }; async stuffSession() { const { taskSession, task: { mode, store: { url } } } = this.context; if (/kith/i.test(url)) { taskSession.cookies.set({ url, name: 'KL_FORMS_MODAL', value: `{%22disabledForms%22:{%22KyEV5m%22:{%22lastCloseTime%22:${Math.floor( Date.now() / 1000 )}%2C%22successActionTypes%22:[]}}%2C%22viewedForms%22:{%22KyEV5m%22:1428897}}` }); } if (mode === Modes.FAST || mode === Modes.RESTOCK) { return; } const profile = this.retrieveProfile(); if (!profile) { return; } const { label } = profile.shipping.country; const toInject: CookieObject[] = []; const cookies = await taskSession.cookies.get({}); try { const y = cookies.find((cookie: Cookie) => cookie.name === '_shopify_y'); if (y) { const { value } = y; toInject.push({ name: '_y', value }); } } catch (e) { // fail silently... } try { const s = cookies.find((cookie: Cookie) => cookie.name === '_shopify_s'); if (s) { const { value } = s; toInject.push({ name: '_s', value }); } } catch (e) { // fail silently... } toInject.push( { name: '_shopify_fs', value: `${encodeURIComponent(new Date(Date.now() - 15000).toJSON())}` }, { name: 'sig-shopify', value: 'true' }, { name: 'shopify_pay_redirect', value: 'pending' }, { name: 'hide_shopify_pay_for_checkout', value: `${this.hash}` }, { name: '_landing_page', value: '/' }, { name: 'acceptedCookies', value: 'yes' }, { name: 'tracked_start_checkout', value: `${this.hash}` }, { name: '_shopify_sa_p', value: '' }, { name: '_shopify_country', value: `${label}` }, { name: '_orig_referrer', value: `${encodeURIComponent(url)}` }, { name: '_shopify_sa_t', value: `${encodeURIComponent(new Date(Date.now() - 15000).toJSON())}` } ); return Promise.allSettled( toInject.map(({ name, value }: { name: string; value: string }) => taskSession.cookies.set({ url, name, value }) ) ); } async injectRequester({ redirect = 'https://checkout.shopify.com', type = CAPTCHA_TYPES.RECAPTCHA_V2 }) { const { id, captchaManager, task: { mode, store: { url, sitekey, sParam } }, proxy, taskSession } = this.context; this.context.setCaptchaToken(''); const cookies = await taskSession.cookies.get({ url }); captchaManager.insert({ id, type, sharing: type === CAPTCHA_TYPES.RECAPTCHA_V3, sitekey: sitekey || SiteKeyForPlatform[Platforms.Shopify], platform: Platforms.Shopify, harvest: this.harvest, host: redirect, proxy: mode !== Modes.FAST && mode !== Modes.RESTOCK && proxy ? proxy.ip : undefined, cookies, s: sParam }); } async sendWebhook(body: string, success: boolean) { const { id, proxy, task: { oneCheckout, store: { url, name }, monitor, retry, mode, quantity }, shopId, webhookManager, checkoutManager, notificationManager } = this.context; await this.extractAllData(body); let profileName; let cardType; const profile = this.retrieveProfile(); if (!profile) { profileName = 'Unknown'; cardType = 'Unknown'; } else { profileName = profile.name; cardType = profile.payment.type; } const { image, price, url: productUrl, name: productName } = this.product; let size = 'N/A'; if (this.product.variant?.title) { size = this.product.variant.title; } const finalPrice = `${price}`.indexOf('.') > -1 ? price : insertDecimal(`${price}`); if (!success) { let message = 'Checkout failed'; if (/Card was decline/i.test(body)) { message = 'Card declined'; } emitEvent(this.context, [id], { message }); if (!this.webhookSent) { this.webhookSent = true; webhookManager.log({ url: `${url}/${shopId}/checkouts/${this.hash}?key=${this.key}`, mode: mode || Modes.SAFE, proxy: proxy ? proxy.proxy : undefined, product: { name: productName ? toTitleCase(productName) : 'Unknown', price: Intl.NumberFormat(this.formatter, { style: 'currency', currency: this.currency }).format(Number(finalPrice)), image: `${image}`.startsWith('http') ? image : `https:${image}`, size: size || 'N/A', url: productUrl || url }, store: { name, url }, delays: { monitor, retry }, profile: { name: profileName, type: cardType }, quantity }); } return; } emitEvent(this.context, [id], { message: 'Check email!' }); notificationManager.insert({ id, message: `Task ${id}: Check email!`, variant: 'success', type: 'DONE' }); if (oneCheckout) { checkoutManager.check({ context: this.context }); } webhookManager.log({ success: true, url: `${url}/${shopId}/checkouts/${this.hash}?key=${this.key}`, mode: mode || Modes.SAFE, proxy: proxy ? proxy.proxy : undefined, product: { name: productName ? toTitleCase(productName) : 'Unknown', price: Intl.NumberFormat(this.formatter, { style: 'currency', currency: this.currency }).format(Number(finalPrice)), image: `${image}`.startsWith('http') ? image : `https:${image}`, size: size || 'N/A', url: productUrl || url }, store: { name, url }, delays: { monitor, retry }, profile: { name: profileName, type: cardType }, quantity }); } async enterQueue(from: string) { const { id, task: { store: { url } }, taskSession } = this.context; emitEvent(this.context, [id], { message: 'Entering queue' }); try { const cookies = await taskSession.cookies.get({}); const isNewQueue = cookies.find( ({ name }) => name === '_checkout_queue_token' ); if (isNewQueue) { this.nextQueueToken = decodeURIComponent(isNewQueue.value); this.useNewQueue = true; } const ctd = cookies.find((cookie: Cookie) => /_ctd/i.test(cookie.name)); if (ctd) { this.ctd = ctd.value; } const { redirect } = await enterQueue({ handler: this.handler, context: this.context, current: this.current, aborter: this.aborter, delayer: this.delayer, ctd: this.ctd || '', url, from }); if (redirect) { this.extractCheckoutHash(redirect); if (/processing/i.test(redirect)) { return States.GET_ORDER; } if (!this.hash) { return States.INIT_CHECKOUT; } if (this.proceedTo) { const { proceedTo } = this; this.proceedTo = null; return proceedTo; } return States.GET_CUSTOMER; } } catch (e) { // fail silently... } return States.GET_QUEUE; } extractRecaptcha(body: string) { const { id, logger } = this.context; if (!body) { return; } const match = body.match( /.*