├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .sequelizerc ├── LICENSE ├── README.md ├── flow-typed └── npm │ ├── bluebird_v3.x.x.js │ ├── body-parser_v1.x.x.js │ ├── debug_v2.x.x.js │ ├── express_v4.x.x.js │ ├── flow-bin_v0.x.x.js │ ├── jest_v20.x.x.js │ ├── lodash_v4.x.x.js │ ├── morgan_v1.x.x.js │ └── pg_v6.x.x.js ├── nodemon.json ├── package.json ├── react-ui ├── .env.development ├── .gitignore ├── README.md ├── flow-typed │ └── npm │ │ ├── @shopify │ │ └── polaris_vx.x.x.js │ │ ├── axios_v0.16.x.js │ │ ├── enzyme_v2.3.x.js │ │ ├── flow-bin_v0.x.x.js │ │ ├── react-redux_v5.x.x.js │ │ ├── react-router-dom_v4.x.x.js │ │ ├── react-router-redux_vx.x.x.js │ │ ├── react-test-renderer_vx.x.x.js │ │ ├── redux-thunk_vx.x.x.js │ │ └── redux_v3.x.x.js ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── containers │ ├── About.js │ ├── About.spec.js │ ├── ProductsPage.js │ ├── ProductsPage.spec.js │ └── __snapshots__ │ │ ├── About.spec.js.snap │ │ └── ProductsPage.spec.js.snap │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── redux │ ├── index.js │ └── products.js │ ├── registerServiceWorker.js │ ├── store.js │ └── types │ └── index.js ├── server ├── app.js ├── config │ └── index.js ├── db │ ├── connect.js │ ├── index.js │ ├── migrations │ │ └── 20170805140708-create-shop.js │ ├── models │ │ ├── index.js │ │ └── shop.js │ ├── sequelize_config.json │ └── session.js ├── routes │ ├── session-helper.js │ ├── setupMocks.js │ ├── shopify.js │ └── shopify.spec.js ├── server.js └── views │ ├── 500.pug │ ├── charge_declined.pug │ └── shopify_redirect.pug └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /compiled/** 2 | /flow-typed/** 3 | /public/** 4 | /node_modules/** 5 | /react-ui/flow-typed/** 6 | /react-ui/node_modules/** 7 | /react-ui/build/** -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb", "plugin:flowtype/recommended"], 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "jest/globals": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2017, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true, 14 | "experimentalObjectRestSpread": true 15 | } 16 | }, 17 | "rules": { 18 | "arrow-parens": 0, 19 | "comma-dangle": 0, 20 | "flowtype-errors/show-errors": 2, 21 | "flowtype-errors/enforce-min-coverage": [2, 50], 22 | "import/no-extraneous-dependencies": [2, { "devDependencies": true }], 23 | "jsx-a11y/img-has-alt": 0, 24 | "no-console": 0, 25 | "no-shadow": 0, 26 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 27 | "react/jsx-wrap-multilines": 0, 28 | "react/prop-types": 0 29 | }, 30 | "plugins": ["react", "jsx-a11y", "flowtype", "flowtype-errors", "jest"], 31 | "globals": { 32 | "__DEVSERVER__": true, 33 | "__DEVCLIENT__": true, 34 | "jest": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*node_modules.* 3 | [include] 4 | 5 | [libs] 6 | react-ui/flow-typed 7 | 8 | [options] 9 | 10 | [lints] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node build artifacts 2 | node_modules 3 | npm-debug.log 4 | 5 | # Local development 6 | *.env 7 | *.dev 8 | .vscode 9 | .DS_Store 10 | compiled 11 | coverage 12 | 13 | # Docker 14 | Dockerfile 15 | docker-compose.yml -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | 'config': path.resolve('./server/db', 'sequelize_config.json'), 5 | 'migrations-path': path.resolve('./server/db', 'migrations'), 6 | 'models-path': path.resolve('./server/db', 'models') 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017-present Mihovil Kovačević 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Create Shopify App with Node and React 3 | 4 | Shopify Application starter with React, Polaris, Express, and Postgres. 5 | 6 | ## Description 7 | 8 | Use this project as a starter for applications using the Shopify API. It gives you all the required code for authenticating with a shop via Oauth. It even includes billing. It also demonstrates the usage of the Embedded App SDK. 9 | 10 | The project has a create-react-app client application backed by an Express server. It fetches a list of products from the shop. You can then add more with the ResourcePicker from the Embedded App SDK. 11 | 12 | ### Why use this? 13 | 14 | If you're a Javascript developer, use create-shopify-app to kickstart your development. It saves you a lot of time you'd use on setting up the project. You get everything you need to build a modern Shopify app with your favorite tools: 15 | 16 | * React, JSX, ES6, and Flow syntax support. 17 | * A Webpack Dev server with live reloading 18 | * State management with Redux 19 | * React Router v4 20 | * Embedded App SDK and Polaris 21 | * Unit testing with Jest 22 | * All the code for authenticating with a shop via oAuth using Express middleware 23 | * Middleware for setting up billing and recurring charges 24 | * Best practices from the community 25 | 26 | ## Prerequisites 27 | 28 | This project uses Postgres for data persistence and Redis for session management. You'll need to install and run both. You'll also need Node and npm. 29 | 30 | Download the project from this Github repository and install dependencies: 31 | 32 | 1. Run **npm install** from the root to install main dependencies 33 | 2. Run **cd react-ui && npm install** for client-side dependencies 34 | 3. Expose your application to the Internet using ngrok. See [Shopify's documentation](https://help.shopify.com/api/tutorials/building-public-app) . (replace port 4567 with 3000) 35 | 36 | ## Getting started 37 | 38 | The following list of steps will get you ready for development. 39 | 40 | ### Step 1: Becoming a Shopify App Developer 41 | 42 | If you don't have a Shopify Partner account yet head over to http://shopify.com/partners to create one. You'll need it before you can start developing apps. 43 | 44 | Once you have a Partner account create a new application to get an API key and other API credentials. 45 | 46 | ### Step 2: Configuring your application 47 | 48 | When you start ngrok, it'll give you a random subdomain (*.ngrok.io). 49 | 50 | In the project root directory, open `server/config/index.js`. Set `APP_URL` to the subdomain ngrok assigned to you. In production, this value should match your deployment URL (for example, **.herokuapp.com). Also, set your `APP_NAME`. 51 | 52 | In the project root directory, create a new file named `.env` and open it in a text editor. Login to your Shopify partner account and find your App credentials. Set your API key and App secret in the `.env` file. 53 | 54 | ```sh 55 | SHOPIFY_API_KEY=your API key 56 | SHOPIFY_API_SECRET=app secret 57 | ``` 58 | 59 | In the `react-ui` directory, create a new file named `.env` and open it in a text editor. Set your API key and development store URL. 60 | 61 | ```sh 62 | REACT_APP_SHOPIFY_API_KEY=your API key 63 | REACT_APP_SHOP_ORIGIN=your-development-store.myshopify.com 64 | ``` 65 | 66 | You'll only use these values in development. The Embedded app SDK uses them to initialize itself. In production, they are injected by the Express server in the built client app. 67 | 68 | **Your api credentials should not be in source control**. In production, keep your keys in environment variables. 69 | 70 | In your partner dashboard, go to App info. For the App URL, set 71 | 72 | ``` 73 | https://#{app_url}/home 74 | ``` 75 | 76 | Here `app_url` is the root path of your application (the same value as APP_URL in your config file). 77 | 78 | For Whistlisted redirection URL, set 79 | 80 | ``` 81 | https://#{app_url}/auth/callback 82 | ``` 83 | 84 | Also, remember to check `enabled` for the embedded settings. 85 | 86 | You can set these URLs in the config file. But, the values in config should match the ones in the partner dashboard. 87 | 88 | ### Step 3: Set-up your database 89 | 90 | This project uses Postgres for its persistence layer, with Sequelize ORM. Create local databases for development and testing. Then run the Sequelize migration script to create a shop table: 91 | 92 | ```sh 93 | createdb shopify-app-development # or test/production 94 | npm run sequelize db:migrate 95 | ``` 96 | 97 | In production, you connect to the database through an environment variable DATABASE_URL. 98 | 99 | ### Step 4: Run the app on your local machine 100 | 101 | ```sh 102 | npm run start:dev 103 | ``` 104 | This will start `nodemon` on the server side and `create-react-app` on the client. The Node server will restart when you make changes in that part of the code. 105 | 106 | The page will reload if you make edits in the `react-ui` folder. You'll see the build errors and lint warnings in the console. 107 | 108 | ### Step 5: Install your app on a test store 109 | 110 | If you don't have one already, create a development store. Open [Shopify's documentation](https://help.shopify.com/api/tutorials/building-public-app). Scroll down to the **Install your app on a test store** section. Follow those steps. Once you start the installation process, the following will happen: 111 | 112 | 1. You'll see a screen to confirm the installation, with the scopes you requested. 113 | 2. Once you confirm, you'll have to accept a recurring application charge. It's only a test charge so don't worry. 114 | 3. You'll see the app inside the Shopify admin. You can play with it or start building. 115 | 116 | ## Deploying to Heroku 117 | 118 | ```sh 119 | heroku create your-app-name 120 | 121 | # Add Redis 122 | heroku addons:create heroku-redis:hobby-dev -a your-app 123 | 124 | # Add Postgres 125 | heroku addons:create heroku-postgresql:hobby-dev -a your-app 126 | 127 | # Deploy to Heroku server 128 | git push heroku master 129 | 130 | # Set environment variables 131 | heroku config:set APP_URL=your-app.herokuapp.com 132 | heroku config:set SHOPIFY_API_KEY=your API key 133 | heroku config:set SHOPIFY_API_SECRET=app secret 134 | heroku config:set SESSION_SECRET=session secret 135 | 136 | # Run the migration 137 | heroku run sequelize db:migrate 138 | 139 | # Open Link in browser 140 | heroku open 141 | ``` 142 | 143 | ## Testing 144 | 145 | We use Jest for both client and server tests. The `create-react-app` project comes with Jest by default. For the server side, we use a custom configuration. Jest has spy and mock capabilities so there's no need for additional libraries. 146 | 147 | ```sh 148 | 149 | # Run client tests in watch mode 150 | npm run test:client 151 | 152 | # Run server tests in watch mode 153 | npm run test:server 154 | 155 | # Run all tests for Continuous integration 156 | npm run test 157 | ``` 158 | 159 | ## License 160 | 161 | [MIT](LICENSE) 162 | -------------------------------------------------------------------------------- /flow-typed/npm/bluebird_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ce48a8f3b5f26602d21681e3ab5c2dbc 2 | // flow-typed version: ae20041ead/bluebird_v3.x.x/flow_>=v0.47.x 3 | 4 | type Bluebird$RangeError = Error; 5 | type Bluebird$CancellationErrors = Error; 6 | type Bluebird$TimeoutError = Error; 7 | type Bluebird$RejectionError = Error; 8 | type Bluebird$OperationalError = Error; 9 | 10 | type Bluebird$ConcurrencyOption = { 11 | concurrency: number, 12 | }; 13 | type Bluebird$SpreadOption = { 14 | spread: boolean; 15 | }; 16 | type Bluebird$MultiArgsOption = { 17 | multiArgs: boolean; 18 | }; 19 | type Bluebird$BluebirdConfig = { 20 | warnings?: boolean, 21 | longStackTraces?: boolean, 22 | cancellation?: boolean, 23 | monitoring?: boolean 24 | }; 25 | 26 | declare class Bluebird$PromiseInspection { 27 | isCancelled(): boolean; 28 | isFulfilled(): boolean; 29 | isRejected(): boolean; 30 | pending(): boolean; 31 | reason(): any; 32 | value(): T; 33 | } 34 | 35 | type Bluebird$PromisifyOptions = {| 36 | multiArgs?: boolean, 37 | context: any 38 | |}; 39 | 40 | declare type Bluebird$PromisifyAllOptions = { 41 | suffix?: string; 42 | filter?: (name: string, func: Function, target?: any, passesDefaultFilter?: boolean) => boolean; 43 | // The promisifier gets a reference to the original method and should return a function which returns a promise 44 | promisifier?: (originalMethod: Function) => () => Bluebird$Promise ; 45 | }; 46 | 47 | declare type $Promisable = Promise | T; 48 | 49 | declare class Bluebird$Disposable { 50 | 51 | } 52 | 53 | declare class Bluebird$Promise<+R> extends Promise{ 54 | static Defer: Class; 55 | static PromiseInspection: Class>; 56 | 57 | static all>( 58 | Promises: Iterable | $Promisable> 59 | ): Bluebird$Promise>; 60 | static props( 61 | input: Object | Map<*,*> | $Promisable> 62 | ): Bluebird$Promise<*>; 63 | static any>( 64 | Promises: Iterable | $Promisable> 65 | ): Bluebird$Promise; 66 | static race>( 67 | Promises: Iterable | $Promisable> 68 | ): Bluebird$Promise; 69 | static reject(error?: any): Bluebird$Promise; 70 | static resolve(object?: $Promisable): Bluebird$Promise; 71 | static some>( 72 | Promises: Iterable | $Promisable>, 73 | count: number 74 | ): Bluebird$Promise>; 75 | static join( 76 | value1: $Promisable, 77 | handler: (a: A) => $Promisable 78 | ): Bluebird$Promise; 79 | static join( 80 | value1: $Promisable, 81 | value2: $Promisable, 82 | handler: (a: A, b: B) => $Promisable 83 | ): Bluebird$Promise; 84 | static join( 85 | value1: $Promisable, 86 | value2: $Promisable, 87 | value3: $Promisable, 88 | handler: (a: A, b: B, c: C) => $Promisable 89 | ): Bluebird$Promise; 90 | static map>( 91 | Promises: Iterable | $Promisable>, 92 | mapper: (item: T, index: number, arrayLength: number) => $Promisable, 93 | options?: Bluebird$ConcurrencyOption 94 | ): Bluebird$Promise>; 95 | static mapSeries>( 96 | Promises: Iterable | $Promisable>, 97 | mapper: (item: T, index: number, arrayLength: number) => $Promisable 98 | ): Bluebird$Promise>; 99 | static reduce>( 100 | Promises: Iterable | $Promisable>, 101 | reducer: (total: U, current: T, index: number, arrayLength: number) => $Promisable, 102 | initialValue?: $Promisable 103 | ): Bluebird$Promise; 104 | static filter>( 105 | Promises: Iterable | $Promisable>, 106 | filterer: (item: T, index: number, arrayLength: number) => $Promisable, 107 | option?: Bluebird$ConcurrencyOption 108 | ): Bluebird$Promise>; 109 | static each>( 110 | Promises: Iterable | $Promisable>, 111 | iterator: (item: T, index: number, arrayLength: number) => $Promisable, 112 | ): Bluebird$Promise>; 113 | static try(fn: () => $Promisable, args: ?Array, ctx: ?any): Bluebird$Promise; 114 | static attempt(fn: () => $Promisable, args: ?Array, ctx: ?any): Bluebird$Promise; 115 | static delay(ms: number, value: $Promisable): Bluebird$Promise; 116 | static delay(ms: number): Bluebird$Promise; 117 | static config(config: Bluebird$BluebirdConfig): void; 118 | 119 | static defer(): Bluebird$Defer; 120 | static setScheduler(scheduler: (callback: (...args: Array) => void) => void): void; 121 | static promisify(nodeFunction: Function, receiver?: Bluebird$PromisifyOptions): Function; 122 | static promisifyAll(target: Object|Array, options?: Bluebird$PromisifyAllOptions): void; 123 | 124 | static coroutine(generatorFunction: Function): Function; 125 | static spawn(generatorFunction: Function): Promise; 126 | 127 | // It doesn't seem possible to have type-generics for a variable number of arguments. 128 | // Handle up to 3 arguments, then just give up and accept 'any'. 129 | static method>(fn: () => R): () => Bluebird$Promise; 130 | static method, A>(fn: (a: A) => R): (a: A) => Bluebird$Promise; 131 | static method, A, B>(fn: (a: A, b: B) => R): (a: A, b: B) => Bluebird$Promise; 132 | static method, A, B, C>(fn: (a: A, b: B, c: C) => R): (a: A, b: B, c: C) => Bluebird$Promise; 133 | static method>(fn: (...args: any) => R): (...args: any) => Bluebird$Promise; 134 | 135 | static cast(value: $Promisable): Bluebird$Promise; 136 | static bind(ctx: any): Bluebird$Promise; 137 | static is(value: any): boolean; 138 | static longStackTraces(): void; 139 | 140 | static onPossiblyUnhandledRejection(handler: (reason: any) => any): void; 141 | static fromCallback(resolver: (fn: (error: ?Error, value?: T) => any) => any, options?: Bluebird$MultiArgsOption): Bluebird$Promise; 142 | 143 | constructor(callback: ( 144 | resolve: (result?: $Promisable) => void, 145 | reject: (error?: any) => void 146 | ) => mixed): void; 147 | then(onFulfill?: (value: R) => $Promisable, onReject?: (error: any) => $Promisable): Bluebird$Promise; 148 | 149 | catch(err: Class, onReject: (error: ErrorT) => $Promisable): Bluebird$Promise; 150 | catch(err1: Class, err2: Class, onReject: (error: ErrorT) => $Promisable): Bluebird$Promise; 151 | catch(err1: Class, err2: Class, err3: Class, onReject: (error: ErrorT) => $Promisable): Bluebird$Promise; 152 | catch(onReject?: (error: any) => $Promisable): Bluebird$Promise; 153 | caught(err: Class, onReject: (error: Error) => $Promisable): Bluebird$Promise; 154 | caught(err1: Class, err2: Class, onReject: (error: ErrorT) => $Promisable): Bluebird$Promise; 155 | caught(err1: Class, err2: Class, err3: Class, onReject: (error: ErrorT) => $Promisable): Bluebird$Promise; 156 | caught(onReject: (error: any) => $Promisable): Bluebird$Promise; 157 | 158 | error(onReject?: (error: any) => ?$Promisable): Bluebird$Promise; 159 | done(onFulfill?: (value: R) => mixed, onReject?: (error: any) => mixed): void; 160 | finally(onDone?: (value: R) => mixed): Bluebird$Promise; 161 | lastly(onDone?: (value: R) => mixed): Bluebird$Promise; 162 | tap(onDone?: (value: R) => mixed): Bluebird$Promise; 163 | delay(ms: number): Bluebird$Promise; 164 | timeout(ms: number, message?: string): Bluebird$Promise; 165 | cancel(): void; 166 | 167 | bind(ctx: any): Bluebird$Promise; 168 | call(propertyName: string, ...args: Array): Bluebird$Promise; 169 | throw(reason: Error): Bluebird$Promise; 170 | thenThrow(reason: Error): Bluebird$Promise; 171 | all(): Bluebird$Promise>; 172 | any(): Bluebird$Promise; 173 | some(count: number): Bluebird$Promise>; 174 | race(): Bluebird$Promise; 175 | map(mapper: (item: T, index: number, arrayLength: number) => $Promisable, options?: Bluebird$ConcurrencyOption): Bluebird$Promise>; 176 | mapSeries(mapper: (item: T, index: number, arrayLength: number) => $Promisable): Bluebird$Promise>; 177 | reduce( 178 | reducer: (total: T, item: U, index: number, arrayLength: number) => $Promisable, 179 | initialValue?: $Promisable 180 | ): Bluebird$Promise; 181 | filter(filterer: (item: T, index: number, arrayLength: number) => $Promisable, options?: Bluebird$ConcurrencyOption): Bluebird$Promise>; 182 | each(iterator: (item: T, index: number, arrayLength: number) => $Promisable): Bluebird$Promise>; 183 | asCallback(callback: (error: ?any, value?: T) => any, options?: Bluebird$SpreadOption): void; 184 | return(value: T): Bluebird$Promise; 185 | thenReturn(value: T): Bluebird$Promise; 186 | spread(...args: Array): Bluebird$Promise<*>; 187 | 188 | reflect(): Bluebird$Promise>; 189 | 190 | isFulfilled(): bool; 191 | isRejected(): bool; 192 | isPending(): bool; 193 | isResolved(): bool; 194 | 195 | value(): R; 196 | reason(): any; 197 | 198 | disposer(disposer: (value: R, promise: Promise<*>) => void): Bluebird$Disposable; 199 | 200 | static using(disposable: Bluebird$Disposable, handler: (value: T) => $Promisable): Bluebird$Promise; 201 | 202 | } 203 | 204 | declare class Bluebird$Defer { 205 | promise: Bluebird$Promise<*>; 206 | resolve: (value: any) => any; 207 | reject: (value: any) => any; 208 | } 209 | 210 | declare module 'bluebird' { 211 | declare var exports: typeof Bluebird$Promise; 212 | 213 | declare type Disposable = Bluebird$Disposable; 214 | } 215 | -------------------------------------------------------------------------------- /flow-typed/npm/body-parser_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: a4c337c9e418b68b652f62cffd5f2a48 2 | // flow-typed version: b43dff3e0e/body-parser_v1.x.x/flow_>=v0.17.x 3 | 4 | import type { Middleware, $Request, $Response } from 'express'; 5 | 6 | declare type bodyParser$Options = { 7 | inflate?: boolean; 8 | limit?: number | string; 9 | type?: string | string[] | ((req: $Request) => any); 10 | verify?: (req: $Request, res: $Response, buf: Buffer, encoding: string) => void; 11 | }; 12 | 13 | declare type bodyParser$OptionsText = bodyParser$Options & { 14 | reviver?: (key: string, value: any) => any; 15 | strict?: boolean; 16 | }; 17 | 18 | declare type bodyParser$OptionsJson = bodyParser$Options & { 19 | reviver?: (key: string, value: any) => any; 20 | strict?: boolean; 21 | }; 22 | 23 | declare type bodyParser$OptionsUrlencoded = bodyParser$Options & { 24 | extended?: boolean; 25 | parameterLimit?: number; 26 | }; 27 | 28 | declare module "body-parser" { 29 | 30 | declare type Options = bodyParser$Options; 31 | declare type OptionsText = bodyParser$OptionsText; 32 | declare type OptionsJson = bodyParser$OptionsJson; 33 | declare type OptionsUrlencoded = bodyParser$OptionsUrlencoded; 34 | 35 | declare function json(options?: OptionsJson): Middleware; 36 | 37 | declare function raw(options?: Options): Middleware; 38 | 39 | declare function text(options?: OptionsText): Middleware; 40 | 41 | declare function urlencoded(options?: OptionsUrlencoded): Middleware; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /flow-typed/npm/debug_v2.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 70424effbac508a7e7a8799e10ed5493 2 | // flow-typed version: 94e9f7e0a4/debug_v2.x.x/flow_>=v0.28.x 3 | 4 | declare module 'debug' { 5 | declare type Debugger = { 6 | (...args: Array): void, 7 | (formatter: string, ...args: Array): void, 8 | (err: Error, ...args: Array): void, 9 | enabled: boolean, 10 | log: () => {}, 11 | namespace: string; 12 | }; 13 | 14 | declare function exports(namespace: string): Debugger; 15 | 16 | declare var names: Array; 17 | declare var skips: Array; 18 | declare var colors: Array; 19 | 20 | declare function disable(): void; 21 | declare function enable(namespaces: string): void; 22 | declare function enabled(name: string): boolean; 23 | declare function humanize(): void; 24 | declare function useColors(): boolean; 25 | declare function log(): void; 26 | 27 | declare var formatters: { 28 | [formatter: string]: () => {} 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /flow-typed/npm/express_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: d375d667aaa59b8207e6a167cc9a06fe 2 | // flow-typed version: ef2fdb0770/express_v4.x.x/flow_>=v0.32.x 3 | 4 | import type { Server } from 'http'; 5 | import type { Socket } from 'net'; 6 | 7 | declare type express$RouterOptions = { 8 | caseSensitive?: boolean, 9 | mergeParams?: boolean, 10 | strict?: boolean 11 | }; 12 | 13 | declare class express$RequestResponseBase { 14 | app: express$Application; 15 | get(field: string): string | void; 16 | } 17 | 18 | declare type express$RequestParams = { 19 | [param: string]: string 20 | } 21 | 22 | declare class express$Request extends http$IncomingMessage mixins express$RequestResponseBase { 23 | baseUrl: string; 24 | body: any; 25 | cookies: {[cookie: string]: string}; 26 | connection: Socket; 27 | fresh: boolean; 28 | hostname: string; 29 | ip: string; 30 | ips: Array; 31 | method: string; 32 | originalUrl: string; 33 | params: express$RequestParams; 34 | path: string; 35 | protocol: 'https' | 'http'; 36 | query: {[name: string]: string}; 37 | route: string; 38 | secure: boolean; 39 | signedCookies: {[signedCookie: string]: string}; 40 | stale: boolean; 41 | subdomains: Array; 42 | xhr: boolean; 43 | accepts(types: string): string | false; 44 | accepts(types: Array): string | false; 45 | acceptsCharsets(...charsets: Array): string | false; 46 | acceptsEncodings(...encoding: Array): string | false; 47 | acceptsLanguages(...lang: Array): string | false; 48 | header(field: string): string | void; 49 | is(type: string): boolean; 50 | param(name: string, defaultValue?: string): string | void; 51 | } 52 | 53 | declare type express$CookieOptions = { 54 | domain?: string, 55 | encode?: (value: string) => string, 56 | expires?: Date, 57 | httpOnly?: boolean, 58 | maxAge?: number, 59 | path?: string, 60 | secure?: boolean, 61 | signed?: boolean 62 | }; 63 | 64 | declare type express$RenderCallback = (err: Error | null, html?: string) => mixed; 65 | 66 | declare type express$SendFileOptions = { 67 | maxAge?: number, 68 | root?: string, 69 | lastModified?: boolean, 70 | headers?: {[name: string]: string}, 71 | dotfiles?: 'allow' | 'deny' | 'ignore' 72 | }; 73 | 74 | declare class express$Response extends http$ServerResponse mixins express$RequestResponseBase { 75 | headersSent: boolean; 76 | locals: {[name: string]: mixed}; 77 | append(field: string, value?: string): this; 78 | attachment(filename?: string): this; 79 | cookie(name: string, value: string, options?: express$CookieOptions): this; 80 | clearCookie(name: string, options?: express$CookieOptions): this; 81 | download(path: string, filename?: string, callback?: (err?: ?Error) => void): this; 82 | format(typesObject: {[type: string]: Function}): this; 83 | json(body?: mixed): this; 84 | jsonp(body?: mixed): this; 85 | links(links: {[name: string]: string}): this; 86 | location(path: string): this; 87 | redirect(url: string, ...args: Array): this; 88 | redirect(status: number, url: string, ...args: Array): this; 89 | render(view: string, locals?: {[name: string]: mixed}, callback?: express$RenderCallback): this; 90 | send(body?: mixed): this; 91 | sendFile(path: string, options?: express$SendFileOptions, callback?: (err?: ?Error) => mixed): this; 92 | sendStatus(statusCode: number): this; 93 | header(field: string, value?: string): this; 94 | header(headers: {[name: string]: string}): this; 95 | set(field: string, value?: string|string[]): this; 96 | set(headers: {[name: string]: string}): this; 97 | status(statusCode: number): this; 98 | type(type: string): this; 99 | vary(field: string): this; 100 | req: express$Request; 101 | } 102 | 103 | declare type express$NextFunction = (err?: ?Error | 'route') => mixed; 104 | declare type express$Middleware = 105 | ((req: express$Request, res: express$Response, next: express$NextFunction) => mixed) | 106 | ((error: ?Error, req: express$Request, res: express$Response, next: express$NextFunction) => mixed); 107 | declare interface express$RouteMethodType { 108 | (middleware: express$Middleware): T; 109 | (...middleware: Array): T; 110 | (path: string|RegExp|string[], ...middleware: Array): T; 111 | } 112 | declare class express$Route { 113 | all: express$RouteMethodType; 114 | get: express$RouteMethodType; 115 | post: express$RouteMethodType; 116 | put: express$RouteMethodType; 117 | head: express$RouteMethodType; 118 | delete: express$RouteMethodType; 119 | options: express$RouteMethodType; 120 | trace: express$RouteMethodType; 121 | copy: express$RouteMethodType; 122 | lock: express$RouteMethodType; 123 | mkcol: express$RouteMethodType; 124 | move: express$RouteMethodType; 125 | purge: express$RouteMethodType; 126 | propfind: express$RouteMethodType; 127 | proppatch: express$RouteMethodType; 128 | unlock: express$RouteMethodType; 129 | report: express$RouteMethodType; 130 | mkactivity: express$RouteMethodType; 131 | checkout: express$RouteMethodType; 132 | merge: express$RouteMethodType; 133 | 134 | // @TODO Missing 'm-search' but get flow illegal name error. 135 | 136 | notify: express$RouteMethodType; 137 | subscribe: express$RouteMethodType; 138 | unsubscribe: express$RouteMethodType; 139 | patch: express$RouteMethodType; 140 | search: express$RouteMethodType; 141 | connect: express$RouteMethodType; 142 | } 143 | 144 | declare class express$Router extends express$Route { 145 | constructor(options?: express$RouterOptions): void; 146 | route(path: string): express$Route; 147 | static (options?: express$RouterOptions): express$Router; 148 | use(middleware: express$Middleware): this; 149 | use(...middleware: Array): this; 150 | use(path: string|RegExp|string[], ...middleware: Array): this; 151 | use(path: string, router: express$Router): this; 152 | handle(req: http$IncomingMessage, res: http$ServerResponse, next: express$NextFunction): void; 153 | 154 | // Can't use regular callable signature syntax due to https://github.com/facebook/flow/issues/3084 155 | $call: (req: http$IncomingMessage, res: http$ServerResponse, next?: ?express$NextFunction) => void; 156 | } 157 | 158 | declare class express$Application extends express$Router mixins events$EventEmitter { 159 | constructor(): void; 160 | locals: {[name: string]: mixed}; 161 | mountpath: string; 162 | listen(port: number, hostname?: string, backlog?: number, callback?: (err?: ?Error) => mixed): Server; 163 | listen(port: number, hostname?: string, callback?: (err?: ?Error) => mixed): Server; 164 | listen(port: number, callback?: (err?: ?Error) => mixed): Server; 165 | listen(path: string, callback?: (err?: ?Error) => mixed): Server; 166 | listen(handle: Object, callback?: (err?: ?Error) => mixed): Server; 167 | disable(name: string): void; 168 | disabled(name: string): boolean; 169 | enable(name: string): express$Application; 170 | enabled(name: string): boolean; 171 | engine(name: string, callback: Function): void; 172 | /** 173 | * Mixed will not be taken as a value option. Issue around using the GET http method name and the get for settings. 174 | */ 175 | // get(name: string): mixed; 176 | set(name: string, value: mixed): mixed; 177 | render(name: string, optionsOrFunction: {[name: string]: mixed}, callback: express$RenderCallback): void; 178 | handle(req: http$IncomingMessage, res: http$ServerResponse, next?: ?express$NextFunction): void; 179 | } 180 | 181 | declare module 'express' { 182 | declare export type RouterOptions = express$RouterOptions; 183 | declare export type CookieOptions = express$CookieOptions; 184 | declare export type Middleware = express$Middleware; 185 | declare export type NextFunction = express$NextFunction; 186 | declare export type RequestParams = express$RequestParams; 187 | declare export type $Response = express$Response; 188 | declare export type $Request = express$Request; 189 | declare export type $Application = express$Application; 190 | 191 | declare module.exports: { 192 | (): express$Application, // If you try to call like a function, it will use this signature 193 | static: (root: string, options?: Object) => express$Middleware, // `static` property on the function 194 | Router: typeof express$Router, // `Router` property on the function 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 67b0c3a16b2d6f8ef0a31a5745a0b3e1 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v20.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 703c3fecde023d82c652badb65825ad2 2 | // flow-typed version: b2a49dc910/jest_v20.x.x/flow_>=v0.39.x 3 | 4 | type JestMockFn, TReturn> = { 5 | (...args: TArguments): TReturn, 6 | /** 7 | * An object for introspecting mock calls 8 | */ 9 | mock: { 10 | /** 11 | * An array that represents all calls that have been made into this mock 12 | * function. Each call is represented by an array of arguments that were 13 | * passed during the call. 14 | */ 15 | calls: Array, 16 | /** 17 | * An array that contains all the object instances that have been 18 | * instantiated from this mock function. 19 | */ 20 | instances: Array 21 | }, 22 | /** 23 | * Resets all information stored in the mockFn.mock.calls and 24 | * mockFn.mock.instances arrays. Often this is useful when you want to clean 25 | * up a mock's usage data between two assertions. 26 | */ 27 | mockClear(): void, 28 | /** 29 | * Resets all information stored in the mock. This is useful when you want to 30 | * completely restore a mock back to its initial state. 31 | */ 32 | mockReset(): void, 33 | /** 34 | * Removes the mock and restores the initial implementation. This is useful 35 | * when you want to mock functions in certain test cases and restore the 36 | * original implementation in others. Beware that mockFn.mockRestore only 37 | * works when mock was created with jest.spyOn. Thus you have to take care of 38 | * restoration yourself when manually assigning jest.fn(). 39 | */ 40 | mockRestore(): void, 41 | /** 42 | * Accepts a function that should be used as the implementation of the mock. 43 | * The mock itself will still record all calls that go into and instances 44 | * that come from itself -- the only difference is that the implementation 45 | * will also be executed when the mock is called. 46 | */ 47 | mockImplementation( 48 | fn: (...args: TArguments) => TReturn, 49 | ): JestMockFn, 50 | /** 51 | * Accepts a function that will be used as an implementation of the mock for 52 | * one call to the mocked function. Can be chained so that multiple function 53 | * calls produce different results. 54 | */ 55 | mockImplementationOnce( 56 | fn: (...args: TArguments) => TReturn, 57 | ): JestMockFn, 58 | /** 59 | * Just a simple sugar function for returning `this` 60 | */ 61 | mockReturnThis(): void, 62 | /** 63 | * Deprecated: use jest.fn(() => value) instead 64 | */ 65 | mockReturnValue(value: TReturn): JestMockFn, 66 | /** 67 | * Sugar for only returning a value once inside your mock 68 | */ 69 | mockReturnValueOnce(value: TReturn): JestMockFn 70 | }; 71 | 72 | type JestAsymmetricEqualityType = { 73 | /** 74 | * A custom Jasmine equality tester 75 | */ 76 | asymmetricMatch(value: mixed): boolean 77 | }; 78 | 79 | type JestCallsType = { 80 | allArgs(): mixed, 81 | all(): mixed, 82 | any(): boolean, 83 | count(): number, 84 | first(): mixed, 85 | mostRecent(): mixed, 86 | reset(): void 87 | }; 88 | 89 | type JestClockType = { 90 | install(): void, 91 | mockDate(date: Date): void, 92 | tick(milliseconds?: number): void, 93 | uninstall(): void 94 | }; 95 | 96 | type JestMatcherResult = { 97 | message?: string | (() => string), 98 | pass: boolean 99 | }; 100 | 101 | type JestMatcher = (actual: any, expected: any) => JestMatcherResult; 102 | 103 | type JestPromiseType = { 104 | /** 105 | * Use rejects to unwrap the reason of a rejected promise so any other 106 | * matcher can be chained. If the promise is fulfilled the assertion fails. 107 | */ 108 | rejects: JestExpectType, 109 | /** 110 | * Use resolves to unwrap the value of a fulfilled promise so any other 111 | * matcher can be chained. If the promise is rejected the assertion fails. 112 | */ 113 | resolves: JestExpectType 114 | }; 115 | 116 | /** 117 | * Plugin: jest-enzyme 118 | */ 119 | type EnzymeMatchersType = { 120 | toBeChecked(): void, 121 | toBeDisabled(): void, 122 | toBeEmpty(): void, 123 | toBePresent(): void, 124 | toContainReact(element: React$Element): void, 125 | toHaveClassName(className: string): void, 126 | toHaveHTML(html: string): void, 127 | toHaveProp(propKey: string, propValue?: any): void, 128 | toHaveRef(refName: string): void, 129 | toHaveState(stateKey: string, stateValue?: any): void, 130 | toHaveStyle(styleKey: string, styleValue?: any): void, 131 | toHaveTagName(tagName: string): void, 132 | toHaveText(text: string): void, 133 | toIncludeText(text: string): void, 134 | toHaveValue(value: any): void, 135 | toMatchElement(element: React$Element): void, 136 | toMatchSelector(selector: string): void, 137 | }; 138 | 139 | type JestExpectType = { 140 | not: JestExpectType & EnzymeMatchersType, 141 | /** 142 | * If you have a mock function, you can use .lastCalledWith to test what 143 | * arguments it was last called with. 144 | */ 145 | lastCalledWith(...args: Array): void, 146 | /** 147 | * toBe just checks that a value is what you expect. It uses === to check 148 | * strict equality. 149 | */ 150 | toBe(value: any): void, 151 | /** 152 | * Use .toHaveBeenCalled to ensure that a mock function got called. 153 | */ 154 | toBeCalled(): void, 155 | /** 156 | * Use .toBeCalledWith to ensure that a mock function was called with 157 | * specific arguments. 158 | */ 159 | toBeCalledWith(...args: Array): void, 160 | /** 161 | * Using exact equality with floating point numbers is a bad idea. Rounding 162 | * means that intuitive things fail. 163 | */ 164 | toBeCloseTo(num: number, delta: any): void, 165 | /** 166 | * Use .toBeDefined to check that a variable is not undefined. 167 | */ 168 | toBeDefined(): void, 169 | /** 170 | * Use .toBeFalsy when you don't care what a value is, you just want to 171 | * ensure a value is false in a boolean context. 172 | */ 173 | toBeFalsy(): void, 174 | /** 175 | * To compare floating point numbers, you can use toBeGreaterThan. 176 | */ 177 | toBeGreaterThan(number: number): void, 178 | /** 179 | * To compare floating point numbers, you can use toBeGreaterThanOrEqual. 180 | */ 181 | toBeGreaterThanOrEqual(number: number): void, 182 | /** 183 | * To compare floating point numbers, you can use toBeLessThan. 184 | */ 185 | toBeLessThan(number: number): void, 186 | /** 187 | * To compare floating point numbers, you can use toBeLessThanOrEqual. 188 | */ 189 | toBeLessThanOrEqual(number: number): void, 190 | /** 191 | * Use .toBeInstanceOf(Class) to check that an object is an instance of a 192 | * class. 193 | */ 194 | toBeInstanceOf(cls: Class<*>): void, 195 | /** 196 | * .toBeNull() is the same as .toBe(null) but the error messages are a bit 197 | * nicer. 198 | */ 199 | toBeNull(): void, 200 | /** 201 | * Use .toBeTruthy when you don't care what a value is, you just want to 202 | * ensure a value is true in a boolean context. 203 | */ 204 | toBeTruthy(): void, 205 | /** 206 | * Use .toBeUndefined to check that a variable is undefined. 207 | */ 208 | toBeUndefined(): void, 209 | /** 210 | * Use .toContain when you want to check that an item is in a list. For 211 | * testing the items in the list, this uses ===, a strict equality check. 212 | */ 213 | toContain(item: any): void, 214 | /** 215 | * Use .toContainEqual when you want to check that an item is in a list. For 216 | * testing the items in the list, this matcher recursively checks the 217 | * equality of all fields, rather than checking for object identity. 218 | */ 219 | toContainEqual(item: any): void, 220 | /** 221 | * Use .toEqual when you want to check that two objects have the same value. 222 | * This matcher recursively checks the equality of all fields, rather than 223 | * checking for object identity. 224 | */ 225 | toEqual(value: any): void, 226 | /** 227 | * Use .toHaveBeenCalled to ensure that a mock function got called. 228 | */ 229 | toHaveBeenCalled(): void, 230 | /** 231 | * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact 232 | * number of times. 233 | */ 234 | toHaveBeenCalledTimes(number: number): void, 235 | /** 236 | * Use .toHaveBeenCalledWith to ensure that a mock function was called with 237 | * specific arguments. 238 | */ 239 | toHaveBeenCalledWith(...args: Array): void, 240 | /** 241 | * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called 242 | * with specific arguments. 243 | */ 244 | toHaveBeenLastCalledWith(...args: Array): void, 245 | /** 246 | * Check that an object has a .length property and it is set to a certain 247 | * numeric value. 248 | */ 249 | toHaveLength(number: number): void, 250 | /** 251 | * 252 | */ 253 | toHaveProperty(propPath: string, value?: any): void, 254 | /** 255 | * Use .toMatch to check that a string matches a regular expression or string. 256 | */ 257 | toMatch(regexpOrString: RegExp | string): void, 258 | /** 259 | * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. 260 | */ 261 | toMatchObject(object: Object): void, 262 | /** 263 | * This ensures that a React component matches the most recent snapshot. 264 | */ 265 | toMatchSnapshot(name?: string): void, 266 | /** 267 | * Use .toThrow to test that a function throws when it is called. 268 | * If you want to test that a specific error gets thrown, you can provide an 269 | * argument to toThrow. The argument can be a string for the error message, 270 | * a class for the error, or a regex that should match the error. 271 | * 272 | * Alias: .toThrowError 273 | */ 274 | toThrow(message?: string | Error | RegExp): void, 275 | toThrowError(message?: string | Error | RegExp): void, 276 | /** 277 | * Use .toThrowErrorMatchingSnapshot to test that a function throws a error 278 | * matching the most recent snapshot when it is called. 279 | */ 280 | toThrowErrorMatchingSnapshot(): void 281 | }; 282 | 283 | type JestObjectType = { 284 | /** 285 | * Disables automatic mocking in the module loader. 286 | * 287 | * After this method is called, all `require()`s will return the real 288 | * versions of each module (rather than a mocked version). 289 | */ 290 | disableAutomock(): JestObjectType, 291 | /** 292 | * An un-hoisted version of disableAutomock 293 | */ 294 | autoMockOff(): JestObjectType, 295 | /** 296 | * Enables automatic mocking in the module loader. 297 | */ 298 | enableAutomock(): JestObjectType, 299 | /** 300 | * An un-hoisted version of enableAutomock 301 | */ 302 | autoMockOn(): JestObjectType, 303 | /** 304 | * Clears the mock.calls and mock.instances properties of all mocks. 305 | * Equivalent to calling .mockClear() on every mocked function. 306 | */ 307 | clearAllMocks(): JestObjectType, 308 | /** 309 | * Resets the state of all mocks. Equivalent to calling .mockReset() on every 310 | * mocked function. 311 | */ 312 | resetAllMocks(): JestObjectType, 313 | /** 314 | * Removes any pending timers from the timer system. 315 | */ 316 | clearAllTimers(): void, 317 | /** 318 | * The same as `mock` but not moved to the top of the expectation by 319 | * babel-jest. 320 | */ 321 | doMock(moduleName: string, moduleFactory?: any): JestObjectType, 322 | /** 323 | * The same as `unmock` but not moved to the top of the expectation by 324 | * babel-jest. 325 | */ 326 | dontMock(moduleName: string): JestObjectType, 327 | /** 328 | * Returns a new, unused mock function. Optionally takes a mock 329 | * implementation. 330 | */ 331 | fn, TReturn>( 332 | implementation?: (...args: TArguments) => TReturn, 333 | ): JestMockFn, 334 | /** 335 | * Determines if the given function is a mocked function. 336 | */ 337 | isMockFunction(fn: Function): boolean, 338 | /** 339 | * Given the name of a module, use the automatic mocking system to generate a 340 | * mocked version of the module for you. 341 | */ 342 | genMockFromModule(moduleName: string): any, 343 | /** 344 | * Mocks a module with an auto-mocked version when it is being required. 345 | * 346 | * The second argument can be used to specify an explicit module factory that 347 | * is being run instead of using Jest's automocking feature. 348 | * 349 | * The third argument can be used to create virtual mocks -- mocks of modules 350 | * that don't exist anywhere in the system. 351 | */ 352 | mock( 353 | moduleName: string, 354 | moduleFactory?: any, 355 | options?: Object 356 | ): JestObjectType, 357 | /** 358 | * Resets the module registry - the cache of all required modules. This is 359 | * useful to isolate modules where local state might conflict between tests. 360 | */ 361 | resetModules(): JestObjectType, 362 | /** 363 | * Exhausts the micro-task queue (usually interfaced in node via 364 | * process.nextTick). 365 | */ 366 | runAllTicks(): void, 367 | /** 368 | * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), 369 | * setInterval(), and setImmediate()). 370 | */ 371 | runAllTimers(): void, 372 | /** 373 | * Exhausts all tasks queued by setImmediate(). 374 | */ 375 | runAllImmediates(): void, 376 | /** 377 | * Executes only the macro task queue (i.e. all tasks queued by setTimeout() 378 | * or setInterval() and setImmediate()). 379 | */ 380 | runTimersToTime(msToRun: number): void, 381 | /** 382 | * Executes only the macro-tasks that are currently pending (i.e., only the 383 | * tasks that have been queued by setTimeout() or setInterval() up to this 384 | * point) 385 | */ 386 | runOnlyPendingTimers(): void, 387 | /** 388 | * Explicitly supplies the mock object that the module system should return 389 | * for the specified module. Note: It is recommended to use jest.mock() 390 | * instead. 391 | */ 392 | setMock(moduleName: string, moduleExports: any): JestObjectType, 393 | /** 394 | * Indicates that the module system should never return a mocked version of 395 | * the specified module from require() (e.g. that it should always return the 396 | * real module). 397 | */ 398 | unmock(moduleName: string): JestObjectType, 399 | /** 400 | * Instructs Jest to use fake versions of the standard timer functions 401 | * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, 402 | * setImmediate and clearImmediate). 403 | */ 404 | useFakeTimers(): JestObjectType, 405 | /** 406 | * Instructs Jest to use the real versions of the standard timer functions. 407 | */ 408 | useRealTimers(): JestObjectType, 409 | /** 410 | * Creates a mock function similar to jest.fn but also tracks calls to 411 | * object[methodName]. 412 | */ 413 | spyOn(object: Object, methodName: string): JestMockFn 414 | }; 415 | 416 | type JestSpyType = { 417 | calls: JestCallsType 418 | }; 419 | 420 | /** Runs this function after every test inside this context */ 421 | declare function afterEach(fn: (done: () => void) => ?Promise, timeout?: number): void; 422 | /** Runs this function before every test inside this context */ 423 | declare function beforeEach(fn: (done: () => void) => ?Promise, timeout?: number): void; 424 | /** Runs this function after all tests have finished inside this context */ 425 | declare function afterAll(fn: (done: () => void) => ?Promise, timeout?: number): void; 426 | /** Runs this function before any tests have started inside this context */ 427 | declare function beforeAll(fn: (done: () => void) => ?Promise, timeout?: number): void; 428 | 429 | /** A context for grouping tests together */ 430 | declare var describe: { 431 | /** 432 | * Creates a block that groups together several related tests in one "test suite" 433 | */ 434 | (name: string, fn: () => void): void, 435 | 436 | /** 437 | * Only run this describe block 438 | */ 439 | only(name: string, fn: () => void): void, 440 | 441 | /** 442 | * Skip running this describe block 443 | */ 444 | skip(name: string, fn: () => void): void, 445 | }; 446 | 447 | 448 | /** An individual test unit */ 449 | declare var it: { 450 | /** 451 | * An individual test unit 452 | * 453 | * @param {string} Name of Test 454 | * @param {Function} Test 455 | * @param {number} Timeout for the test, in milliseconds. 456 | */ 457 | (name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, 458 | /** 459 | * Only run this test 460 | * 461 | * @param {string} Name of Test 462 | * @param {Function} Test 463 | * @param {number} Timeout for the test, in milliseconds. 464 | */ 465 | only(name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, 466 | /** 467 | * Skip running this test 468 | * 469 | * @param {string} Name of Test 470 | * @param {Function} Test 471 | * @param {number} Timeout for the test, in milliseconds. 472 | */ 473 | skip(name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, 474 | /** 475 | * Run the test concurrently 476 | * 477 | * @param {string} Name of Test 478 | * @param {Function} Test 479 | * @param {number} Timeout for the test, in milliseconds. 480 | */ 481 | concurrent(name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, 482 | }; 483 | declare function fit( 484 | name: string, 485 | fn: (done: () => void) => ?Promise, 486 | timeout?: number, 487 | ): void; 488 | /** An individual test unit */ 489 | declare var test: typeof it; 490 | /** A disabled group of tests */ 491 | declare var xdescribe: typeof describe; 492 | /** A focused group of tests */ 493 | declare var fdescribe: typeof describe; 494 | /** A disabled individual test */ 495 | declare var xit: typeof it; 496 | /** A disabled individual test */ 497 | declare var xtest: typeof it; 498 | 499 | /** The expect function is used every time you want to test a value */ 500 | declare var expect: { 501 | /** The object that you want to make assertions against */ 502 | (value: any): JestExpectType & JestPromiseType & EnzymeMatchersType, 503 | /** Add additional Jasmine matchers to Jest's roster */ 504 | extend(matchers: { [name: string]: JestMatcher }): void, 505 | /** Add a module that formats application-specific data structures. */ 506 | addSnapshotSerializer(serializer: (input: Object) => string): void, 507 | assertions(expectedAssertions: number): void, 508 | hasAssertions(): void, 509 | any(value: mixed): JestAsymmetricEqualityType, 510 | anything(): void, 511 | arrayContaining(value: Array): void, 512 | objectContaining(value: Object): void, 513 | /** Matches any received string that contains the exact expected string. */ 514 | stringContaining(value: string): void, 515 | stringMatching(value: string | RegExp): void 516 | }; 517 | 518 | // TODO handle return type 519 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 520 | declare function spyOn(value: mixed, method: string): Object; 521 | 522 | /** Holds all functions related to manipulating test runner */ 523 | declare var jest: JestObjectType; 524 | 525 | /** 526 | * The global Jamine object, this is generally not exposed as the public API, 527 | * using features inside here could break in later versions of Jest. 528 | */ 529 | declare var jasmine: { 530 | DEFAULT_TIMEOUT_INTERVAL: number, 531 | any(value: mixed): JestAsymmetricEqualityType, 532 | anything(): void, 533 | arrayContaining(value: Array): void, 534 | clock(): JestClockType, 535 | createSpy(name: string): JestSpyType, 536 | createSpyObj( 537 | baseName: string, 538 | methodNames: Array 539 | ): { [methodName: string]: JestSpyType }, 540 | objectContaining(value: Object): void, 541 | stringMatching(value: string): void 542 | }; 543 | -------------------------------------------------------------------------------- /flow-typed/npm/lodash_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 328ddf9f3d981b2b0f0ca4186bf96103 2 | // flow-typed version: 8daeabda84/lodash_v4.x.x/flow_>=v0.47.x 3 | 4 | declare module 'lodash' { 5 | declare type TemplateSettings = { 6 | escape?: RegExp, 7 | evaluate?: RegExp, 8 | imports?: Object, 9 | interpolate?: RegExp, 10 | variable?: string, 11 | }; 12 | 13 | declare type TruncateOptions = { 14 | length?: number, 15 | omission?: string, 16 | separator?: RegExp|string, 17 | }; 18 | 19 | declare type DebounceOptions = { 20 | leading?: bool, 21 | maxWait?: number, 22 | trailing?: bool, 23 | }; 24 | 25 | declare type ThrottleOptions = { 26 | leading?: bool, 27 | trailing?: bool, 28 | }; 29 | 30 | declare type NestedArray = Array>; 31 | 32 | declare type matchesIterateeShorthand = Object; 33 | declare type matchesPropertyIterateeShorthand = [string, any]; 34 | declare type propertyIterateeShorthand = string; 35 | 36 | declare type OPredicate = 37 | | ((value: A, key: string, object: O) => any) 38 | | matchesIterateeShorthand 39 | | matchesPropertyIterateeShorthand 40 | | propertyIterateeShorthand; 41 | 42 | declare type OIterateeWithResult = Object|string|((value: V, key: string, object: O) => R); 43 | declare type OIteratee = OIterateeWithResult; 44 | declare type OFlatMapIteratee = OIterateeWithResult>; 45 | 46 | declare type Predicate = 47 | | ((value: T, index: number, array: Array) => any) 48 | | matchesIterateeShorthand 49 | | matchesPropertyIterateeShorthand 50 | | propertyIterateeShorthand; 51 | 52 | declare type _ValueOnlyIteratee = (value: T) => mixed; 53 | declare type ValueOnlyIteratee = _ValueOnlyIteratee|string; 54 | declare type _Iteratee = (item: T, index: number, array: ?Array) => mixed; 55 | declare type Iteratee = _Iteratee|Object|string; 56 | declare type FlatMapIteratee = ((item: T, index: number, array: ?Array) => Array)|Object|string; 57 | declare type Comparator = (item: T, item2: T) => bool; 58 | 59 | declare type MapIterator = 60 | | ((item: T, index: number, array: Array) => U) 61 | | propertyIterateeShorthand; 62 | 63 | declare type OMapIterator = 64 | | ((item: T, key: string, object: O) => U) 65 | | propertyIterateeShorthand; 66 | 67 | declare class Lodash { 68 | // Array 69 | chunk(array: ?Array, size?: number): Array>; 70 | compact(array: Array): Array; 71 | concat(base: Array, ...elements: Array): Array; 72 | difference(array: ?Array, values?: Array): Array; 73 | differenceBy(array: ?Array, values: Array, iteratee: ValueOnlyIteratee): T[]; 74 | differenceWith(array: T[], values: T[], comparator?: Comparator): T[]; 75 | drop(array: ?Array, n?: number): Array; 76 | dropRight(array: ?Array, n?: number): Array; 77 | dropRightWhile(array: ?Array, predicate?: Predicate): Array; 78 | dropWhile(array: ?Array, predicate?: Predicate): Array; 79 | fill(array: ?Array, value: U, start?: number, end?: number): Array; 80 | findIndex(array: ?Array, predicate?: Predicate, fromIndex?: number): number; 81 | findLastIndex(array: ?Array, predicate?: Predicate, fromIndex?: number): number; 82 | // alias of _.head 83 | first(array: ?Array): T; 84 | flatten(array: Array|X>): Array; 85 | flattenDeep(array: any[]): Array; 86 | flattenDepth(array: any[], depth?: number): any[]; 87 | fromPairs(pairs: Array): Object; 88 | head(array: ?Array): T; 89 | indexOf(array: ?Array, value: T, fromIndex?: number): number; 90 | initial(array: ?Array): Array; 91 | intersection(...arrays: Array>): Array; 92 | //Workaround until (...parameter: T, parameter2: U) works 93 | intersectionBy(a1: Array, iteratee?: ValueOnlyIteratee): Array; 94 | intersectionBy(a1: Array, a2: Array, iteratee?: ValueOnlyIteratee): Array; 95 | intersectionBy(a1: Array, a2: Array, a3: Array, iteratee?: ValueOnlyIteratee): Array; 96 | intersectionBy(a1: Array, a2: Array, a3: Array, a4: Array, iteratee?: ValueOnlyIteratee): Array; 97 | //Workaround until (...parameter: T, parameter2: U) works 98 | intersectionWith(a1: Array, comparator: Comparator): Array; 99 | intersectionWith(a1: Array, a2: Array, comparator: Comparator): Array; 100 | intersectionWith(a1: Array, a2: Array, a3: Array, comparator: Comparator): Array; 101 | intersectionWith(a1: Array, a2: Array, a3: Array, a4: Array, comparator: Comparator): Array; 102 | join(array: ?Array, separator?: string): string; 103 | last(array: ?Array): T; 104 | lastIndexOf(array: ?Array, value: T, fromIndex?: number): number; 105 | nth(array: T[], n?: number): T; 106 | pull(array: ?Array, ...values?: Array): Array; 107 | pullAll(array: ?Array, values: Array): Array; 108 | pullAllBy(array: ?Array, values: Array, iteratee?: ValueOnlyIteratee): Array; 109 | pullAllWith(array?: T[], values: T[], comparator?: Function): T[]; 110 | pullAt(array: ?Array, ...indexed?: Array): Array; 111 | pullAt(array: ?Array, indexed?: Array): Array; 112 | remove(array: ?Array, predicate?: Predicate): Array; 113 | reverse(array: ?Array): Array; 114 | slice(array: ?Array, start?: number, end?: number): Array; 115 | sortedIndex(array: ?Array, value: T): number; 116 | sortedIndexBy(array: ?Array, value: T, iteratee?: ValueOnlyIteratee): number; 117 | sortedIndexOf(array: ?Array, value: T): number; 118 | sortedLastIndex(array: ?Array, value: T): number; 119 | sortedLastIndexBy(array: ?Array, value: T, iteratee?: ValueOnlyIteratee): number; 120 | sortedLastIndexOf(array: ?Array, value: T): number; 121 | sortedUniq(array: ?Array): Array; 122 | sortedUniqBy(array: ?Array, iteratee?: (value: T) => mixed): Array; 123 | tail(array: ?Array): Array; 124 | take(array: ?Array, n?: number): Array; 125 | takeRight(array: ?Array, n?: number): Array; 126 | takeRightWhile(array: ?Array, predicate?: Predicate): Array; 127 | takeWhile(array: ?Array, predicate?: Predicate): Array; 128 | union(...arrays?: Array>): Array; 129 | //Workaround until (...parameter: T, parameter2: U) works 130 | unionBy(a1: Array, iteratee?: ValueOnlyIteratee): Array; 131 | unionBy(a1: Array, a2: Array, iteratee?: ValueOnlyIteratee): Array; 132 | unionBy(a1: Array, a2: Array, a3: Array, iteratee?: ValueOnlyIteratee): Array; 133 | unionBy(a1: Array, a2: Array, a3: Array, a4: Array, iteratee?: ValueOnlyIteratee): Array; 134 | //Workaround until (...parameter: T, parameter2: U) works 135 | unionWith(a1: Array, comparator?: Comparator): Array; 136 | unionWith(a1: Array, a2: Array, comparator?: Comparator): Array; 137 | unionWith(a1: Array, a2: Array, a3: Array, comparator?: Comparator): Array; 138 | unionWith(a1: Array, a2: Array, a3: Array, a4: Array, comparator?: Comparator): Array; 139 | uniq(array: ?Array): Array; 140 | uniqBy(array: ?Array, iteratee?: ValueOnlyIteratee): Array; 141 | uniqWith(array: ?Array, comparator?: Comparator): Array; 142 | unzip(array: ?Array): Array; 143 | unzipWith(array: ?Array, iteratee?: Iteratee): Array; 144 | without(array: ?Array, ...values?: Array): Array; 145 | xor(...array: Array>): Array; 146 | //Workaround until (...parameter: T, parameter2: U) works 147 | xorBy(a1: Array, iteratee?: ValueOnlyIteratee): Array; 148 | xorBy(a1: Array, a2: Array, iteratee?: ValueOnlyIteratee): Array; 149 | xorBy(a1: Array, a2: Array, a3: Array, iteratee?: ValueOnlyIteratee): Array; 150 | xorBy(a1: Array, a2: Array, a3: Array, a4: Array, iteratee?: ValueOnlyIteratee): Array; 151 | //Workaround until (...parameter: T, parameter2: U) works 152 | xorWith(a1: Array, comparator?: Comparator): Array; 153 | xorWith(a1: Array, a2: Array, comparator?: Comparator): Array; 154 | xorWith(a1: Array, a2: Array, a3: Array, comparator?: Comparator): Array; 155 | xorWith(a1: Array, a2: Array, a3: Array, a4: Array, comparator?: Comparator): Array; 156 | zip(a1: A[], a2: B[]): Array<[A, B]>; 157 | zip(a1: A[], a2: B[], a3: C[]): Array<[A, B, C]>; 158 | zip(a1: A[], a2: B[], a3: C[], a4: D[]): Array<[A, B, C, D]>; 159 | zip(a1: A[], a2: B[], a3: C[], a4: D[], a5: E[]): Array<[A, B, C, D, E]>; 160 | 161 | zipObject(props?: Array, values?: Array): Object; 162 | zipObjectDeep(props?: any[], values?: any): Object; 163 | //Workaround until (...parameter: T, parameter2: U) works 164 | zipWith(a1: NestedArray, iteratee?: Iteratee): Array; 165 | zipWith(a1: NestedArray, a2: NestedArray, iteratee?: Iteratee): Array; 166 | zipWith(a1: NestedArray, a2: NestedArray, a3: NestedArray, iteratee?: Iteratee): Array; 167 | zipWith(a1: NestedArray, a2: NestedArray, a3: NestedArray, a4: NestedArray, iteratee?: Iteratee): Array; 168 | 169 | // Collection 170 | countBy(array: ?Array, iteratee?: ValueOnlyIteratee): Object; 171 | countBy(object: T, iteratee?: ValueOnlyIteratee): Object; 172 | // alias of _.forEach 173 | each(array: ?Array, iteratee?: Iteratee): Array; 174 | each(object: T, iteratee?: OIteratee): T; 175 | // alias of _.forEachRight 176 | eachRight(array: ?Array, iteratee?: Iteratee): Array; 177 | eachRight(object: T, iteratee?: OIteratee): T; 178 | every(array: ?Array, iteratee?: Iteratee): bool; 179 | every(object: T, iteratee?: OIteratee): bool; 180 | filter(array: ?Array, predicate?: Predicate): Array; 181 | filter(object: T, predicate?: OPredicate): Array; 182 | find(array: ?Array, predicate?: Predicate, fromIndex?: number): T|void; 183 | find(object: T, predicate?: OPredicate, fromIndex?: number): V; 184 | findLast(array: ?Array, predicate?: Predicate, fromIndex?: number): T|void; 185 | findLast(object: T, predicate?: OPredicate): V; 186 | flatMap(array: ?Array, iteratee?: FlatMapIteratee): Array; 187 | flatMap(object: T, iteratee?: OFlatMapIteratee): Array; 188 | flatMapDeep(array: ?Array, iteratee?: FlatMapIteratee): Array; 189 | flatMapDeep(object: T, iteratee?: OFlatMapIteratee): Array; 190 | flatMapDepth(array: ?Array, iteratee?: FlatMapIteratee, depth?: number): Array; 191 | flatMapDepth(object: T, iteratee?: OFlatMapIteratee, depth?: number): Array; 192 | forEach(array: ?Array, iteratee?: Iteratee): Array; 193 | forEach(object: T, iteratee?: OIteratee): T; 194 | forEachRight(array: ?Array, iteratee?: Iteratee): Array; 195 | forEachRight(object: T, iteratee?: OIteratee): T; 196 | groupBy(array: ?Array, iteratee?: ValueOnlyIteratee): {[key: V]: Array}; 197 | groupBy(object: T, iteratee?: ValueOnlyIteratee): {[key: V]: Array}; 198 | includes(array: ?Array, value: T, fromIndex?: number): bool; 199 | includes(object: T, value: any, fromIndex?: number): bool; 200 | includes(str: string, value: string, fromIndex?: number): bool; 201 | invokeMap(array: ?Array, path: ((value: T) => Array|string)|Array|string, ...args?: Array): Array; 202 | invokeMap(object: T, path: ((value: any) => Array|string)|Array|string, ...args?: Array): Array; 203 | keyBy(array: ?Array, iteratee?: ValueOnlyIteratee): {[key: V]: ?T}; 204 | keyBy(object: T, iteratee?: ValueOnlyIteratee): {[key: V]: ?A}; 205 | map(array: ?Array, iteratee?: MapIterator): Array; 206 | map(object: ?T, iteratee?: OMapIterator): Array; 207 | map(str: ?string, iteratee?: (char: string, index: number, str: string) => any): string; 208 | orderBy(array: ?Array, iteratees?: Array>|string, orders?: Array<'asc'|'desc'>|string): Array; 209 | orderBy(object: T, iteratees?: Array>|string, orders?: Array<'asc'|'desc'>|string): Array; 210 | partition(array: ?Array, predicate?: Predicate): NestedArray; 211 | partition(object: T, predicate?: OPredicate): NestedArray; 212 | reduce(array: ?Array, iteratee?: (accumulator: U, value: T, index: number, array: ?Array) => U, accumulator?: U): U; 213 | reduce(object: T, iteratee?: (accumulator: U, value: any, key: string, object: T) => U, accumulator?: U): U; 214 | reduceRight(array: ?Array, iteratee?: (accumulator: U, value: T, index: number, array: ?Array) => U, accumulator?: U): U; 215 | reduceRight(object: T, iteratee?: (accumulator: U, value: any, key: string, object: T) => U, accumulator?: U): U; 216 | reject(array: ?Array, predicate?: Predicate): Array; 217 | reject(object: T, predicate?: OPredicate): Array; 218 | sample(array: ?Array): T; 219 | sample(object: T): V; 220 | sampleSize(array: ?Array, n?: number): Array; 221 | sampleSize(object: T, n?: number): Array; 222 | shuffle(array: ?Array): Array; 223 | shuffle(object: T): Array; 224 | size(collection: Array|Object): number; 225 | some(array: ?Array, predicate?: Predicate): bool; 226 | some(object?: ?T, predicate?: OPredicate): bool; 227 | sortBy(array: ?Array, ...iteratees?: Array>): Array; 228 | sortBy(array: ?Array, iteratees?: Array>): Array; 229 | sortBy(object: T, ...iteratees?: Array>): Array; 230 | sortBy(object: T, iteratees?: Array>): Array; 231 | 232 | // Date 233 | now(): number; 234 | 235 | // Function 236 | after(n: number, fn: Function): Function; 237 | ary(func: Function, n?: number): Function; 238 | before(n: number, fn: Function): Function; 239 | bind(func: Function, thisArg: any, ...partials: Array): Function; 240 | bindKey(obj: Object, key: string, ...partials: Array): Function; 241 | curry(func: Function, arity?: number): Function; 242 | curryRight(func: Function, arity?: number): Function; 243 | debounce(func: Function, wait?: number, options?: DebounceOptions): Function; 244 | defer(func: Function, ...args?: Array): number; 245 | delay(func: Function, wait: number, ...args?: Array): number; 246 | flip(func: Function): Function; 247 | memoize(func: Function, resolver?: Function): Function; 248 | negate(predicate: Function): Function; 249 | once(func: Function): Function; 250 | overArgs(func: Function, ...transforms: Array): Function; 251 | overArgs(func: Function, transforms: Array): Function; 252 | partial(func: Function, ...partials: any[]): Function; 253 | partialRight(func: Function, ...partials: Array): Function; 254 | partialRight(func: Function, partials: Array): Function; 255 | rearg(func: Function, ...indexes: Array): Function; 256 | rearg(func: Function, indexes: Array): Function; 257 | rest(func: Function, start?: number): Function; 258 | spread(func: Function): Function; 259 | throttle(func: Function, wait?: number, options?: ThrottleOptions): Function; 260 | unary(func: Function): Function; 261 | wrap(value: any, wrapper: Function): Function; 262 | 263 | // Lang 264 | castArray(value: *): any[]; 265 | clone(value: T): T; 266 | cloneDeep(value: T): T; 267 | cloneDeepWith(value: T, customizer?: ?(value: T, key: number|string, object: T, stack: any) => U): U; 268 | cloneWith(value: T, customizer?: ?(value: T, key: number|string, object: T, stack: any) => U): U; 269 | conformsTo(source: T, predicates: T&{[key:string]:(x:any)=>boolean}): boolean; 270 | eq(value: any, other: any): bool; 271 | gt(value: any, other: any): bool; 272 | gte(value: any, other: any): bool; 273 | isArguments(value: any): bool; 274 | isArray(value: any): bool; 275 | isArrayBuffer(value: any): bool; 276 | isArrayLike(value: any): bool; 277 | isArrayLikeObject(value: any): bool; 278 | isBoolean(value: any): bool; 279 | isBuffer(value: any): bool; 280 | isDate(value: any): bool; 281 | isElement(value: any): bool; 282 | isEmpty(value: any): bool; 283 | isEqual(value: any, other: any): bool; 284 | isEqualWith(value: T, other: U, customizer?: (objValue: any, otherValue: any, key: number|string, object: T, other: U, stack: any) => bool|void): bool; 285 | isError(value: any): bool; 286 | isFinite(value: any): bool; 287 | isFunction(value: Function): true; 288 | isFunction(value: number|string|void|null|Object): false; 289 | isInteger(value: any): bool; 290 | isLength(value: any): bool; 291 | isMap(value: any): bool; 292 | isMatch(object?: ?Object, source: Object): bool; 293 | isMatchWith(object: T, source: U, customizer?: (objValue: any, srcValue: any, key: number|string, object: T, source: U) => bool|void): bool; 294 | isNaN(value: any): bool; 295 | isNative(value: any): bool; 296 | isNil(value: any): bool; 297 | isNull(value: any): bool; 298 | isNumber(value: any): bool; 299 | isObject(value: any): bool; 300 | isObjectLike(value: any): bool; 301 | isPlainObject(value: any): bool; 302 | isRegExp(value: any): bool; 303 | isSafeInteger(value: any): bool; 304 | isSet(value: any): bool; 305 | isString(value: string): true; 306 | isString(value: number|bool|Function|void|null|Object|Array): false; 307 | isSymbol(value: any): bool; 308 | isTypedArray(value: any): bool; 309 | isUndefined(value: any): bool; 310 | isWeakMap(value: any): bool; 311 | isWeakSet(value: any): bool; 312 | lt(value: any, other: any): bool; 313 | lte(value: any, other: any): bool; 314 | toArray(value: any): Array; 315 | toFinite(value: any): number; 316 | toInteger(value: any): number; 317 | toLength(value: any): number; 318 | toNumber(value: any): number; 319 | toPlainObject(value: any): Object; 320 | toSafeInteger(value: any): number; 321 | toString(value: any): string; 322 | 323 | // Math 324 | add(augend: number, addend: number): number; 325 | ceil(number: number, precision?: number): number; 326 | divide(dividend: number, divisor: number): number; 327 | floor(number: number, precision?: number): number; 328 | max(array: ?Array): T; 329 | maxBy(array: ?Array, iteratee?: Iteratee): T; 330 | mean(array: Array<*>): number; 331 | meanBy(array: Array, iteratee?: Iteratee): number; 332 | min(array: ?Array): T; 333 | minBy(array: ?Array, iteratee?: Iteratee): T; 334 | multiply(multiplier: number, multiplicand: number): number; 335 | round(number: number, precision?: number): number; 336 | subtract(minuend: number, subtrahend: number): number; 337 | sum(array: Array<*>): number; 338 | sumBy(array: Array, iteratee?: Iteratee): number; 339 | 340 | // number 341 | clamp(number: number, lower?: number, upper: number): number; 342 | inRange(number: number, start?: number, end: number): bool; 343 | random(lower?: number, upper?: number, floating?: bool): number; 344 | 345 | // Object 346 | assign(object?: ?Object, ...sources?: Array): Object; 347 | assignIn(a: A, b: B): A & B; 348 | assignIn(a: A, b: B, c: C): A & B & C; 349 | assignIn(a: A, b: B, c: C, d: D): A & B & C & D; 350 | assignIn(a: A, b: B, c: C, d: D, e: E): A & B & C & D & E; 351 | assignInWith(object: T, s1: A, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A) => any|void): Object; 352 | assignInWith(object: T, s1: A, s2: B, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B) => any|void): Object; 353 | assignInWith(object: T, s1: A, s2: B, s3: C, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C) => any|void): Object; 354 | assignInWith(object: T, s1: A, s2: B, s3: C, s4: D, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C|D) => any|void): Object; 355 | assignWith(object: T, s1: A, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A) => any|void): Object; 356 | assignWith(object: T, s1: A, s2: B, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B) => any|void): Object; 357 | assignWith(object: T, s1: A, s2: B, s3: C, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C) => any|void): Object; 358 | assignWith(object: T, s1: A, s2: B, s3: C, s4: D, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C|D) => any|void): Object; 359 | at(object?: ?Object, ...paths: Array): Array; 360 | at(object?: ?Object, paths: Array): Array; 361 | create(prototype: T, properties?: Object): $Supertype; 362 | defaults(object?: ?Object, ...sources?: Array): Object; 363 | defaultsDeep(object?: ?Object, ...sources?: Array): Object; 364 | // alias for _.toPairs 365 | entries(object?: ?Object): NestedArray; 366 | // alias for _.toPairsIn 367 | entriesIn(object?: ?Object): NestedArray; 368 | // alias for _.assignIn 369 | extend(a: A, b: B): A & B; 370 | extend(a: A, b: B, c: C): A & B & C; 371 | extend(a: A, b: B, c: C, d: D): A & B & C & D; 372 | extend(a: A, b: B, c: C, d: D, e: E): A & B & C & D & E; 373 | // alias for _.assignInWith 374 | extendWith(object: T, s1: A, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A) => any|void): Object; 375 | extendWith(object: T, s1: A, s2: B, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B) => any|void): Object; 376 | extendWith(object: T, s1: A, s2: B, s3: C, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C) => any|void): Object; 377 | extendWith(object: T, s1: A, s2: B, s3: C, s4: D, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C|D) => any|void): Object; 378 | findKey(object?: ?T, predicate?: OPredicate): string|void; 379 | findLastKey(object?: ?T, predicate?: OPredicate): string|void; 380 | forIn(object?: ?Object, iteratee?: OIteratee<*>): Object; 381 | forInRight(object?: ?Object, iteratee?: OIteratee<*>): Object; 382 | forOwn(object?: ?Object, iteratee?: OIteratee<*>): Object; 383 | forOwnRight(object?: ?Object, iteratee?: OIteratee<*>): Object; 384 | functions(object?: ?Object): Array; 385 | functionsIn(object?: ?Object): Array; 386 | get(object?: ?Object|?Array, path?: ?Array|string, defaultValue?: any): any; 387 | has(object?: ?Object, path?: ?Array|string): bool; 388 | hasIn(object?: ?Object, path?: ?Array|string): bool; 389 | invert(object?: ?Object, multiVal?: bool): Object; 390 | invertBy(object: ?Object, iteratee?: Function): Object; 391 | invoke(object?: ?Object, path?: ?Array|string, ...args?: Array): any; 392 | keys(object?: ?Object): Array; 393 | keysIn(object?: ?Object): Array; 394 | mapKeys(object?: ?Object, iteratee?: OIteratee<*>): Object; 395 | mapValues(object?: ?Object, iteratee?: OIteratee<*>): Object; 396 | merge(object?: ?Object, ...sources?: Array): Object; 397 | mergeWith(object: T, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A) => any|void): Object; 398 | mergeWith(object: T, s1: A, s2: B, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B) => any|void): Object; 399 | mergeWith(object: T, s1: A, s2: B, s3: C, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C) => any|void): Object; 400 | mergeWith(object: T, s1: A, s2: B, s3: C, s4: D, customizer?: (objValue: any, srcValue: any, key: string, object: T, source: A|B|C|D) => any|void): Object; 401 | omit(object?: ?Object, ...props: Array): Object; 402 | omit(object?: ?Object, props: Array): Object; 403 | omitBy(object?: ?T, predicate?: OPredicate): Object; 404 | pick(object?: ?Object, ...props: Array): Object; 405 | pick(object?: ?Object, props: Array): Object; 406 | pickBy(object?: ?T, predicate?: OPredicate): Object; 407 | result(object?: ?Object, path?: ?Array|string, defaultValue?: any): any; 408 | set(object?: ?Object, path?: ?Array|string, value: any): Object; 409 | setWith(object: T, path?: ?Array|string, value: any, customizer?: (nsValue: any, key: string, nsObject: T) => any): Object; 410 | toPairs(object?: ?Object|Array<*>): NestedArray; 411 | toPairsIn(object?: ?Object): NestedArray; 412 | transform(collection: Object|Array, iteratee?: OIteratee<*>, accumulator?: any): any; 413 | unset(object?: ?Object, path?: ?Array|string): bool; 414 | update(object: Object, path: string[]|string, updater: Function): Object; 415 | updateWith(object: Object, path: string[]|string, updater: Function, customizer?: Function): Object; 416 | values(object?: ?Object): Array; 417 | valuesIn(object?: ?Object): Array; 418 | 419 | // Seq 420 | // harder to read, but this is _() 421 | (value: any): any; 422 | chain(value: T): any; 423 | tap(value: T, interceptor: (value:T)=>any): T; 424 | thru(value: T1, interceptor: (value:T1)=>T2): T2; 425 | // TODO: _.prototype.* 426 | 427 | // String 428 | camelCase(string?: ?string): string; 429 | capitalize(string?: string): string; 430 | deburr(string?: string): string; 431 | endsWith(string?: string, target?: string, position?: number): bool; 432 | escape(string?: string): string; 433 | escapeRegExp(string?: string): string; 434 | kebabCase(string?: string): string; 435 | lowerCase(string?: string): string; 436 | lowerFirst(string?: string): string; 437 | pad(string?: string, length?: number, chars?: string): string; 438 | padEnd(string?: string, length?: number, chars?: string): string; 439 | padStart(string?: string, length?: number, chars?: string): string; 440 | parseInt(string: string, radix?: number): number; 441 | repeat(string?: string, n?: number): string; 442 | replace(string?: string, pattern: RegExp|string, replacement: ((string: string) => string)|string): string; 443 | snakeCase(string?: string): string; 444 | split(string?: string, separator: RegExp|string, limit?: number): Array; 445 | startCase(string?: string): string; 446 | startsWith(string?: string, target?: string, position?: number): bool; 447 | template(string?: string, options?: TemplateSettings): Function; 448 | toLower(string?: string): string; 449 | toUpper(string?: string): string; 450 | trim(string?: string, chars?: string): string; 451 | trimEnd(string?: string, chars?: string): string; 452 | trimStart(string?: string, chars?: string): string; 453 | truncate(string?: string, options?: TruncateOptions): string; 454 | unescape(string?: string): string; 455 | upperCase(string?: string): string; 456 | upperFirst(string?: string): string; 457 | words(string?: string, pattern?: RegExp|string): Array; 458 | 459 | // Util 460 | attempt(func: Function, ...args: Array): any; 461 | bindAll(object?: ?Object, methodNames: Array): Object; 462 | bindAll(object?: ?Object, ...methodNames: Array): Object; 463 | cond(pairs: NestedArray): Function; 464 | conforms(source: Object): Function; 465 | constant(value: T): () => T; 466 | defaultTo(value: T1, default: T2): T1; 467 | // NaN is a number instead of its own type, otherwise it would behave like null/void 468 | defaultTo(value: T1, default: T2): T1|T2; 469 | defaultTo(value: T1, default: T2): T2; 470 | flow(...funcs?: Array): Function; 471 | flow(funcs?: Array): Function; 472 | flowRight(...funcs?: Array): Function; 473 | flowRight(funcs?: Array): Function; 474 | identity(value: T): T; 475 | iteratee(func?: any): Function; 476 | matches(source: Object): Function; 477 | matchesProperty(path?: ?Array|string, srcValue: any): Function; 478 | method(path?: ?Array|string, ...args?: Array): Function; 479 | methodOf(object?: ?Object, ...args?: Array): Function; 480 | mixin(object?: T, source: Object, options?: { chain: bool }): T; 481 | noConflict(): Lodash; 482 | noop(...args: Array): void; 483 | nthArg(n?: number): Function; 484 | over(...iteratees: Array): Function; 485 | over(iteratees: Array): Function; 486 | overEvery(...predicates: Array): Function; 487 | overEvery(predicates: Array): Function; 488 | overSome(...predicates: Array): Function; 489 | overSome(predicates: Array): Function; 490 | property(path?: ?Array|string): Function; 491 | propertyOf(object?: ?Object): Function; 492 | range(start: number, end: number, step?: number): Array; 493 | range(end: number, step?: number): Array; 494 | rangeRight(start: number, end: number, step?: number): Array; 495 | rangeRight(end: number, step?: number): Array; 496 | runInContext(context?: Object): Function; 497 | 498 | stubArray(): Array<*>; 499 | stubFalse(): false; 500 | stubObject(): {}; 501 | stubString(): ''; 502 | stubTrue(): true; 503 | times(n: number, ...rest: Array): Array; 504 | times(n: number, iteratee: ((i: number) => T)): Array; 505 | toPath(value: any): Array; 506 | uniqueId(prefix?: string): string; 507 | 508 | // Properties 509 | VERSION: string; 510 | templateSettings: TemplateSettings; 511 | } 512 | 513 | declare var exports: Lodash; 514 | } 515 | -------------------------------------------------------------------------------- /flow-typed/npm/morgan_v1.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f3639eb89945dc27e7a7ab35964fa591 2 | // flow-typed version: b43dff3e0e/morgan_v1.x.x/flow_>=v0.20.x 3 | 4 | /* @flow */ 5 | import type { Middleware, $Request, $Response } from 'express'; 6 | 7 | declare module "morgan" { 8 | 9 | declare type FormatFn = (tokens: TokenIndexer, req: $Request, res: $Response) => string; 10 | 11 | declare type TokenCallbackFn = (req: $Request, res: $Response, arg?: string | number | boolean) => string; 12 | 13 | declare interface TokenIndexer { 14 | [tokenName: string]: TokenCallbackFn; 15 | } 16 | 17 | /** 18 | * Public interface of morgan logger 19 | */ 20 | declare interface Morgan { 21 | /*** 22 | * Create a new morgan logger middleware function using the given format and options. The format argument may be a string of a predefined name (see below for the names), 23 | * or a string of a format string containing defined tokens. 24 | * @param format 25 | * @param options 26 | */ 27 | (format: string, options?: Options): Middleware; 28 | /*** 29 | * Standard Apache combined log output. 30 | * :remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" 31 | * @param format 32 | * @param options 33 | */ 34 | (format: 'combined', options?: Options): Middleware; 35 | /*** 36 | * Standard Apache common log output. 37 | * :remote-addr - :remote-user [:date] ":method :url HTTP/:http-version" :status :res[content-length] 38 | * @param format 39 | * @param options 40 | */ 41 | (format: 'common', options?: Options): Middleware; 42 | /** 43 | * Concise output colored by response status for development use. The :status token will be colored red for server error codes, yellow for client error codes, cyan for redirection codes, and uncolored for all other codes. 44 | * :method :url :status :response-time ms - :res[content-length] 45 | * @param format 46 | * @param options 47 | */ 48 | (format: 'dev', options?: Options): Middleware; 49 | 50 | /*** 51 | * Shorter than default, also including response time. 52 | * :remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms 53 | * @param format 54 | * @param options 55 | */ 56 | (format: 'short', options?: Options): Middleware; 57 | 58 | /*** 59 | * The minimal output. 60 | * :method :url :status :res[content-length] - :response-time ms 61 | * @param format 62 | * @param options 63 | */ 64 | (format: 'tiny', options?: Options): Middleware; 65 | 66 | /*** 67 | * Create a new morgan logger middleware function using the given format and options. The format argument may be a 68 | * custom format function which adheres to the signature. 69 | * @param format 70 | * @param options 71 | */ 72 | (format: FormatFn, options?: Options): Middleware; 73 | 74 | /** 75 | * Define a custom token which can be used in custom morgan logging formats. 76 | */ 77 | token(name: string, callback: TokenCallbackFn): Morgan; 78 | /** 79 | * Define a named custom format by specifying a format string in token notation 80 | */ 81 | format(name: string, fmt: string): Morgan; 82 | 83 | /** 84 | * Define a named custom format by specifying a format function 85 | */ 86 | format(name: string, fmt: FormatFn): Morgan; 87 | 88 | /** 89 | * Compile a format string in token notation into a format function 90 | */ 91 | compile(format: string): FormatFn; 92 | } 93 | 94 | /** 95 | * Define a custom token which can be used in custom morgan logging formats. 96 | */ 97 | declare function token(name: string, callback: TokenCallbackFn): Morgan; 98 | 99 | /** 100 | * Define a named custom format by specifying a format string in token notation 101 | */ 102 | declare function format(name: string, fmt: string): Morgan; 103 | 104 | /** 105 | * Define a named custom format by specifying a format function 106 | */ 107 | declare function format(name: string, fmt: FormatFn): Morgan; 108 | 109 | /** 110 | * Compile a format string in token notation into a format function 111 | */ 112 | declare function compile(format: string): FormatFn; 113 | 114 | declare interface StreamOptions { 115 | /** 116 | * Output stream for writing log lines 117 | */ 118 | write: (str: string) => void; 119 | } 120 | 121 | /*** 122 | * Morgan accepts these properties in the options object. 123 | */ 124 | declare interface Options { 125 | 126 | /*** 127 | * Buffer duration before writing logs to the stream, defaults to false. When set to true, defaults to 1000 ms. 128 | */ 129 | buffer?: boolean; 130 | 131 | /*** 132 | * Write log line on request instead of response. This means that a requests will be logged even if the server crashes, but data from the response cannot be logged (like the response code). 133 | */ 134 | immediate?: boolean; 135 | 136 | /*** 137 | * Function to determine if logging is skipped, defaults to false. This function will be called as skip(req, res). 138 | */ 139 | skip?: (req: $Request, res: $Response) => boolean; 140 | 141 | /*** 142 | * Output stream for writing log lines, defaults to process.stdout. 143 | * @param str 144 | */ 145 | stream?: StreamOptions; 146 | } 147 | 148 | declare var exports: Morgan 149 | } 150 | -------------------------------------------------------------------------------- /flow-typed/npm/pg_v6.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 79786137c50ff431cd8c9a940b0877c4 2 | // flow-typed version: b43dff3e0e/pg_v6.x.x/flow_>=v0.28.x 3 | 4 | declare module pg { 5 | // Note: Currently There are some issues in Function overloading. 6 | // https://github.com/facebook/flow/issues/2423 7 | // So i temporarily remove the 8 | // `((event: string, listener: Function) => EventEmitter );` 9 | // from all overloading for EventEmitter.on(). 10 | 11 | // `any` types exised in this file, cause of currently `mixed` did not work well 12 | // in Function Overloading. 13 | 14 | // `Function` types exised in this file, cause of they come from another 15 | // untyped npm lib. 16 | 17 | /* Cause of > 31 | /* 32 | * PgPoolConfig's properties are passed unchanged to both 33 | * the node-postgres Client constructor and the node-pool constructor 34 | * allowing you to fully configure the behavior of both 35 | * node-pool (https://github.com/coopernurse/node-pool) 36 | */ 37 | declare type PgPoolConfig = { 38 | // node-pool ---------------- 39 | name: string, 40 | create: Function, 41 | destroy: Function, 42 | max: number, 43 | min: number, 44 | refreshIdle: boolean, 45 | idleTimeoutMillis: number, 46 | reapIntervalMillis: number, 47 | returnToHead: boolean, 48 | priorityRange: number, 49 | validate: Function, 50 | validateAsync: Function, 51 | log: Function, 52 | 53 | // node-postgres Client ------ 54 | //database user's name 55 | user: string, 56 | //name of database to connect 57 | database: string, 58 | //database user's password 59 | password: string, 60 | //database port 61 | port: number, 62 | // database host. defaults to localhost 63 | host?: string, 64 | // whether to try SSL/TLS to connect to server. default value: false 65 | ssl?: boolean, 66 | // name displayed in the pg_stat_activity view and included in CSV log entries 67 | // default value: process.env.PGAPPNAME 68 | application_name?: string, 69 | // fallback value for the application_name configuration parameter 70 | // default value: false 71 | fallback_application_name?: string, 72 | 73 | // pg-pool 74 | Client: mixed, 75 | Promise: mixed, 76 | onCreate: Function, 77 | }; 78 | 79 | /* 80 | * Not extends from Client, cause some of Client's functions(ex: connect and end) 81 | * should not be used by PoolClient (which returned from Pool.connect). 82 | */ 83 | declare type PoolClient = { 84 | release(error?: mixed): void, 85 | 86 | query: 87 | ( (query: QueryConfig|string, callback?: QueryCallback) => Query ) & 88 | ( (text: string, values: Array, callback?: QueryCallback) => Query ), 89 | 90 | on: 91 | ((event: 'drain', listener: () => void) => events$EventEmitter )& 92 | ((event: 'error', listener: (err: PG_ERROR) => void) => events$EventEmitter )& 93 | ((event: 'notification', listener: (message: any) => void) => events$EventEmitter )& 94 | ((event: 'notice', listener: (message: any) => void) => events$EventEmitter )& 95 | ((event: 'end', listener: () => void) => events$EventEmitter ), 96 | } 97 | 98 | declare type PoolConnectCallback = (error: PG_ERROR|null, 99 | client: PoolClient|null, done: DoneCallback) => void; 100 | declare type DoneCallback = (error?: mixed) => void; 101 | // https://github.com/facebook/flow/blob/master/lib/node.js#L581 102 | // on() returns a events$EventEmitter 103 | declare class Pool extends events$EventEmitter { 104 | constructor(options: $Shape, Client?: Class): void; 105 | connect(cb?: PoolConnectCallback): Promise; 106 | take(cb?: PoolConnectCallback): Promise; 107 | end(cb?: DoneCallback): Promise; 108 | 109 | // Note: not like the pg's Client, the Pool.query return a Promise, 110 | // not a Thenable Query which Client returned. 111 | // And there is a flow(<0.34) issue here, when Array, 112 | // the overloading will not work 113 | query: 114 | ( (query: QueryConfig|string, callback?: QueryCallback) => Promise ) & 115 | ( (text: string, values: Array, callback?: QueryCallback) => Promise); 116 | 117 | /* flow issue: https://github.com/facebook/flow/issues/2423 118 | * When this fixed, this overloading can be used. 119 | */ 120 | /* 121 | on: 122 | ((event: 'connect', listener: (client: PoolClient) => void) => events$EventEmitter )& 123 | ((event: 'acquire', listener: (client: PoolClient) => void) => events$EventEmitter )& 124 | ((event: "error", listener: (err: PG_ERROR) => void) => events$EventEmitter )& 125 | ((event: string, listener: Function) => events$EventEmitter); 126 | */ 127 | } 128 | 129 | // <<------------- copy from 'pg-pool' ------------------------------ 130 | 131 | 132 | // error 133 | declare type PG_ERROR = { 134 | name: string, 135 | length: number, 136 | severity: string, 137 | code: string, 138 | detail: string|void, 139 | hint: string|void, 140 | position: string|void, 141 | internalPosition: string|void, 142 | internalQuery: string|void, 143 | where: string|void, 144 | schema: string|void, 145 | table: string|void, 146 | column: string|void, 147 | dataType: string|void, 148 | constraint: string|void, 149 | file: string|void, 150 | line: string|void, 151 | routine: string|void 152 | }; 153 | 154 | declare type ClientConfig = { 155 | //database user's name 156 | user: string, 157 | //name of database to connect 158 | database: string, 159 | //database user's password 160 | password: string, 161 | //database port 162 | port: number, 163 | // database host. defaults to localhost 164 | host: string, 165 | // whether to try SSL/TLS to connect to server. default value: false 166 | ssl: boolean, 167 | // name displayed in the pg_stat_activity view and included in CSV log entries 168 | // default value: process.env.PGAPPNAME 169 | application_name: string, 170 | // fallback value for the application_name configuration parameter 171 | // default value: false 172 | fallback_application_name: string, 173 | } 174 | 175 | declare type Row = { 176 | [key: string]: mixed, 177 | }; 178 | declare type ResultSet = { 179 | command: string, 180 | rowCount: number, 181 | oid: number, 182 | rows: Array, 183 | }; 184 | declare type ResultBuilder = { 185 | command: string, 186 | rowCount: number, 187 | oid: number, 188 | rows: Array, 189 | addRow: (row: Row) => void, 190 | }; 191 | declare type QueryConfig = { 192 | name?: string, 193 | text: string, 194 | values?: any[], 195 | }; 196 | 197 | declare type QueryCallback = (err: PG_ERROR|null, result: ResultSet|void) => void; 198 | declare type ClientConnectCallback = (err: PG_ERROR|null, client: Client|void) => void; 199 | 200 | /* 201 | * lib/query.js 202 | * Query extends from EventEmitter in source code. 203 | * but in Flow there is no multiple extends. 204 | * And in Flow await is a `declare function $await(p: Promise | T): T;` 205 | * seems can not resolve a Thenable's value type directly 206 | * so `Query extends Promise` to make thing temporarily work. 207 | * like this: 208 | * const q = client.query('select * from some'); 209 | * q.on('row',cb); // Event 210 | * const result = await q; // or await 211 | * 212 | * ToDo: should find a better way. 213 | */ 214 | declare class Query extends Promise { 215 | then( onFulfill?: (value: ResultSet) => Promise | U, 216 | onReject?: (error: PG_ERROR) => Promise | U 217 | ): Promise; 218 | // Because then and catch return a Promise, 219 | // .then.catch will lose catch's type information PG_ERROR. 220 | catch( onReject?: (error: PG_ERROR) => ?Promise | U ): Promise; 221 | 222 | on : 223 | ((event: 'row', listener: (row: Row, result: ResultBuilder) => void) => events$EventEmitter )& 224 | ((event: 'end', listener: (result: ResultBuilder) => void) => events$EventEmitter )& 225 | ((event: 'error', listener: (err: PG_ERROR) => void) => events$EventEmitter ); 226 | } 227 | 228 | /* 229 | * lib/client.js 230 | * Note: not extends from EventEmitter, for This Type returned by on(). 231 | * Flow's EventEmitter force return a EventEmitter in on(). 232 | * ToDo: Not sure in on() if return events$EventEmitter or this will be more suitable 233 | * return this will restrict event to given literial when chain on().on().on(). 234 | * return a events$EventEmitter will fallback to raw EventEmitter, when chains 235 | */ 236 | declare class Client { 237 | constructor(config?: string | ClientConfig): void; 238 | connect(callback?: ClientConnectCallback):void; 239 | end(): void; 240 | 241 | query: 242 | ( (query: QueryConfig|string, callback?: QueryCallback) => Query ) & 243 | ( (text: string, values: Array, callback?: QueryCallback) => Query ); 244 | 245 | on: 246 | ((event: 'drain', listener: () => void) => this )& 247 | ((event: 'error', listener: (err: PG_ERROR) => void) => this )& 248 | ((event: 'notification', listener: (message: any) => void) => this )& 249 | ((event: 'notice', listener: (message: any) => void) => this )& 250 | ((event: 'end', listener: () => void) => this ); 251 | } 252 | 253 | /* 254 | * require('pg-types') 255 | */ 256 | declare type TypeParserText = (value: string) => any; 257 | declare type TypeParserBinary = (value: Buffer) => any; 258 | declare type Types = { 259 | getTypeParser: 260 | ((oid: number, format?: 'text') => TypeParserText )& 261 | ((oid: number, format: 'binary') => TypeParserBinary ); 262 | 263 | setTypeParser: 264 | ((oid: number, format?: 'text', parseFn: TypeParserText) => void )& 265 | ((oid: number, format: 'binary', parseFn: TypeParserBinary) => void)& 266 | ((oid: number, parseFn: TypeParserText) => void), 267 | } 268 | 269 | /* 270 | * lib/index.js ( class PG) 271 | */ 272 | declare class PG extends events$EventEmitter { 273 | types: Types; 274 | Client: Class; 275 | Pool: Class; 276 | Connection: mixed; //Connection is used internally by the Client. 277 | constructor(client: Client): void; 278 | native: { // native binding, have the same capability like PG 279 | types: Types; 280 | Client: Class; 281 | Pool: Class; 282 | Connection: mixed; 283 | }; 284 | // The end(),connect(),cancel() in PG is abandoned ? 285 | } 286 | 287 | // These class are not exposed by pg. 288 | declare type PoolType = Pool; 289 | declare type PGType = PG; 290 | declare type QueryType = Query; 291 | // module export, keep same structure with index.js 292 | declare module.exports: PG; 293 | } 294 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "watch": ["server", "webpack.config.js", ".env"], 4 | "exec": "npm run build:dev && node compiled/server.dev.js" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-shopify-app", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "npm run clean && cross-env NODE_ENV=production npm run webpack", 7 | "build:dev": "cross-env NODE_ENV=development npm run webpack", 8 | "clean": "rimraf compiled", 9 | "flow": "flow", 10 | "heroku-postbuild": "npm run build && cd react-ui/ && npm install && npm install --only=dev --no-shrinkwrap && npm run build", 11 | "lint": "node node_modules/eslint/bin/eslint .", 12 | "prettier": "node_modules/.bin/prettier --single-quote --write \"src/**/*.{js,jsx}\"", 13 | "sequelize": "node_modules/.bin/sequelize", 14 | "server:dev": "cross-env NODE_ENV=development nodemon", 15 | "start:dev": "concurrently \"npm run server:dev\" \"cd react-ui/ && npm start\"", 16 | "start": "cross-env NODE_ENV=production node compiled/server.js", 17 | "test:client": "cd react-ui/ && npm test", 18 | "test:server": "jest --watch", 19 | "test": "cross-env CI=true npm run test:client && jest --forceExit", 20 | "webpack": "webpack --colors --display-error-details --config ./webpack.config.js" 21 | }, 22 | "cacheDirectories": [ 23 | "node_modules", 24 | "react-ui/node_modules" 25 | ], 26 | "dependencies": { 27 | "babel-cli": "^6.9.0", 28 | "babel-core": "^6.9.0", 29 | "babel-loader": "^7.1.1", 30 | "babel-preset-es2015": "^6.9.0", 31 | "babel-preset-stage-0": "^6.5.0", 32 | "bluebird": "^3.5.0", 33 | "body-parser": "~1.17.1", 34 | "compression": "^1.7.0", 35 | "connect-pg-simple": "^4.2.0", 36 | "connect-redis": "^3.3.0", 37 | "cookie-parser": "~1.4.3", 38 | "cross-env": "^5.0.1", 39 | "debug": "~2.6.3", 40 | "dotenv": "^4.0.0", 41 | "express": "~4.15.2", 42 | "express-flash": "0.0.2", 43 | "express-session": "^1.15.3", 44 | "helmet": "^3.6.1", 45 | "lodash": "^4.17.4", 46 | "method-override": "^2.3.9", 47 | "morgan": "~1.8.1", 48 | "pg": "^6.4.1", 49 | "pg-hstore": "^2.3.2", 50 | "pug": "^2.0.0-rc.4", 51 | "rimraf": "^2.6.1", 52 | "sequelize": "^4.3.2", 53 | "sequelize-cli": "^3.0.0-2", 54 | "serialize-javascript": "^1.4.0", 55 | "serve-favicon": "~2.4.2", 56 | "shopify-api-node": "^2.7.0", 57 | "shopify-token": "^3.0.1", 58 | "webpack": "^3.3.0", 59 | "winston": "^2.3.1" 60 | }, 61 | "devDependencies": { 62 | "babel-eslint": "^7.2.1", 63 | "babel-jest": "^20.0.3", 64 | "babel-register": "^6.24.0", 65 | "cheerio": "^0.22.0", 66 | "concurrently": "^3.5.0", 67 | "eslint": "^4.6.1", 68 | "eslint-config-airbnb": "^15.1.0", 69 | "eslint-plugin-flowtype": "^2.35.1", 70 | "eslint-plugin-flowtype-errors": "^3.3.1", 71 | "eslint-plugin-import": "^2.7.0", 72 | "eslint-plugin-jest": "^21.0.2", 73 | "eslint-plugin-jsx-a11y": "^5.1.1", 74 | "eslint-plugin-react": "^7.3.0", 75 | "flow": "^0.2.3", 76 | "flow-bin": "^0.53.1", 77 | "jest": "^20.0.4", 78 | "jsdom": "^9.12.0", 79 | "nodemon": "1.11.0", 80 | "prettier": "^1.5.3", 81 | "redis": "^2.7.1", 82 | "supertest": "^3.0.0", 83 | "yargs": "^8.0.1" 84 | }, 85 | "jest": { 86 | "coverageDirectory": "./coverage", 87 | "testPathIgnorePatterns": [ 88 | "/node_modules/", 89 | "react-ui" 90 | ] 91 | }, 92 | "keywords": [ 93 | "Shopify", 94 | "API", 95 | "Node", 96 | "React", 97 | "Express", 98 | "Polaris" 99 | ], 100 | "author": "Mihovil Kovačević ", 101 | "license": "MIT" 102 | } 103 | -------------------------------------------------------------------------------- /react-ui/.env.development: -------------------------------------------------------------------------------- 1 | DANGEROUSLY_DISABLE_HOST_CHECK=true -------------------------------------------------------------------------------- /react-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/@shopify/polaris_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: bfb4285194d53619c3dcc622d1d50c8e 2 | // flow-typed version: <>/@shopify/polaris_v^1.1.1/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * '@shopify/polaris' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module '@shopify/polaris' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module '@shopify/polaris/embedded' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module '@shopify/polaris/index.es' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module '@shopify/polaris/embedded.js' { 35 | declare module.exports: $Exports<'@shopify/polaris/embedded'>; 36 | } 37 | declare module '@shopify/polaris/index.es.js' { 38 | declare module.exports: $Exports<'@shopify/polaris/index.es'>; 39 | } 40 | declare module '@shopify/polaris/index' { 41 | declare module.exports: $Exports<'@shopify/polaris'>; 42 | } 43 | declare module '@shopify/polaris/index.js' { 44 | declare module.exports: $Exports<'@shopify/polaris'>; 45 | } 46 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/axios_v0.16.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: fe9c24a017795779e7874809ca910ad6 2 | // flow-typed version: b43dff3e0e/axios_v0.16.x/flow_>=v0.25.x 3 | 4 | declare module 'axios' { 5 | declare interface ProxyConfig { 6 | host: string; 7 | port: number; 8 | } 9 | declare interface Cancel { 10 | constructor(message?: string): Cancel; 11 | message: string; 12 | } 13 | declare interface Canceler { 14 | (message?: string): void; 15 | } 16 | declare interface CancelTokenSource { 17 | token: CancelToken; 18 | cancel: Canceler; 19 | } 20 | declare interface CancelToken { 21 | constructor(executor: (cancel: Canceler) => void): CancelToken; 22 | static source(): CancelTokenSource; 23 | promise: Promise; 24 | reason?: Cancel; 25 | throwIfRequested(): void; 26 | } 27 | declare interface AxiosXHRConfigBase { 28 | adapter?: (config: AxiosXHRConfig) => Promise>; 29 | auth?: { 30 | username: string, 31 | password: string 32 | }; 33 | baseURL?: string, 34 | cancelToken?: CancelToken; 35 | headers?: Object; 36 | httpAgent?: mixed; // Missing the type in the core flow node libdef 37 | httpsAgent?: mixed; // Missing the type in the core flow node libdef 38 | maxContentLength?: number; 39 | maxRedirects?: 5, 40 | params?: Object; 41 | paramsSerializer?: (params: Object) => string; 42 | progress?: (progressEvent: Event) => void | mixed; 43 | proxy?: ProxyConfig; 44 | responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'; 45 | timeout?: number; 46 | transformRequest?: Array<(data: T) => U|Array<(data: T) => U>>; 47 | transformResponse?: Array<(data: T) => U>; 48 | validateStatus?: (status: number) => boolean, 49 | withCredentials?: boolean; 50 | xsrfCookieName?: string; 51 | xsrfHeaderName?: string; 52 | } 53 | declare type $AxiosXHRConfigBase = AxiosXHRConfigBase; 54 | declare interface AxiosXHRConfig extends AxiosXHRConfigBase { 55 | data?: T; 56 | method?: string; 57 | url: string; 58 | } 59 | declare type $AxiosXHRConfig = AxiosXHRConfig; 60 | declare class AxiosXHR { 61 | config: AxiosXHRConfig; 62 | data: T; 63 | headers: Object; 64 | status: number; 65 | statusText: string, 66 | request: http$ClientRequest | XMLHttpRequest 67 | } 68 | declare type $AxiosXHR = $AxiosXHR; 69 | declare class AxiosInterceptorIdent extends String {} 70 | declare class AxiosRequestInterceptor { 71 | use( 72 | successHandler: ?(response: AxiosXHRConfig) => Promise> | AxiosXHRConfig<*>, 73 | errorHandler: ?(error: mixed) => mixed, 74 | ): AxiosInterceptorIdent; 75 | eject(ident: AxiosInterceptorIdent): void; 76 | } 77 | declare class AxiosResponseInterceptor { 78 | use( 79 | successHandler: ?(response: AxiosXHR) => mixed, 80 | errorHandler: ?(error: mixed) => mixed, 81 | ): AxiosInterceptorIdent; 82 | eject(ident: AxiosInterceptorIdent): void; 83 | } 84 | declare type AxiosPromise = Promise>; 85 | declare class Axios { 86 | constructor(config?: AxiosXHRConfigBase): void; 87 | $call: (config: AxiosXHRConfig | string, config?: AxiosXHRConfig) => AxiosPromise; 88 | request(config: AxiosXHRConfig): AxiosPromise; 89 | delete(url: string, config?: AxiosXHRConfigBase): AxiosPromise; 90 | get(url: string, config?: AxiosXHRConfigBase): AxiosPromise; 91 | head(url: string, config?: AxiosXHRConfigBase): AxiosPromise; 92 | post(url: string, data?: mixed, config?: AxiosXHRConfigBase): AxiosPromise; 93 | put(url: string, data?: mixed, config?: AxiosXHRConfigBase): AxiosPromise; 94 | patch(url: string, data?: mixed, config?: AxiosXHRConfigBase): AxiosPromise; 95 | interceptors: { 96 | request: AxiosRequestInterceptor, 97 | response: AxiosResponseInterceptor, 98 | }; 99 | defaults: AxiosXHRConfig<*> & { headers: Object }; 100 | } 101 | 102 | declare class AxiosError extends Error { 103 | config: AxiosXHRConfig; 104 | response: AxiosXHR; 105 | code?: string; 106 | } 107 | 108 | declare type $AxiosError = AxiosError; 109 | 110 | declare interface AxiosExport extends Axios { 111 | Axios: typeof Axios; 112 | Cancel: Class; 113 | CancelToken: Class; 114 | isCancel(value: any): boolean; 115 | create(config?: AxiosXHRConfigBase): Axios; 116 | all: typeof Promise.all; 117 | spread(callback: Function): (arr: Array) => Function 118 | } 119 | declare module.exports: AxiosExport; 120 | } 121 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/enzyme_v2.3.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 9928203302e33de509770774cda1e531 2 | // flow-typed version: 60d0f06df5/enzyme_v2.3.x/flow_>=v0.28.x 3 | 4 | declare module 'enzyme' { 5 | declare type PredicateFunction = ( 6 | wrapper: T, 7 | index: number 8 | ) => boolean; 9 | declare type NodeOrNodes = React$Element | Array>; 10 | declare type EnzymeSelector = string | React$Element<*> | Object; 11 | 12 | // CheerioWrapper is a type alias for an actual cheerio instance 13 | // TODO: Reference correct type from cheerio's type declarations 14 | declare type CheerioWrapper = any; 15 | 16 | declare class Wrapper { 17 | find(selector: EnzymeSelector): this, 18 | findWhere(predicate: PredicateFunction): this, 19 | filter(selector: EnzymeSelector): this, 20 | filterWhere(predicate: PredicateFunction): this, 21 | contains(nodeOrNodes: NodeOrNodes): boolean, 22 | containsMatchingElement(node: React$Element): boolean, 23 | containsAllMatchingElements(nodes: NodeOrNodes): boolean, 24 | containsAnyMatchingElements(nodes: NodeOrNodes): boolean, 25 | dive(option?: { context?: Object }): this, 26 | exists(): boolean, 27 | matchesElement(node: React$Element): boolean, 28 | hasClass(className: string): boolean, 29 | is(selector: EnzymeSelector): boolean, 30 | isEmpty(): boolean, 31 | not(selector: EnzymeSelector): this, 32 | children(selector?: EnzymeSelector): this, 33 | childAt(index: number): this, 34 | parents(selector?: EnzymeSelector): this, 35 | parent(): this, 36 | closest(selector: EnzymeSelector): this, 37 | render(): CheerioWrapper, 38 | unmount(): this, 39 | text(): string, 40 | html(): string, 41 | get(index: number): React$Element, 42 | getNode(): React$Element, 43 | getNodes(): Array>, 44 | getDOMNode(): HTMLElement | HTMLInputElement, 45 | at(index: number): this, 46 | first(): this, 47 | last(): this, 48 | state(key?: string): any, 49 | context(key?: string): any, 50 | props(): Object, 51 | prop(key: string): any, 52 | key(): string, 53 | simulate(event: string, ...args: Array): this, 54 | setState(state: {}, callback?: Function): this, 55 | setProps(props: {}): this, 56 | setContext(context: Object): this, 57 | instance(): React$Component<*, *, *>, 58 | update(): this, 59 | debug(): string, 60 | type(): string | Function | null, 61 | name(): string, 62 | forEach(fn: (node: this, index: number) => mixed): this, 63 | map(fn: (node: this, index: number) => T): Array, 64 | reduce( 65 | fn: (value: T, node: this, index: number) => T, 66 | initialValue?: T 67 | ): Array, 68 | reduceRight( 69 | fn: (value: T, node: this, index: number) => T, 70 | initialValue?: T 71 | ): Array, 72 | some(selector: EnzymeSelector): boolean, 73 | someWhere(predicate: PredicateFunction): boolean, 74 | every(selector: EnzymeSelector): boolean, 75 | everyWhere(predicate: PredicateFunction): boolean, 76 | length: number 77 | } 78 | 79 | declare export class ReactWrapper extends Wrapper { 80 | constructor(nodes: NodeOrNodes, root: any, options?: ?Object): ReactWrapper, 81 | mount(): this, 82 | ref(refName: string): this, 83 | detach(): void 84 | } 85 | 86 | declare export class ShallowWrapper extends Wrapper { 87 | equals(node: React$Element): boolean, 88 | shallow(options?: { context?: Object }): ShallowWrapper 89 | } 90 | 91 | declare export function shallow( 92 | node: React$Element, 93 | options?: { context?: Object } 94 | ): ShallowWrapper; 95 | declare export function mount( 96 | node: React$Element, 97 | options?: { 98 | context?: Object, 99 | attachTo?: HTMLElement, 100 | childContextTypes?: Object 101 | } 102 | ): ReactWrapper; 103 | declare export function render( 104 | node: React$Element, 105 | options?: { context?: Object } 106 | ): CheerioWrapper; 107 | } 108 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 67b0c3a16b2d6f8ef0a31a5745a0b3e1 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/react-redux_v5.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 8d2267c19e5c5e51cde771e279074874 2 | // flow-typed version: aff2bf770e/react-redux_v5.x.x/flow_>=v0.53.x 3 | 4 | // flow-typed signature: 8db7b853f57c51094bf0ab8b2650fd9c 5 | // flow-typed version: ab8db5f14d/react-redux_v5.x.x/flow_>=v0.30.x 6 | 7 | import type { Dispatch, Store } from "redux"; 8 | 9 | declare module "react-redux" { 10 | /* 11 | 12 | S = State 13 | A = Action 14 | OP = OwnProps 15 | SP = StateProps 16 | DP = DispatchProps 17 | 18 | */ 19 | 20 | declare type MapStateToProps = ( 21 | state: S, 22 | ownProps: OP 23 | ) => SP | MapStateToProps; 24 | 25 | declare type MapDispatchToProps = 26 | | ((dispatch: Dispatch, ownProps: OP) => DP) 27 | | DP; 28 | 29 | declare type MergeProps = ( 30 | stateProps: SP, 31 | dispatchProps: DP, 32 | ownProps: OP 33 | ) => P; 34 | 35 | declare type Context = { store: Store<*, *> }; 36 | 37 | declare class ConnectedComponent extends React$Component { 38 | static WrappedComponent: Class>, 39 | getWrappedInstance(): React$Component

, 40 | props: OP, 41 | state: void 42 | } 43 | 44 | declare type ConnectedComponentClass = Class< 45 | ConnectedComponent 46 | >; 47 | 48 | declare type Connector = ( 49 | component: React$ComponentType

50 | ) => ConnectedComponentClass; 51 | 52 | declare class Provider extends React$Component<{ 53 | store: Store, 54 | children?: any 55 | }> {} 56 | 57 | declare type ConnectOptions = { 58 | pure?: boolean, 59 | withRef?: boolean 60 | }; 61 | 62 | declare type Null = null | void; 63 | 64 | declare function connect( 65 | ...rest: Array // <= workaround for https://github.com/facebook/flow/issues/2360 66 | ): Connector } & OP>>; 67 | 68 | declare function connect( 69 | mapStateToProps: Null, 70 | mapDispatchToProps: Null, 71 | mergeProps: Null, 72 | options: ConnectOptions 73 | ): Connector } & OP>>; 74 | 75 | declare function connect( 76 | mapStateToProps: MapStateToProps, 77 | mapDispatchToProps: Null, 78 | mergeProps: Null, 79 | options?: ConnectOptions 80 | ): Connector } & OP>>; 81 | 82 | declare function connect( 83 | mapStateToProps: Null, 84 | mapDispatchToProps: MapDispatchToProps, 85 | mergeProps: Null, 86 | options?: ConnectOptions 87 | ): Connector>; 88 | 89 | declare function connect( 90 | mapStateToProps: MapStateToProps, 91 | mapDispatchToProps: MapDispatchToProps, 92 | mergeProps: Null, 93 | options?: ConnectOptions 94 | ): Connector>; 95 | 96 | declare function connect( 97 | mapStateToProps: MapStateToProps, 98 | mapDispatchToProps: Null, 99 | mergeProps: MergeProps, 100 | options?: ConnectOptions 101 | ): Connector; 102 | 103 | declare function connect( 104 | mapStateToProps: MapStateToProps, 105 | mapDispatchToProps: MapDispatchToProps, 106 | mergeProps: MergeProps, 107 | options?: ConnectOptions 108 | ): Connector; 109 | } 110 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/react-router-dom_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: be340b56956828aa8e9838ed72b7e0a9 2 | // flow-typed version: 37d8964a70/react-router-dom_v4.x.x/flow_v0.53.x 3 | 4 | import * as React from 'react'; 5 | 6 | declare module 'react-router-dom' { 7 | declare export class BrowserRouter extends React$Component<{ 8 | basename?: string, 9 | forceRefresh?: boolean, 10 | getUserConfirmation?: GetUserConfirmation, 11 | keyLength?: number, 12 | children?: React$Node 13 | }> {} 14 | 15 | declare export class HashRouter extends React$Component<{ 16 | basename?: string, 17 | getUserConfirmation?: GetUserConfirmation, 18 | hashType?: 'slash' | 'noslash' | 'hashbang', 19 | children?: React$Node 20 | }> {} 21 | 22 | declare export class Link extends React$Component<{ 23 | to: string | LocationShape, 24 | replace?: boolean, 25 | children?: React$Node 26 | }> {} 27 | 28 | declare export class NavLink extends React$Component<{ 29 | to: string | LocationShape, 30 | activeClassName?: string, 31 | className?: string, 32 | activeStyle?: Object, 33 | style?: Object, 34 | isActive?: (match: Match, location: Location) => boolean, 35 | children?: React$Node, 36 | exact?: boolean, 37 | strict?: boolean 38 | }> {} 39 | 40 | // NOTE: Below are duplicated from react-router. If updating these, please 41 | // update the react-router and react-router-native types as well. 42 | declare export type Location = { 43 | pathname: string, 44 | search: string, 45 | hash: string, 46 | state?: any, 47 | key?: string 48 | }; 49 | 50 | declare export type LocationShape = { 51 | pathname?: string, 52 | search?: string, 53 | hash?: string, 54 | state?: any 55 | }; 56 | 57 | declare export type HistoryAction = 'PUSH' | 'REPLACE' | 'POP'; 58 | 59 | declare export type RouterHistory = { 60 | length: number, 61 | location: Location, 62 | action: HistoryAction, 63 | listen( 64 | callback: (location: Location, action: HistoryAction) => void 65 | ): () => void, 66 | push(path: string | LocationShape, state?: any): void, 67 | replace(path: string | LocationShape, state?: any): void, 68 | go(n: number): void, 69 | goBack(): void, 70 | goForward(): void, 71 | canGo?: (n: number) => boolean, 72 | block( 73 | callback: (location: Location, action: HistoryAction) => boolean 74 | ): void, 75 | // createMemoryHistory 76 | index?: number, 77 | entries?: Array 78 | }; 79 | 80 | declare export type Match = { 81 | params: { [key: string]: ?string }, 82 | isExact: boolean, 83 | path: string, 84 | url: string 85 | }; 86 | 87 | declare export type ContextRouter = { 88 | history: RouterHistory, 89 | location: Location, 90 | match: Match 91 | }; 92 | 93 | declare export type GetUserConfirmation = ( 94 | message: string, 95 | callback: (confirmed: boolean) => void 96 | ) => void; 97 | 98 | declare type StaticRouterContext = { 99 | url?: string 100 | }; 101 | 102 | declare export class StaticRouter extends React$Component<{ 103 | basename?: string, 104 | location?: string | Location, 105 | context: StaticRouterContext, 106 | children?: React$Node 107 | }> {} 108 | 109 | declare export class MemoryRouter extends React$Component<{ 110 | initialEntries?: Array, 111 | initialIndex?: number, 112 | getUserConfirmation?: GetUserConfirmation, 113 | keyLength?: number, 114 | children?: React$Node 115 | }> {} 116 | 117 | declare export class Router extends React$Component<{ 118 | history: RouterHistory, 119 | children?: React$Node 120 | }> {} 121 | 122 | declare export class Prompt extends React$Component<{ 123 | message: string | ((location: Location) => string | true), 124 | when?: boolean 125 | }> {} 126 | 127 | declare export class Redirect extends React$Component<{ 128 | to: string | LocationShape, 129 | push?: boolean 130 | }> {} 131 | 132 | declare export class Route extends React$Component<{ 133 | component?: React$ComponentType<*>, 134 | render?: (router: ContextRouter) => React$Node, 135 | children?: React.ComponentType, 136 | path?: string, 137 | exact?: boolean, 138 | strict?: boolean 139 | }> {} 140 | 141 | declare export class Switch extends React$Component<{ 142 | children?: React$Node 143 | }> {} 144 | 145 | declare export function withRouter

( 146 | Component: React$ComponentType 147 | ): React$ComponentType

; 148 | 149 | declare type MatchPathOptions = { 150 | path?: string, 151 | exact?: boolean, 152 | sensitive?: boolean, 153 | strict?: boolean 154 | }; 155 | 156 | declare export function matchPath( 157 | pathname: string, 158 | options?: MatchPathOptions | string 159 | ): null | Match; 160 | } 161 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/react-router-redux_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 544be0949417f8eaaa26567dfb394ab6 2 | // flow-typed version: <>/react-router-redux_v^5.0.0-alpha.6/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'react-router-redux' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'react-router-redux' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'react-router-redux/actions' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'react-router-redux/ConnectedRouter' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'react-router-redux/es/actions' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'react-router-redux/es/ConnectedRouter' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'react-router-redux/es/index' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'react-router-redux/es/middleware' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'react-router-redux/es/reducer' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'react-router-redux/middleware' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'react-router-redux/reducer' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'react-router-redux/umd/react-router-redux' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'react-router-redux/umd/react-router-redux.min' { 66 | declare module.exports: any; 67 | } 68 | 69 | // Filename aliases 70 | declare module 'react-router-redux/actions.js' { 71 | declare module.exports: $Exports<'react-router-redux/actions'>; 72 | } 73 | declare module 'react-router-redux/ConnectedRouter.js' { 74 | declare module.exports: $Exports<'react-router-redux/ConnectedRouter'>; 75 | } 76 | declare module 'react-router-redux/es/actions.js' { 77 | declare module.exports: $Exports<'react-router-redux/es/actions'>; 78 | } 79 | declare module 'react-router-redux/es/ConnectedRouter.js' { 80 | declare module.exports: $Exports<'react-router-redux/es/ConnectedRouter'>; 81 | } 82 | declare module 'react-router-redux/es/index.js' { 83 | declare module.exports: $Exports<'react-router-redux/es/index'>; 84 | } 85 | declare module 'react-router-redux/es/middleware.js' { 86 | declare module.exports: $Exports<'react-router-redux/es/middleware'>; 87 | } 88 | declare module 'react-router-redux/es/reducer.js' { 89 | declare module.exports: $Exports<'react-router-redux/es/reducer'>; 90 | } 91 | declare module 'react-router-redux/index' { 92 | declare module.exports: $Exports<'react-router-redux'>; 93 | } 94 | declare module 'react-router-redux/index.js' { 95 | declare module.exports: $Exports<'react-router-redux'>; 96 | } 97 | declare module 'react-router-redux/middleware.js' { 98 | declare module.exports: $Exports<'react-router-redux/middleware'>; 99 | } 100 | declare module 'react-router-redux/reducer.js' { 101 | declare module.exports: $Exports<'react-router-redux/reducer'>; 102 | } 103 | declare module 'react-router-redux/umd/react-router-redux.js' { 104 | declare module.exports: $Exports<'react-router-redux/umd/react-router-redux'>; 105 | } 106 | declare module 'react-router-redux/umd/react-router-redux.min.js' { 107 | declare module.exports: $Exports<'react-router-redux/umd/react-router-redux.min'>; 108 | } 109 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/redux-thunk_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: f4c4b4a278e8c8646bb2a10775fb0345 2 | // flow-typed version: <>/redux-thunk_v^2.2.0/flow_v0.53.1 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'redux-thunk' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'redux-thunk' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'redux-thunk/dist/redux-thunk' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'redux-thunk/dist/redux-thunk.min' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'redux-thunk/es/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'redux-thunk/lib/index' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'redux-thunk/src/index' { 42 | declare module.exports: any; 43 | } 44 | 45 | // Filename aliases 46 | declare module 'redux-thunk/dist/redux-thunk.js' { 47 | declare module.exports: $Exports<'redux-thunk/dist/redux-thunk'>; 48 | } 49 | declare module 'redux-thunk/dist/redux-thunk.min.js' { 50 | declare module.exports: $Exports<'redux-thunk/dist/redux-thunk.min'>; 51 | } 52 | declare module 'redux-thunk/es/index.js' { 53 | declare module.exports: $Exports<'redux-thunk/es/index'>; 54 | } 55 | declare module 'redux-thunk/lib/index.js' { 56 | declare module.exports: $Exports<'redux-thunk/lib/index'>; 57 | } 58 | declare module 'redux-thunk/src/index.js' { 59 | declare module.exports: $Exports<'redux-thunk/src/index'>; 60 | } 61 | -------------------------------------------------------------------------------- /react-ui/flow-typed/npm/redux_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: cce508c1d66e8095078fedab63ed6430 2 | // flow-typed version: a165222d28/redux_v3.x.x/flow_>=v0.33.x 3 | 4 | declare module 'redux' { 5 | /* 6 | 7 | S = State 8 | A = Action 9 | D = Dispatch 10 | 11 | */ 12 | 13 | declare type DispatchAPI = (action: A) => A; 14 | declare type Dispatch }> = DispatchAPI; 15 | 16 | declare type MiddlewareAPI> = { 17 | dispatch: D, 18 | getState(): S 19 | }; 20 | 21 | declare type Store> = { 22 | // rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages) 23 | dispatch: D, 24 | getState(): S, 25 | subscribe(listener: () => void): () => void, 26 | replaceReducer(nextReducer: Reducer): void 27 | }; 28 | 29 | declare type Reducer = (state: S, action: A) => S; 30 | 31 | declare type CombinedReducer = ( 32 | state: ($Shape & {}) | void, 33 | action: A 34 | ) => S; 35 | 36 | declare type Middleware> = ( 37 | api: MiddlewareAPI 38 | ) => (next: D) => D; 39 | 40 | declare type StoreCreator> = { 41 | (reducer: Reducer, enhancer?: StoreEnhancer): Store, 42 | ( 43 | reducer: Reducer, 44 | preloadedState: S, 45 | enhancer?: StoreEnhancer 46 | ): Store 47 | }; 48 | 49 | declare type StoreEnhancer> = ( 50 | next: StoreCreator 51 | ) => StoreCreator; 52 | 53 | declare function createStore( 54 | reducer: Reducer, 55 | enhancer?: StoreEnhancer 56 | ): Store; 57 | declare function createStore( 58 | reducer: Reducer, 59 | preloadedState: S, 60 | enhancer?: StoreEnhancer 61 | ): Store; 62 | 63 | declare function applyMiddleware( 64 | ...middlewares: Array> 65 | ): StoreEnhancer; 66 | 67 | declare type ActionCreator = (...args: Array) => A; 68 | declare type ActionCreators = { [key: K]: ActionCreator }; 69 | 70 | declare function bindActionCreators< 71 | A, 72 | C: ActionCreator, 73 | D: DispatchAPI 74 | >( 75 | actionCreator: C, 76 | dispatch: D 77 | ): C; 78 | declare function bindActionCreators< 79 | A, 80 | K, 81 | C: ActionCreators, 82 | D: DispatchAPI 83 | >( 84 | actionCreators: C, 85 | dispatch: D 86 | ): C; 87 | 88 | declare function combineReducers( 89 | reducers: O 90 | ): CombinedReducer<$ObjMap(r: Reducer) => S>, A>; 91 | 92 | declare function compose(ab: (a: A) => B): (a: A) => B; 93 | declare function compose( 94 | bc: (b: B) => C, 95 | ab: (a: A) => B 96 | ): (a: A) => C; 97 | declare function compose( 98 | cd: (c: C) => D, 99 | bc: (b: B) => C, 100 | ab: (a: A) => B 101 | ): (a: A) => D; 102 | declare function compose( 103 | de: (d: D) => E, 104 | cd: (c: C) => D, 105 | bc: (b: B) => C, 106 | ab: (a: A) => B 107 | ): (a: A) => E; 108 | declare function compose( 109 | ef: (e: E) => F, 110 | de: (d: D) => E, 111 | cd: (c: C) => D, 112 | bc: (b: B) => C, 113 | ab: (a: A) => B 114 | ): (a: A) => F; 115 | declare function compose( 116 | fg: (f: F) => G, 117 | ef: (e: E) => F, 118 | de: (d: D) => E, 119 | cd: (c: C) => D, 120 | bc: (b: B) => C, 121 | ab: (a: A) => B 122 | ): (a: A) => G; 123 | declare function compose( 124 | gh: (g: G) => H, 125 | fg: (f: F) => G, 126 | ef: (e: E) => F, 127 | de: (d: D) => E, 128 | cd: (c: C) => D, 129 | bc: (b: B) => C, 130 | ab: (a: A) => B 131 | ): (a: A) => H; 132 | declare function compose( 133 | hi: (h: H) => I, 134 | gh: (g: G) => H, 135 | fg: (f: F) => G, 136 | ef: (e: E) => F, 137 | de: (d: D) => E, 138 | cd: (c: C) => D, 139 | bc: (b: B) => C, 140 | ab: (a: A) => B 141 | ): (a: A) => I; 142 | } 143 | -------------------------------------------------------------------------------- /react-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@shopify/polaris": "^1.1.1", 7 | "axios": "^0.16.2", 8 | "lodash": "^4.17.4", 9 | "react": "^15.6.1", 10 | "react-dom": "^15.6.1", 11 | "react-redux": "^5.0.5", 12 | "react-router-dom": "^4.1.2", 13 | "react-router-redux": "^5.0.0-alpha.6", 14 | "react-scripts": "1.0.13", 15 | "redux": "^3.7.2", 16 | "redux-thunk": "^2.2.0", 17 | "url-search-params": "^0.9.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject", 24 | "flow": "flow" 25 | }, 26 | "proxy": { 27 | "/": { 28 | "target": "http://localhost:3001" 29 | } 30 | }, 31 | "devDependencies": { 32 | "enzyme": "^2.9.1", 33 | "flow-bin": "^0.53.1", 34 | "react-test-renderer": "^15.6.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /react-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmiha/create-shopify-app/1e22ce0e0dd6c31fc5f71470209f9918697b9ccf/react-ui/public/favicon.ico -------------------------------------------------------------------------------- /react-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 | 33 | 36 |

37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /react-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /react-ui/src/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { Route } from 'react-router-dom'; 4 | 5 | import About from './containers/About'; 6 | import ProductsPage from './containers/ProductsPage'; 7 | 8 | const App = () => 9 |
10 | 11 | 12 |
; 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /react-ui/src/containers/About.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import type { RouterHistory } from 'react-router-redux'; 4 | import { Banner, Page } from '@shopify/polaris'; 5 | 6 | const About = ({ history }: { history: RouterHistory }) => 7 | history.goBack() } 11 | ]} 12 | > 13 | 17 | ; 18 | 19 | export default About; 20 | -------------------------------------------------------------------------------- /react-ui/src/containers/About.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import About from './About'; 6 | 7 | it('renders without crashing', () => { 8 | const div = document.createElement('div'); 9 | ReactDOM.render(, div); 10 | }); 11 | 12 | it('Matches snapshot', () => { 13 | const tree = renderer.create().toJSON(); 14 | expect(tree).toMatchSnapshot(); 15 | }); 16 | -------------------------------------------------------------------------------- /react-ui/src/containers/ProductsPage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import _ from 'lodash'; 3 | import React, { Component } from 'react'; 4 | import type { Element } from 'react'; 5 | import type { RouterHistory } from 'react-router-redux'; 6 | import { connect } from 'react-redux'; 7 | 8 | import { Page, Card, ResourceList, Thumbnail } from '@shopify/polaris'; 9 | import { ResourcePicker } from '@shopify/polaris/embedded'; 10 | 11 | import { addProduct, fetchProducts } from '../redux/products'; 12 | import type { AddProductAction, Product, State, ThunkAction } from '../types'; 13 | 14 | type ProductResource = { 15 | media?: Element<*>, 16 | attributeOne: string, 17 | attributeTwo: string, 18 | attributeThree: string 19 | }; 20 | 21 | type OwnProps = { 22 | history: RouterHistory 23 | }; 24 | 25 | type StateProps = { 26 | products: ProductResource[] 27 | }; 28 | 29 | type DispatchProps = { 30 | addProduct: (product: Product) => AddProductAction, 31 | fetchProducts: () => ThunkAction 32 | }; 33 | 34 | type Props = StateProps & DispatchProps & OwnProps; 35 | 36 | type OwnState = { 37 | resourcePickerOpen: boolean 38 | }; 39 | 40 | type Resources = { 41 | products: Product[] 42 | }; 43 | 44 | export class ProductsPageComponent extends Component { 45 | state = { 46 | resourcePickerOpen: false 47 | }; 48 | 49 | componentDidMount() { 50 | const { fetchProducts } = this.props; 51 | 52 | fetchProducts(); 53 | } 54 | 55 | handleGoToProducts = () => { 56 | const { history } = this.props; 57 | 58 | history.push('/about'); 59 | }; 60 | 61 | handleResourceSelected = (resources: Resources) => { 62 | const { addProduct } = this.props; 63 | const { products } = resources; 64 | 65 | addProduct(products[0]); 66 | 67 | this.setState({ resourcePickerOpen: false }); 68 | }; 69 | 70 | render() { 71 | const { products = [] } = this.props; 72 | const { resourcePickerOpen } = this.state; 73 | 74 | return ( 75 | this.setState({ resourcePickerOpen: true }) 80 | }} 81 | secondaryActions={[ 82 | { 83 | content: 'Go to About', 84 | onAction: this.handleGoToProducts 85 | } 86 | ]} 87 | > 88 | 89 | 92 | } 93 | /> 94 | 95 | this.setState({ resourcePickerOpen: false })} 100 | /> 101 | 102 | ); 103 | } 104 | } 105 | 106 | const getProductResources = (products: ?(Product[])) => 107 | _.map(products, (product: Product): ProductResource => { 108 | const { image = {}, product_type, title, vendor } = product; 109 | 110 | return { 111 | media: image && , 112 | attributeOne: title, 113 | attributeTwo: product_type, 114 | attributeThree: vendor 115 | }; 116 | }); 117 | 118 | const mapStateToProps = (state: State): StateProps => { 119 | const { products } = state; 120 | 121 | return { 122 | products: getProductResources(products) 123 | }; 124 | }; 125 | 126 | const dispatchProps: DispatchProps = { addProduct, fetchProducts }; 127 | 128 | export default connect(mapStateToProps, dispatchProps)(ProductsPageComponent); 129 | -------------------------------------------------------------------------------- /react-ui/src/containers/ProductsPage.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import { ProductsPageComponent as ProductsPage } from './ProductsPage'; 6 | 7 | const fetchProducts = jest.fn(() => true); 8 | 9 | it('renders without crashing', () => { 10 | const div = document.createElement('div'); 11 | ReactDOM.render(, div); 12 | }); 13 | 14 | it('Matches snapshot', () => { 15 | const tree = renderer 16 | .create() 17 | .toJSON(); 18 | expect(tree).toMatchSnapshot(); 19 | }); 20 | -------------------------------------------------------------------------------- /react-ui/src/containers/__snapshots__/About.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Matches snapshot 1`] = ` 4 |
7 |
10 |
13 |

16 | About 17 |

18 |
19 |
22 |
25 |
28 | 37 |
38 |
39 |
40 |
41 |
44 |
53 |
56 | 60 | ", 65 | } 66 | } 67 | viewBox="0 0 20 20" 68 | /> 69 | 70 |
71 |
72 |
76 |

79 | Your React Shopify app is ready. You can start building solutions for merchants! 80 |

81 |
82 |
83 |
84 |
85 |
86 | `; 87 | -------------------------------------------------------------------------------- /react-ui/src/containers/__snapshots__/ProductsPage.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Matches snapshot 1`] = ` 4 |
7 |
10 |
13 |
16 |
19 |

22 | Products 23 |

24 |
25 |
28 |
31 | 51 |
52 |
55 |
58 | 67 |
68 |
69 |
70 |
71 |
74 | 94 |
95 |
96 |
97 |
100 |
103 |
    106 |
107 |
108 |
109 | `; 110 | -------------------------------------------------------------------------------- /react-ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /react-ui/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | import { ConnectedRouter } from 'react-router-redux'; 6 | import { Link } from 'react-router-dom'; 7 | import axios from 'axios'; 8 | import { useLinkComponent } from '@shopify/polaris'; 9 | // $FlowFixMe 10 | import '@shopify/polaris/styles.css'; 11 | import { EmbeddedApp } from '@shopify/polaris/embedded'; 12 | 13 | import store, { history } from './store'; 14 | 15 | import './index.css'; 16 | import App from './App'; 17 | // import registerServiceWorker from './registerServiceWorker'; 18 | 19 | axios.defaults.withCredentials = true; 20 | 21 | useLinkComponent(Link); 22 | 23 | // These values are used in development. They are defined in the .env file 24 | const { REACT_APP_SHOPIFY_API_KEY, REACT_APP_SHOP_ORIGIN } = process.env; 25 | 26 | type environment = { 27 | SHOPIFY_API_KEY?: string, 28 | SHOP_ORIGIN?: string 29 | }; 30 | 31 | const env: environment = window.env || {}; 32 | 33 | // Express injects these values in the client script when serving index.html 34 | const { SHOPIFY_API_KEY, SHOP_ORIGIN } = env; 35 | 36 | const apiKey: ?string = REACT_APP_SHOPIFY_API_KEY || SHOPIFY_API_KEY; 37 | const shop: ?string = REACT_APP_SHOP_ORIGIN || SHOP_ORIGIN; 38 | 39 | const shopOrigin: ?string = shop && `https://${shop}`; 40 | 41 | const target = document.getElementById('root'); 42 | 43 | render( 44 | 45 | 46 | 47 | 48 | 49 | 50 | , 51 | target 52 | ); 53 | 54 | // registerServiceWorker(); 55 | -------------------------------------------------------------------------------- /react-ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /react-ui/src/redux/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineReducers } from 'redux'; 3 | import { routerReducer } from 'react-router-redux'; 4 | import products from './products'; 5 | 6 | export default combineReducers({ 7 | router: routerReducer, 8 | products 9 | }); 10 | -------------------------------------------------------------------------------- /react-ui/src/redux/products.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import axios from 'axios'; 3 | 4 | import type { Action, AddProductAction, Product, ThunkAction } from '../types'; 5 | 6 | export const fetchProducts = (): ThunkAction => dispatch => 7 | axios 8 | .get('/api/products', { 9 | credentials: 'include' 10 | }) 11 | .then(response => { 12 | console.log(response); 13 | dispatch({ 14 | type: 'LOAD_PRODUCTS', 15 | products: response.data 16 | }); 17 | }); 18 | 19 | export const addProduct = (product: Product): AddProductAction => ({ 20 | type: 'ADD_PRODUCT', 21 | product 22 | }); 23 | 24 | export default (state: Product[] = [], action: Action): Product[] => { 25 | switch (action.type) { 26 | case 'LOAD_PRODUCTS': 27 | return action.products; 28 | 29 | case 'ADD_PRODUCT': 30 | return [...state, action.product]; 31 | 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /react-ui/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 18 | ), 19 | ); 20 | 21 | function registerValidSW(swUrl) { 22 | navigator.serviceWorker 23 | .register(swUrl) 24 | .then((registration) => { 25 | // eslint-disable-next-line no-param-reassign 26 | registration.onupdatefound = () => { 27 | const installingWorker = registration.installing; 28 | installingWorker.onstatechange = () => { 29 | if (installingWorker.state === 'installed') { 30 | if (navigator.serviceWorker.controller) { 31 | // At this point, the old content will have been purged and 32 | // the fresh content will have been added to the cache. 33 | // It's the perfect time to display a "New content is 34 | // available; please refresh." message in your web app. 35 | console.log('New content is available; please refresh.'); 36 | } else { 37 | // At this point, everything has been precached. 38 | // It's the perfect time to display a 39 | // "Content is cached for offline use." message. 40 | console.log('Content is cached for offline use.'); 41 | } 42 | } 43 | }; 44 | }; 45 | }) 46 | .catch((error) => { 47 | console.error('Error during service worker registration:', error); 48 | }); 49 | } 50 | 51 | function checkValidServiceWorker(swUrl) { 52 | // Check if the service worker can be found. If it can't reload the page. 53 | fetch(swUrl) 54 | .then((response) => { 55 | // Ensure service worker exists, and that we really are getting a JS file. 56 | if ( 57 | response.status === 404 || 58 | response.headers.get('content-type').indexOf('javascript') === -1 59 | ) { 60 | // No service worker found. Probably a different app. Reload the page. 61 | navigator.serviceWorker.ready.then((registration) => { 62 | registration.unregister().then(() => { 63 | window.location.reload(); 64 | }); 65 | }); 66 | } else { 67 | // Service worker found. Proceed as normal. 68 | registerValidSW(swUrl); 69 | } 70 | }) 71 | .catch(() => { 72 | console.log( 73 | 'No internet connection found. App is running in offline mode.', 74 | ); 75 | }); 76 | } 77 | 78 | export default function register() { 79 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 80 | // The URL constructor is available in all browsers that support SW. 81 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 82 | if (publicUrl.origin !== window.location.origin) { 83 | // Our service worker won't work if PUBLIC_URL is on a different origin 84 | // from what our page is served on. This might happen if a CDN is used to 85 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 86 | return; 87 | } 88 | 89 | window.addEventListener('load', () => { 90 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 91 | 92 | if (!isLocalhost) { 93 | // Is not local host. Just register service worker 94 | registerValidSW(swUrl); 95 | } else { 96 | // This is running on localhost. Lets check if a service worker still exists or not. 97 | checkValidServiceWorker(swUrl); 98 | } 99 | }); 100 | } 101 | } 102 | 103 | export function unregister() { 104 | if ('serviceWorker' in navigator) { 105 | navigator.serviceWorker.ready.then((registration) => { 106 | registration.unregister(); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /react-ui/src/store.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createStore, applyMiddleware, compose } from 'redux'; 3 | import { routerMiddleware } from 'react-router-redux'; 4 | import thunk from 'redux-thunk'; 5 | /* eslint-disable import/no-extraneous-dependencies */ 6 | // $FlowFixMe 7 | import createHistory from 'history/createBrowserHistory'; 8 | import rootReducer from './redux'; 9 | 10 | import type { Store } from './types'; 11 | 12 | export const history = createHistory(); 13 | 14 | const initialState = {}; 15 | const enhancers = []; 16 | const middleware = [thunk, routerMiddleware(history)]; 17 | 18 | if (process.env.NODE_ENV === 'development') { 19 | const devToolsExtension = window.devToolsExtension; 20 | 21 | if (typeof devToolsExtension === 'function') { 22 | enhancers.push(devToolsExtension()); 23 | } 24 | } 25 | 26 | const composedEnhancers = compose(applyMiddleware(...middleware), ...enhancers); 27 | 28 | const store: Store = createStore(rootReducer, initialState, composedEnhancers); 29 | 30 | export default store; 31 | -------------------------------------------------------------------------------- /react-ui/src/types/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Store as ReduxStore } from 'redux'; 3 | 4 | export type Image = { 5 | src: string 6 | }; 7 | 8 | export type Product = { 9 | body_html: string, 10 | image: Image, 11 | product_type: string, 12 | title: string, 13 | vendor: string 14 | }; 15 | 16 | export type State = { 17 | router?: any, 18 | products?: Product[] 19 | }; 20 | 21 | export type LoadProductsAction = { 22 | +type: 'LOAD_PRODUCTS', 23 | +products: Product[] 24 | }; 25 | 26 | export type AddProductAction = { 27 | +type: 'ADD_PRODUCT', 28 | +product: Product 29 | }; 30 | 31 | export type Action = LoadProductsAction | AddProductAction; 32 | 33 | export type Store = ReduxStore; 34 | 35 | export type GetState = () => Object; 36 | 37 | // eslint-disable-next-line no-use-before-define 38 | export type ThunkAction = (dispatch: Dispatch, getState: GetState) => any; 39 | export type PromiseAction = Promise; 40 | 41 | export type Dispatch = ( 42 | action: Action | ThunkAction | PromiseAction | Array 43 | ) => any; 44 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | // import favicon from 'serve-favicon'; 4 | import cookieParser from 'cookie-parser'; 5 | import bodyParser from 'body-parser'; 6 | import flash from 'express-flash'; 7 | import fs from 'fs'; 8 | import methodOverride from 'method-override'; 9 | import gzip from 'compression'; 10 | import helmet from 'helmet'; 11 | import morgan from 'morgan'; 12 | import serialize from 'serialize-javascript'; 13 | import session from 'express-session'; 14 | import logger from 'winston'; 15 | 16 | import { APP_URL, ENV, isDebug, isProduction, sessionSecret } from './config'; 17 | 18 | import { connect, session as sessionStore } from './db'; 19 | 20 | import initShopify from './routes/shopify'; 21 | 22 | // We configure dotenv as early as possible in the app 23 | require('dotenv').config(); 24 | 25 | // Connect database 26 | connect(); 27 | 28 | const { SHOPIFY_API_KEY } = process.env; 29 | 30 | const app = express(); 31 | 32 | if (ENV === 'production') { 33 | app.use(gzip()); 34 | // Secure your Express apps by setting various HTTP headers. Documentation: https://github.com/helmetjs/helmet 35 | app.use( 36 | helmet({ 37 | frameguard: { 38 | action: 'allow-from', 39 | domain: 'https://myshopify.com' 40 | } 41 | }) 42 | ); 43 | } 44 | 45 | const env = { 46 | SHOPIFY_API_KEY 47 | }; 48 | 49 | // view engine setup 50 | app.set('views', path.join(__dirname, 'views')); 51 | app.set('view engine', 'pug'); 52 | 53 | // app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 54 | app.use(morgan(isDebug ? 'dev' : 'combined')); 55 | 56 | const rawBodySaver = (req, res, buf, encoding) => { 57 | if (buf && buf.length) { 58 | req.rawBody = buf.toString(encoding || 'utf8'); 59 | } 60 | }; 61 | 62 | app.use(bodyParser.json({ verify: rawBodySaver })); 63 | app.use(bodyParser.urlencoded({ extended: false })); 64 | 65 | app.use(methodOverride()); 66 | app.use(cookieParser()); 67 | 68 | const staticOptions = isProduction && { index: '_' }; 69 | app.use( 70 | express.static(path.join(__dirname, '../react-ui/build'), staticOptions) 71 | ); 72 | 73 | const sessionConfig = { 74 | resave: false, 75 | saveUninitialized: false, 76 | secret: sessionSecret, 77 | proxy: true, // The "X-Forwarded-Proto" header will be used. 78 | name: 'sessionId', 79 | // Add HTTPOnly, Secure attributes on Session Cookie 80 | // If secure is set, and you access your site over HTTP, the cookie will not be set 81 | cookie: { 82 | httpOnly: true, 83 | secure: ENV === 'production' 84 | }, 85 | store: sessionStore 86 | }; 87 | 88 | app.use(session(sessionConfig)); 89 | app.use(flash()); 90 | 91 | app.use('/', initShopify()); 92 | 93 | app.get('/api/products', (req, res) => { 94 | const { shopify } = req; 95 | 96 | shopify.product.list({ limit: 5 }).then(products => { 97 | res.status(200).json(products); 98 | }); 99 | }); 100 | 101 | app.get('*', (req, res, next) => { 102 | const { shop } = req.session.shopify || {}; 103 | 104 | const environment = { ...env, SHOP_ORIGIN: shop }; 105 | 106 | fs.readFile( 107 | path.join(__dirname, '../react-ui/build/index.html'), 108 | 'utf8', 109 | (err, content) => { 110 | if (err) { 111 | return next(err); 112 | } 113 | 114 | // Inject environment variables (Shopify API key and shop) in client code, 115 | // to be usd by the embedded app 116 | const replacement = `window.env = ${serialize(environment)}`; 117 | const result = content.replace('var __ENVIRONMENT__', replacement); 118 | return res.send(result); 119 | } 120 | ); 121 | }); 122 | 123 | // eslint-disable-next-line no-unused-vars 124 | app.use((err, req, res, next) => { 125 | logger.error(err); 126 | const status = err.status || 500; 127 | 128 | res.status(status); 129 | res.render(`${status}`, { 130 | message: err.message, 131 | error: isDebug && err.stack, 132 | APP_URL 133 | }); 134 | }); 135 | 136 | export default app; 137 | -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | export const ENV = process.env.NODE_ENV || 'development'; 2 | export const isProduction = ENV === 'production'; 3 | export const isDebug = ENV === 'development'; 4 | export const isClient = typeof window !== 'undefined'; 5 | export const isTest = ENV === 'test'; 6 | 7 | export const SCOPES = 'read_orders,read_products'; 8 | export const ACTIVATE_CHARGE_ROUTE = '/activate_charge'; 9 | export const APP_NAME = 'mihovil-test-app'; 10 | export const APP_URL = ''; 11 | export const APP_HOME_ROUTE = '/home'; 12 | export const AUTH_CALLBACK_ROUTE = '/auth/callback'; 13 | export const INSTALL_PAGE = `https://apps.shopify.com/${APP_NAME}`; 14 | export const UNINSTALL_ROUTE = '/uninstall'; 15 | 16 | export const sessionSecret = 17 | process.env.SESSION_SECRET || 'Your Session Secret goes here'; 18 | 19 | export const REDIS_URL = process.env.REDIS_URL || 'redis://:@127.0.0.1:6379'; 20 | -------------------------------------------------------------------------------- /server/db/connect.js: -------------------------------------------------------------------------------- 1 | import logger from 'winston'; 2 | import { sequelize } from './models'; 3 | 4 | export default () => { 5 | sequelize 6 | .authenticate() 7 | .then(() => { 8 | logger.info('Connection has been established successfully.'); 9 | }) 10 | .catch((err) => { 11 | logger.error('Unable to connect to the database:', err); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | import connect from './connect'; 2 | import session from './session'; 3 | import { Models, sequelize } from './models'; 4 | 5 | export { connect, Models, sequelize, session }; 6 | -------------------------------------------------------------------------------- /server/db/migrations/20170805140708-create-shop.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up(queryInterface, Sequelize) { 3 | return queryInterface.createTable('Shops', { 4 | id: { 5 | allowNull: false, 6 | autoIncrement: true, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER, 9 | }, 10 | domain: { 11 | type: Sequelize.STRING, 12 | unique: true, 13 | }, 14 | chargeId: { 15 | type: Sequelize.BIGINT, 16 | unique: true, 17 | }, 18 | createdAt: { 19 | allowNull: false, 20 | type: Sequelize.DATE, 21 | }, 22 | updatedAt: { 23 | allowNull: false, 24 | type: Sequelize.DATE, 25 | }, 26 | }); 27 | }, 28 | down(queryInterface) { 29 | return queryInterface.dropTable('Shops'); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /server/db/models/index.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | 3 | import { ENV } from '../../config'; 4 | import sequelizeConfig from '../sequelize_config.json'; 5 | import shopModel from './shop'; 6 | 7 | const config = sequelizeConfig[ENV]; 8 | 9 | const db = {}; 10 | 11 | const dbUrl = process.env[config.use_env_variable]; 12 | 13 | const { database, username, password } = config; 14 | 15 | const sequelize = dbUrl 16 | ? new Sequelize(dbUrl) 17 | : new Sequelize(database, username, password, config); 18 | 19 | db.Shop = sequelize.import('Shop', shopModel); 20 | 21 | Object.keys(db).forEach((modelName) => { 22 | if (db[modelName].associate) { 23 | db[modelName].associate(db); 24 | } 25 | }); 26 | 27 | export { db as Models, sequelize }; 28 | -------------------------------------------------------------------------------- /server/db/models/shop.js: -------------------------------------------------------------------------------- 1 | export default (sequelize, DataTypes) => { 2 | const Shop = sequelize.define( 3 | 'Shop', 4 | { 5 | domain: { 6 | type: DataTypes.STRING, 7 | unique: true 8 | }, 9 | chargeId: { 10 | type: DataTypes.BIGINT, 11 | unique: true 12 | } 13 | }, 14 | { 15 | classMethods: { 16 | associate() { 17 | // associations can be defined here 18 | } 19 | } 20 | } 21 | ); 22 | return Shop; 23 | }; 24 | -------------------------------------------------------------------------------- /server/db/sequelize_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "postgres", 4 | "password": "postgres", 5 | "database": "shopify-app-development", 6 | "host": "127.0.0.1", 7 | "dialect": "postgres" 8 | }, 9 | "test": { 10 | "username": "postgres", 11 | "password": "postgres", 12 | "database": "shopify-app-test", 13 | "host": "127.0.0.1", 14 | "dialect": "postgres", 15 | "logging": false 16 | }, 17 | "production": { 18 | "use_env_variable": "DATABASE_URL", 19 | "username": "postgres", 20 | "password": "postgres", 21 | "database": "shopify-app-production", 22 | "host": "127.0.0.1", 23 | "dialect": "postgres" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/db/session.js: -------------------------------------------------------------------------------- 1 | import session from 'express-session'; 2 | import connectRedis from 'connect-redis'; 3 | 4 | import { isTest, REDIS_URL } from '../config'; 5 | 6 | const RedisStore = connectRedis(session); 7 | 8 | const store = new RedisStore({ 9 | url: REDIS_URL, 10 | // We use the 2nd database in Redis (1) in test to be able to clean it. 11 | db: isTest ? 1 : 0, 12 | }); 13 | 14 | store.on('connect', () => { 15 | console.log(`===> 😊 Connected to Redis Server on ${REDIS_URL}. . .`); 16 | }); 17 | 18 | export default store; 19 | -------------------------------------------------------------------------------- /server/routes/session-helper.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import app from '../app'; 4 | 5 | import { APP_HOME_ROUTE, AUTH_CALLBACK_ROUTE } from '../config'; 6 | import { shopifyToken } from './setupMocks'; 7 | 8 | export const getSessionCookie = response => 9 | response.headers['set-cookie'].pop().split(';')[0]; 10 | 11 | let cookies; 12 | 13 | /** 14 | * Authenticates a shop with the application. 15 | * 16 | * @param {string} shop The shop 17 | * 18 | * @return Promise that resolves with the cookie that has the active session's ID 19 | */ 20 | export const login = shop => 21 | new Promise(resolve => { 22 | const loginPath = `${APP_HOME_ROUTE}?shop=${shop}`; 23 | 24 | request(app).get(loginPath).end((err, res) => { 25 | // Save the cookie to use it later to retrieve the session 26 | const Cookies = getSessionCookie(res); 27 | 28 | const state = shopifyToken.generateNonce(); 29 | 30 | const authCallback = `${AUTH_CALLBACK_ROUTE}?state=${state}&code=code&shop=${shop}`; 31 | 32 | const req = request(app).get(authCallback); 33 | req.cookies = Cookies; 34 | 35 | req.end(() => { 36 | cookies = Cookies; 37 | resolve(Cookies); 38 | }); 39 | }); 40 | }); 41 | 42 | /** 43 | * Creates an authenticated request. 44 | * 45 | * @param {string} route A protected route 46 | */ 47 | export const getRequest = route => { 48 | const req = request(app).get(route); 49 | req.cookies = cookies; 50 | return req; 51 | }; 52 | -------------------------------------------------------------------------------- /server/routes/setupMocks.js: -------------------------------------------------------------------------------- 1 | import ShopifyTokenModule from 'shopify-token'; 2 | import ShopifyApiModule from 'shopify-api-node'; 3 | 4 | import redis from 'redis'; 5 | import Promise from 'bluebird'; 6 | import { REDIS_URL } from '../config'; 7 | import { sequelize } from '../db/models'; 8 | 9 | Promise.promisifyAll(redis.RedisClient.prototype); 10 | 11 | const client = redis.createClient({ 12 | url: REDIS_URL, 13 | db: 1 14 | }); 15 | 16 | jest.mock('shopify-token'); 17 | jest.mock('shopify-api-node'); 18 | 19 | // We use these mock values throughout our tests 20 | export const accessToken = 'token'; 21 | export const shopName = 'mihovil'; 22 | export const shop = `${shopName}.myshopify.com`; 23 | export const nonce = 'randomToken'; 24 | 25 | export const shopifyToken = { 26 | generateAuthUrl: () => `https://${shop}/admin/oauth/authorize`, 27 | generateNonce: () => nonce 28 | }; 29 | 30 | // The mocked ShopifyToken module returns a constructor function. 31 | // We return a dummy token object from it. 32 | export const ShopifyToken = jest.fn(() => shopifyToken); 33 | 34 | export const shopifyApi = { 35 | order: {}, 36 | shop: { 37 | get: () => Promise.resolve() 38 | }, 39 | recurringApplicationCharge: {}, 40 | webhook: { 41 | create: () => Promise.resolve() 42 | } 43 | }; 44 | 45 | export const ShopifyApi = jest.fn(() => shopifyApi); 46 | 47 | export default () => { 48 | ShopifyTokenModule.mockImplementation(ShopifyToken); 49 | ShopifyApiModule.mockImplementation(ShopifyApi); 50 | 51 | // Clean old sessions from the test Redis database before each test suite. 52 | return client.flushdbAsync(); 53 | }; 54 | 55 | export const seedDatabase = () => sequelize.sync({ force: true }); 56 | -------------------------------------------------------------------------------- /server/routes/shopify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Routes for express app 3 | */ 4 | import _ from 'lodash'; 5 | import crypto from 'crypto'; 6 | import express from 'express'; 7 | import ShopifyToken from 'shopify-token'; 8 | import ShopifyApi from 'shopify-api-node'; 9 | import logger from 'winston'; 10 | 11 | import { 12 | ACTIVATE_CHARGE_ROUTE, 13 | APP_HOME_ROUTE, 14 | APP_NAME, 15 | APP_URL, 16 | AUTH_CALLBACK_ROUTE, 17 | INSTALL_PAGE, 18 | SCOPES, 19 | UNINSTALL_ROUTE 20 | } from '../config'; 21 | 22 | import { Models } from '../db'; 23 | 24 | const { Shop } = Models; 25 | 26 | const router = express.Router(); 27 | 28 | export default () => { 29 | const { SHOPIFY_API_KEY, SHOPIFY_API_SECRET } = process.env; 30 | 31 | const getShopifyToken = () => 32 | new ShopifyToken({ 33 | sharedSecret: SHOPIFY_API_SECRET, 34 | redirectUri: `${APP_URL}${AUTH_CALLBACK_ROUTE}`, 35 | scopes: SCOPES, 36 | apiKey: SHOPIFY_API_KEY 37 | }); 38 | 39 | const getAppsHome = shop => `https://${shop}/admin/apps/`; 40 | 41 | // The home page of the app in Shopify Admin 42 | const getEmbeddedAppHome = shop => `${getAppsHome(shop)}${APP_NAME}`; 43 | 44 | /** 45 | * Authenticates the shop with Shopify when accessing protected routes. 46 | * Returns a template file that redirects to the Shopify authorization page. 47 | * This mechanism is used to authorize an embedded app. 48 | * We need custom Javascript to escape the iframe as described in the docs. 49 | * See the "shopify_redirect" template for details. 50 | */ 51 | const authenticate = (req, res) => { 52 | const { query, session } = req; 53 | 54 | const shop = query.shop || req.body.shop; 55 | 56 | logger.info('Authenticating shop %s', shop); 57 | 58 | if (!shop) { 59 | res.redirect(INSTALL_PAGE); 60 | return; 61 | } 62 | 63 | const shopifyToken = getShopifyToken(); 64 | const nonce = shopifyToken.generateNonce(); 65 | 66 | // Save the nonce to state to verify it in the callback route later on 67 | session.state = nonce; 68 | 69 | const shopName = shop.split('.')[0]; 70 | 71 | const url = decodeURI( 72 | shopifyToken.generateAuthUrl(shopName, undefined, nonce) 73 | ); 74 | 75 | res.render('shopify_redirect', { url, shop }); 76 | }; 77 | 78 | /** 79 | * Creates an interface for accessing the Shopify API. 80 | * @param session A Shopify session with shop domain and access token 81 | */ 82 | const getShopifyApi = session => { 83 | const { shopify: { shop: shopUrl, token } } = session; 84 | 85 | return new ShopifyApi({ 86 | shopName: shopUrl.split('.')[0], 87 | accessToken: token 88 | }); 89 | }; 90 | 91 | /** 92 | * This method gets called when the app is installed. 93 | * Setup any webhooks or services you need on Shopify inside here. 94 | * 95 | * @param session New session 96 | */ 97 | const afterShopifyAuth = session => { 98 | const shopify = getShopifyApi(session); 99 | 100 | const webhook = { 101 | topic: 'app/uninstalled', 102 | address: `${APP_URL}${UNINSTALL_ROUTE}`, 103 | format: 'json' 104 | }; 105 | 106 | shopify.webhook.create(webhook); 107 | }; 108 | 109 | /** 110 | * Creates a new recurring application charge and redirects the mercant to 111 | * the confirmation screen. 112 | */ 113 | const createRecurringApplicationCharge = (req, res, next) => { 114 | const { shopify, session: { shopify: { shop } } } = req; 115 | 116 | const newCharge = { 117 | name: APP_NAME, 118 | price: 9.99, 119 | return_url: `${APP_URL}${ACTIVATE_CHARGE_ROUTE}`, 120 | test: true, 121 | trial_days: 7 122 | }; 123 | 124 | shopify.recurringApplicationCharge 125 | .create(newCharge) 126 | .then(charge => { 127 | res.render('shopify_redirect', { 128 | url: charge.confirmation_url, 129 | shop 130 | }); 131 | }) 132 | .catch(next); 133 | }; 134 | 135 | const hasActiveRecurringApplicationCharge = shopify => 136 | shopify.recurringApplicationCharge 137 | .list() 138 | .then(charges => _.find(charges, { status: 'active' })); 139 | 140 | /** 141 | * Shopify calls this route after the merchant authorizes the app. 142 | * It needs to match the callback route that you set in app settings. 143 | */ 144 | router.get(AUTH_CALLBACK_ROUTE, (req, res, next) => { 145 | const { query, session } = req; 146 | 147 | const { code, shop, state } = query; 148 | 149 | const shopifyToken = getShopifyToken(); 150 | 151 | if ( 152 | typeof state !== 'string' || 153 | state !== session.state || // Validate the state. 154 | !shopifyToken.verifyHmac(query) // Validate the hmac. 155 | ) { 156 | return res.status(400).send('Security checks failed'); 157 | } 158 | 159 | // Exchange the authorization code for a permanent access token. 160 | return shopifyToken 161 | .getAccessToken(shop, code) 162 | .then(token => { 163 | session.shopify = { shop, token }; 164 | 165 | return Shop.findOrCreate({ 166 | where: { 167 | domain: shop 168 | } 169 | }).spread(() => { 170 | afterShopifyAuth(session); 171 | 172 | req.shopify = getShopifyApi(session); 173 | 174 | hasActiveRecurringApplicationCharge(req.shopify).then(isActive => { 175 | if (isActive) { 176 | return res.redirect(getEmbeddedAppHome(shop)); 177 | } 178 | return createRecurringApplicationCharge(req, res, next); 179 | }); 180 | }); 181 | }) 182 | .catch(next); 183 | }); 184 | 185 | const verifyWebhookHMAC = req => { 186 | const hmac = req.headers['x-shopify-hmac-sha256']; 187 | 188 | const digest = crypto 189 | .createHmac('SHA256', SHOPIFY_API_SECRET) 190 | .update(req.rawBody) 191 | .digest('base64'); 192 | 193 | return digest === hmac; 194 | }; 195 | 196 | /** 197 | * This endpoint recieves the uninstall webhook and cleans up data. 198 | * Add to this endpoint as your app stores more data. If you need to do a lot of work, return 200 199 | * right away and queue it as a worker job. 200 | */ 201 | router.post(UNINSTALL_ROUTE, (req, res) => { 202 | if (!verifyWebhookHMAC(req)) { 203 | res.status(401).send('Webhook HMAC Failed'); 204 | return; 205 | } 206 | 207 | Shop.destroy({ 208 | where: { 209 | domain: req.headers['x-shopify-shop-domain'] 210 | } 211 | }).then(() => { 212 | res.status(200).send('Uninstalled'); 213 | }); 214 | }); 215 | 216 | /** 217 | * This middleware checks if we have a session. 218 | * If so, it attaches the Shopify API to the request object. 219 | * If there is no session or we have a different shop, 220 | * we start the authentication process. 221 | */ 222 | const authMiddleware = (req, res, next) => { 223 | logger.info(`Checking for valid session: ${req.query.shop}`); 224 | const { session, query: { shop } } = req; 225 | 226 | if (!session.shopify || (shop && session.shopify.shop !== shop)) { 227 | delete session.shopify; 228 | authenticate(req, res); 229 | return; 230 | } 231 | 232 | req.shopify = getShopifyApi(session); 233 | next(); 234 | }; 235 | 236 | router.use(authMiddleware); 237 | 238 | /* 239 | * Shopify calls this route when the merchant accepts or declines the charge. 240 | */ 241 | router.get('/activate_charge', (req, res, next) => { 242 | const { 243 | shopify: { recurringApplicationCharge }, 244 | query: { charge_id: chargeId }, 245 | session: { shopify: { shop } } 246 | } = req; 247 | 248 | recurringApplicationCharge 249 | .get(chargeId) 250 | .then(charge => { 251 | if (charge.status === 'accepted') { 252 | return recurringApplicationCharge.activate(chargeId).then(() => 253 | // We redirect to the home page of the app in Shopify admin 254 | res.redirect(getEmbeddedAppHome(shop)) 255 | ); 256 | } 257 | res.status(401); 258 | return res.render('charge_declined', { APP_URL }); 259 | }) 260 | .catch(next); 261 | }); 262 | 263 | router.get('/logout', (req, res) => { 264 | const { shop } = req.session.shopify; 265 | 266 | delete req.session.shopify; 267 | 268 | res.redirect(getAppsHome(shop)); 269 | }); 270 | 271 | router.get('/api/orders', (req, res) => { 272 | const { shopify } = req; 273 | 274 | shopify.order.list({ limit: 5 }).then(orders => { 275 | res.status(200).json(orders); 276 | }); 277 | }); 278 | 279 | /** 280 | * Checks if we have an active application charge. 281 | * This middleware is active when the app is initially loaded. 282 | */ 283 | const checkActiveRecurringApplicationCharge = (req, res, next) => { 284 | logger.info(`Checking for active application charge: ${req.query.shop}`); 285 | const { shopify } = req; 286 | 287 | hasActiveRecurringApplicationCharge(shopify).then(isActive => { 288 | if (!isActive) { 289 | logger.info(`No active charge found: ${req.query.shop}`); 290 | createRecurringApplicationCharge(req, res); 291 | return; 292 | } 293 | next(); 294 | }); 295 | }; 296 | 297 | /* 298 | * Checks if the session is still valid by making a basic API call, as described in: 299 | * https://stackoverflow.com/questions/14418415/shopify-how-can-i-handle-an-uninstall-followed-by-an-instant-re-install 300 | */ 301 | const checkForValidSession = (req, res, next) => { 302 | logger.info(`Checking if the session is still valid: ${req.query.shop}`); 303 | const { session, shopify } = req; 304 | 305 | return shopify.shop 306 | .get() 307 | .then(() => next()) 308 | .catch(() => { 309 | // Destroy the Shopify reference 310 | delete session.shopify; 311 | authenticate(req, res); 312 | }); 313 | }; 314 | 315 | router.get( 316 | APP_HOME_ROUTE, 317 | checkForValidSession, 318 | checkActiveRecurringApplicationCharge, 319 | (req, res) => { 320 | res.redirect('/'); 321 | } 322 | ); 323 | 324 | return router; 325 | }; 326 | -------------------------------------------------------------------------------- /server/routes/shopify.spec.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import cheerio from 'cheerio'; 3 | import crypto from 'crypto'; 4 | 5 | import setupMocks, { 6 | accessToken, 7 | nonce, 8 | seedDatabase, 9 | shop, 10 | shopName, 11 | shopifyApi, 12 | ShopifyApi, 13 | shopifyToken, 14 | ShopifyToken 15 | } from './setupMocks'; 16 | 17 | import { getRequest, login } from './session-helper'; 18 | 19 | import app from '../app'; 20 | import { Models } from '../db'; 21 | 22 | import { 23 | ACTIVATE_CHARGE_ROUTE, 24 | APP_HOME_ROUTE, 25 | APP_NAME, 26 | APP_URL, 27 | AUTH_CALLBACK_ROUTE, 28 | SCOPES, 29 | INSTALL_PAGE, 30 | UNINSTALL_ROUTE 31 | } from '../config'; 32 | 33 | const { Shop } = Models; 34 | 35 | const { SHOPIFY_API_KEY, SHOPIFY_API_SECRET } = process.env; 36 | 37 | const appsList = `https://${shop}/admin/apps/`; 38 | const appHome = `${appsList}${APP_NAME}`; 39 | 40 | const shopifyTokenOptions = { 41 | sharedSecret: SHOPIFY_API_SECRET, 42 | redirectUri: `${APP_URL}${AUTH_CALLBACK_ROUTE}`, 43 | scopes: SCOPES, 44 | apiKey: SHOPIFY_API_KEY 45 | }; 46 | 47 | const { recurringApplicationCharge } = shopifyApi; 48 | 49 | const confirmationUrl = 'Confirmation URL'; 50 | 51 | const newCharge = { 52 | name: APP_NAME, 53 | price: 9.99, 54 | return_url: `${APP_URL}${ACTIVATE_CHARGE_ROUTE}`, 55 | test: true, 56 | trial_days: 7 57 | }; 58 | 59 | const expectShopifyAuthorizationRedirect = res => { 60 | const expectedUrl = shopifyToken.generateAuthUrl(); 61 | 62 | const $ = cheerio.load(res.text); 63 | const script = $('script').html(); 64 | 65 | expect(script).toMatch(`window.top.location.href = "${expectedUrl}";`); 66 | expect(script).toMatch("message: 'Shopify.API.remoteRedirect',"); 67 | expect(script).toMatch(`data: { location: '${expectedUrl}' }`); 68 | expect(script).toMatch( 69 | `window.parent.postMessage(message, 'https://${shop}');` 70 | ); 71 | }; 72 | 73 | const expectChargeConfirmationRedirect = res => { 74 | expect(recurringApplicationCharge.create).toHaveBeenCalledWith(newCharge); 75 | 76 | const $ = cheerio.load(res.text); 77 | const script = $('script').html(); 78 | 79 | expect(script).toMatch(`window.top.location.href = "${confirmationUrl}";`); 80 | expect(script).toMatch("message: 'Shopify.API.remoteRedirect',"); 81 | expect(script).toMatch(`data: { location: '${confirmationUrl}' }`); 82 | expect(script).toMatch( 83 | `window.parent.postMessage(message, 'https://${shop}');` 84 | ); 85 | }; 86 | 87 | describe('Authentication middleware', () => { 88 | beforeAll(() => setupMocks().then(() => seedDatabase())); 89 | 90 | beforeEach(() => { 91 | shopifyToken.verifyHmac = jest.fn(() => true); 92 | shopifyToken.getAccessToken = jest.fn(() => Promise.resolve(accessToken)); 93 | 94 | recurringApplicationCharge.list = () => Promise.resolve([]); 95 | recurringApplicationCharge.create = jest.fn(() => 96 | Promise.resolve({ confirmation_url: confirmationUrl }) 97 | ); 98 | }); 99 | 100 | it('should redirect to install page when there is no Shopify session and no shop in URL', () => 101 | request(app).get(APP_HOME_ROUTE).expect('Location', INSTALL_PAGE)); 102 | 103 | it('should instantiate Shopify token with proper options', () => 104 | request(app).get(`${APP_HOME_ROUTE}?shop=${shop}`).then(() => { 105 | expect(ShopifyToken).toHaveBeenCalledWith(shopifyTokenOptions); 106 | })); 107 | 108 | it('should redirect to authorization URL on Shopify when there is no Shopify session and a shop is present in the query', () => 109 | request(app) 110 | .get(`${APP_HOME_ROUTE}?shop=${shop}`) 111 | .expect(200) 112 | .then(expectShopifyAuthorizationRedirect)); 113 | 114 | it("should redirect to authorization URL on Shopify when there is a shop in the query and it's different than the one in session", () => 115 | login('someothershop.myshopify.com', nonce).then(Cookies => { 116 | const req = request(app).get(`${APP_HOME_ROUTE}?shop=${shop}`); 117 | req.cookies = Cookies; 118 | req.expect(200).then(expectShopifyAuthorizationRedirect); 119 | })); 120 | 121 | describe('Auth callback', () => { 122 | const originalPath = `${APP_HOME_ROUTE}?shop=${shop}`; 123 | const code = 'code'; 124 | const authCallback = `${AUTH_CALLBACK_ROUTE}?state=${nonce}&code=${code}&shop=${shop}`; 125 | 126 | let Cookies; 127 | 128 | const callAuthCallback = () => { 129 | const req = request(app).get(authCallback); 130 | req.cookies = Cookies; 131 | return req; 132 | }; 133 | 134 | beforeEach(() => 135 | request(app).get(originalPath).then(res => { 136 | // Save the cookie to use it later to retrieve the session 137 | Cookies = res.headers['set-cookie'].pop().split(';')[0]; 138 | return seedDatabase(); 139 | }) 140 | ); 141 | 142 | it("should return 400 if state sent in the query doesn't match the one saved in session", () => { 143 | const req = request(app).get(`${AUTH_CALLBACK_ROUTE}?state=wrongState`); 144 | req.cookies = Cookies; 145 | return req.expect(400); 146 | }); 147 | 148 | it('should verify HMAC and return 400 if verification fails', () => { 149 | shopifyToken.verifyHmac = jest.fn(() => false); 150 | return callAuthCallback().expect(400).then(() => { 151 | expect(shopifyToken.verifyHmac).toHaveBeenCalledWith({ 152 | state: nonce, 153 | code, 154 | shop 155 | }); 156 | }); 157 | }); 158 | 159 | it('should fetch access token for shop with given code when verification succeeds', () => 160 | callAuthCallback().then(() => { 161 | expect(shopifyToken.getAccessToken).toHaveBeenCalledWith(shop, code); 162 | })); 163 | 164 | it('should save shop to database after installation', () => 165 | callAuthCallback().then(() => 166 | Shop.findOne({ where: { domain: shop } }).then(shopObject => { 167 | expect(shopObject).not.toBeNull(); 168 | }) 169 | )); 170 | 171 | it('should attach the Shopify API object to authenticated request and be able to make API calls', () => 172 | callAuthCallback().then(() => { 173 | expect(ShopifyApi).toHaveBeenCalledWith({ 174 | shopName, 175 | accessToken 176 | }); 177 | 178 | const orders = [{ name: 'order1', id: 1 }]; 179 | shopifyApi.order.list = jest.fn(() => Promise.resolve(orders)); 180 | 181 | const newReq = request(app).get('/api/orders'); 182 | newReq.cookies = Cookies; 183 | 184 | return newReq.expect(200).then(res => { 185 | expect(res.body).toEqual(orders); 186 | }); 187 | })); 188 | 189 | it('should create an uninstall webhook after authentication and delete the shop on uninstall', () => { 190 | const webhook = { 191 | topic: 'app/uninstalled', 192 | address: `${APP_URL}${UNINSTALL_ROUTE}`, 193 | format: 'json' 194 | }; 195 | 196 | shopifyApi.webhook.create = jest.fn(() => Promise.resolve()); 197 | 198 | return callAuthCallback().then(() => { 199 | expect(shopifyApi.webhook.create).toHaveBeenCalledWith(webhook); 200 | 201 | const hmac = crypto 202 | .createHmac('SHA256', SHOPIFY_API_SECRET) 203 | .update(JSON.stringify({ shop })) 204 | .digest('base64'); 205 | 206 | return request(app) 207 | .post(UNINSTALL_ROUTE) 208 | .set('x-shopify-hmac-sha256', hmac) 209 | .send({ 210 | shop 211 | }) 212 | .expect(200) 213 | .then( 214 | () => 215 | expect(Shop.findOne({ where: { domain: shop } })).resolves 216 | .toBeNull 217 | ); 218 | }); 219 | }); 220 | 221 | it('should return 401 for uninstall webhook when HMAC validation fails', () => 222 | callAuthCallback().then(() => 223 | request(app).post(UNINSTALL_ROUTE).send({}).expect(401) 224 | )); 225 | 226 | it('should return 500 if fetching the access token fails', () => { 227 | shopifyToken.getAccessToken = jest.fn(() => Promise.reject({})); 228 | 229 | return callAuthCallback().expect(500); 230 | }); 231 | 232 | it('should create a new reccuring charge when none is active and redirect to confirmation URL', () => 233 | callAuthCallback().then(expectChargeConfirmationRedirect)); 234 | 235 | it('should redirect to app home when there is an active recurring application charge', () => { 236 | recurringApplicationCharge.list = () => 237 | Promise.resolve([{ status: 'active' }]); 238 | 239 | return login(shop).then(() => 240 | callAuthCallback().expect('Location', appHome) 241 | ); 242 | }); 243 | 244 | it('should return 500 when creating a recurring charge fails', () => { 245 | recurringApplicationCharge.create = jest.fn(() => Promise.reject({})); 246 | 247 | return callAuthCallback().expect(500); 248 | }); 249 | 250 | it('should clear the session after logout and redirect to apps home page', () => 251 | callAuthCallback().then(() => { 252 | let req = request(app).get('/logout'); 253 | req.cookies = Cookies; 254 | return req.expect('Location', appsList).then(() => { 255 | req = request(app).get('/api/orders'); 256 | req.cookies = Cookies; 257 | return req.expect('Location', INSTALL_PAGE); 258 | }); 259 | })); 260 | }); 261 | 262 | describe('Billing', () => { 263 | beforeEach(() => seedDatabase().then(() => login(shop))); 264 | 265 | it("should check if there's an active reccuring charge when home page is accessed and let the request through if so", () => { 266 | recurringApplicationCharge.list = () => 267 | Promise.resolve([{ status: 'active' }]); 268 | 269 | return getRequest(APP_HOME_ROUTE).expect('Location', '/'); 270 | }); 271 | 272 | it('should create a new recurring charge when the app is accessed and there is no active charge', () => 273 | getRequest(APP_HOME_ROUTE).then(expectChargeConfirmationRedirect)); 274 | 275 | it('should activate the charge if it has been accepted and route to app home', () => { 276 | const chargeId = 'id_001'; 277 | recurringApplicationCharge.get = jest.fn(() => 278 | Promise.resolve({ status: 'accepted' }) 279 | ); 280 | recurringApplicationCharge.activate = jest.fn(() => Promise.resolve()); 281 | 282 | return getRequest(`${ACTIVATE_CHARGE_ROUTE}?charge_id=${chargeId}`) 283 | .expect('Location', appHome) 284 | .then(() => { 285 | expect(recurringApplicationCharge.get).toHaveBeenCalledWith(chargeId); 286 | expect(recurringApplicationCharge.activate).toHaveBeenCalled(); 287 | }); 288 | }); 289 | 290 | it('should return 500 if getting the charge fails', () => { 291 | recurringApplicationCharge.get = jest.fn(() => Promise.reject({})); 292 | 293 | return getRequest(ACTIVATE_CHARGE_ROUTE).expect(500); 294 | }); 295 | 296 | it('should return 500 if activating the charge fails', () => { 297 | recurringApplicationCharge.get = jest.fn(() => 298 | Promise.resolve({ status: 'accepted' }) 299 | ); 300 | recurringApplicationCharge.activate = jest.fn(() => Promise.reject({})); 301 | 302 | return getRequest(ACTIVATE_CHARGE_ROUTE).expect(500); 303 | }); 304 | 305 | it('should return 401 if the merchant declines the charge', () => { 306 | recurringApplicationCharge.get = jest.fn(() => 307 | Promise.resolve({ status: 'declined' }) 308 | ); 309 | 310 | return getRequest(ACTIVATE_CHARGE_ROUTE).expect(401); 311 | }); 312 | }); 313 | 314 | it('should check if the session is still when home page is accessed and redirect to authorization if not', () => { 315 | shopifyApi.shop.get = () => Promise.reject({}); 316 | 317 | return login(shop).then(() => 318 | getRequest(`${APP_HOME_ROUTE}?shop=${shop}`).then( 319 | expectShopifyAuthorizationRedirect 320 | ) 321 | ); 322 | }); 323 | }); 324 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import app from './app'; 3 | 4 | /** 5 | * Get port from environment and store in Express. 6 | */ 7 | const port = process.env.PORT || '3001'; 8 | app.set('port', port); 9 | 10 | /** 11 | * Create HTTP server. 12 | */ 13 | const server = http.createServer(app); 14 | 15 | /** 16 | * Listen on provided port, on all network interfaces. 17 | */ 18 | server.listen(port); 19 | 20 | console.log('--------------------------'); 21 | console.log('===> 😊 Starting Server . . .'); 22 | console.log(`===> Environment: ${process.env.NODE_ENV}`); 23 | console.log(`===> Listening on port: ${app.get('port')}`); 24 | console.log('--------------------------'); 25 | 26 | /** 27 | * Event listener for HTTP server "error" event. 28 | */ 29 | function onError(error) { 30 | if (error.syscall !== 'listen') { 31 | throw error; 32 | } 33 | 34 | const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`; 35 | 36 | // handle specific listen errors with friendly messages 37 | switch (error.code) { 38 | case 'EACCES': 39 | console.error(`${bind} requires elevated privileges`); 40 | process.exit(1); 41 | break; 42 | case 'EADDRINUSE': 43 | console.error(`${bind} is already in use`); 44 | process.exit(1); 45 | break; 46 | default: 47 | throw error; 48 | } 49 | } 50 | 51 | /** 52 | * Event listener for HTTP server "listening" event. 53 | */ 54 | 55 | function onListening() { 56 | const addr = server.address(); 57 | const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`; 58 | console.log(`Listening on ${bind}`); 59 | } 60 | 61 | server.on('error', onError); 62 | server.on('listening', onListening); 63 | -------------------------------------------------------------------------------- /server/views/500.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title We're sorry, but something went wrong (500) 5 | meta(name='viewport', content='width=device-width,initial-scale=1') 6 | link(rel='stylesheet', href='https://sdks.shopifycdn.com/polaris/latest/polaris.css') 7 | body 8 | .Polaris-Banner.Polaris-Banner--statusWarning(tabindex='0', role='banner warning', aria-describedby='Banner11Content', aria-labelledby='Banner11Heading') 9 | .Polaris-Banner__Ribbon 10 | span.Polaris-Icon.Polaris-Icon--colorYellowDark.Polaris-Icon--hasBackdrop 11 | svg.Polaris-Icon__Svg(viewbox='0 0 20 20') 12 | g(fill-rule='evenodd') 13 | circle(fill='currentColor', cx='10', cy='10', r='9') 14 | path(d='M10 0C4.486 0 0 4.486 0 10s4.486 10 10 10 10-4.486 10-10S15.514 0 10 0m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8m0-13a1 1 0 0 0-1 1v4a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1m0 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2') 15 | div 16 | #Banner11Heading.Polaris-Banner__Heading 17 | p.Polaris-Heading We're sorry, but something went wrong. 18 | #Banner11Content.Polaris- 19 | if error 20 | span #{error} 21 | else 22 | span Please try accessing the app again. If the problem persists, contact our customer support. 23 | .Polaris-Banner__Actions 24 | a.Polaris-Link(href=APP_URL) 25 | | Return to app home 26 | -------------------------------------------------------------------------------- /server/views/charge_declined.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Application charge declined 5 | meta(name='viewport', content='width=device-width,initial-scale=1') 6 | link(rel='stylesheet', href='https://sdks.shopifycdn.com/polaris/latest/polaris.css') 7 | body 8 | .Polaris-Banner.Polaris-Banner(tabindex='0', role='banner warning', aria-describedby='Banner11Content', aria-labelledby='Banner11Heading') 9 | .Polaris-Banner__Ribbon 10 | span.Polaris-Icon.Polaris-Icon--colorInk.Polaris-Icon--hasBackdrop 11 | svg.Polaris-Icon__Svg(viewbox='0 0 20 20') 12 | g(fill-rule='evenodd') 13 | path(d='M1.65 18.329a2.22 2.22 0 0 0 2.33.517L13 14 6 7l-4.867 9a2.22 2.22 0 0 0 .517 2.329', fill='currentColor') 14 | path(d='M6.707 6.293a1.003 1.003 0 0 0-1.587.232l-4.866 9a.986.986 0 0 0-.06.133 3.23 3.23 0 0 0 .75 3.378 3.23 3.23 0 0 0 3.377.749c.045-.016.09-.036.132-.058l9.021-4.846a.998.998 0 0 0 .233-1.588l-7-7zM3.58 17.926a1.222 1.222 0 0 1-1.525-1.524l4.187-7.746 5.101 5.1-7.763 4.17zM15 4a1 1 0 0 1 1 1 1 1 0 1 0 2 0 1 1 0 0 1 1-1 1 1 0 1 0 0-2 1 1 0 0 1-1-1 1 1 0 1 0-2 0 1 1 0 0 1-1 1 1 1 0 1 0 0 2M1 4a1 1 0 0 1 1 1 1 1 0 1 0 2 0 1 1 0 0 1 1-1 1 1 0 1 0 0-2 1 1 0 0 1-1-1 1 1 0 1 0-2 0 1 1 0 0 1-1 1 1 1 0 1 0 0 2m18 12a1 1 0 0 1-1-1 1 1 0 1 0-2 0 1 1 0 0 1-1 1 1 1 0 1 0 0 2 1 1 0 0 1 1 1 1 1 0 1 0 2 0 1 1 0 0 1 1-1 1 1 0 1 0 0-2m-7-7a.997.997 0 0 0 .707-.293l1-1a.999.999 0 1 0-1.414-1.414l-1 1A.999.999 0 0 0 12 9m6.684.052l-3 1a1 1 0 1 0 .633 1.896l3-1a1 1 0 1 0-.633-1.896m-10-4.104a1.001 1.001 0 0 0 1.265-.632l1-3A1 1 0 0 0 9.052.683l-1 3a1 1 0 0 0 .632 1.265') 15 | div 16 | #Banner11Heading.Polaris-Banner__Heading 17 | p.Polaris-Heading This app requires an active recurring charge 18 | #Banner11Content.Polaris- 19 | span To start using it, please go back and accept the terms. 20 | .Polaris-Banner__Actions 21 | a.Polaris-Link(href=APP_URL) 22 | | Return to app home 23 | -------------------------------------------------------------------------------- /server/views/shopify_redirect.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | base(target='_top') 6 | title Redirecting… 7 | script(type='text/javascript'). 8 | // If the current window is the 'parent', change the URL by setting location.href 9 | if (window.top == window.self) { 10 | window.top.location.href = "#{url}"; 11 | // If the current window is the 'child', change the parent's URL with postMessage 12 | } else { 13 | message = JSON.stringify({ 14 | message: 'Shopify.API.remoteRedirect', 15 | data: { location: '#{url}' } 16 | }); 17 | window.parent.postMessage(message, 'https://#{shop}'); 18 | } 19 | body -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | const isProduction = process.env.NODE_ENV === 'production'; 6 | 7 | const CURRENT_WORKING_DIR = process.cwd(); 8 | 9 | const PATHS = { 10 | app: path.resolve(CURRENT_WORKING_DIR), 11 | assets: path.resolve(CURRENT_WORKING_DIR, 'public', 'assets'), 12 | compiled: path.resolve(CURRENT_WORKING_DIR, 'compiled'), 13 | public: '/assets/', 14 | modules: path.resolve(CURRENT_WORKING_DIR, 'node_modules'), 15 | }; 16 | 17 | const node = { __dirname: true, __filename: true }; 18 | 19 | const externals = fs 20 | .readdirSync('node_modules') 21 | .filter(x => ['.bin'].indexOf(x) === -1) 22 | .reduce((acc, cur) => Object.assign(acc, { [cur]: `commonjs ${cur}` }), {}); 23 | 24 | const resolve = { 25 | modules: [PATHS.app, PATHS.modules], 26 | extensions: ['.js', '.jsx', '.css'], 27 | }; 28 | 29 | const bannerOptions = { 30 | raw: true, 31 | banner: 'require("source-map-support").install();', 32 | }; 33 | const compileTimeConstantForMinification = { 34 | __PRODUCTION__: JSON.stringify(isProduction), 35 | }; 36 | 37 | const developmentPlugins = [ 38 | new webpack.EnvironmentPlugin(['NODE_ENV']), 39 | new webpack.DefinePlugin(compileTimeConstantForMinification), 40 | new webpack.BannerPlugin(bannerOptions), 41 | ]; 42 | 43 | const productionPlugins = [ 44 | ...developmentPlugins, 45 | new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }), 46 | ]; 47 | 48 | const plugins = isProduction ? productionPlugins : developmentPlugins; 49 | 50 | const config = { 51 | devtool: 'sourcemap', 52 | context: PATHS.app, 53 | entry: { server: 'server/server.js' }, 54 | target: 'node', 55 | node, 56 | externals, 57 | output: { 58 | path: PATHS.compiled, 59 | filename: isProduction ? '[name].js' : '[name].dev.js', 60 | publicPath: PATHS.public, 61 | libraryTarget: 'commonjs2', 62 | }, 63 | module: { 64 | rules: [ 65 | { 66 | test: /\.js$|\.jsx$/, 67 | loader: 'babel-loader', 68 | options: { 69 | presets: ['es2015', 'stage-0'], 70 | }, 71 | exclude: PATHS.modules, 72 | }, 73 | ], 74 | }, 75 | resolve, 76 | plugins, 77 | }; 78 | 79 | module.exports = config; 80 | --------------------------------------------------------------------------------