├── .eslintrc.js ├── .gitignore ├── CHANGELOG.md ├── LICENCE.md ├── README.md ├── TODO ├── app.js ├── config ├── development.js ├── index.js └── production.js ├── docs ├── .gitignore ├── Gemfile ├── _config.yml ├── _data │ └── common.yml ├── _documentation │ ├── about.md │ ├── application-scope.md │ ├── configuration.md │ ├── getting-started-configuration.md │ ├── getting-started.md │ ├── helper-functions.md │ ├── mongo.md │ ├── ngrok.md │ ├── routes.md │ ├── running.md │ ├── shopify-partner-dashboard.md │ ├── utils.md │ └── views.md ├── _includes │ ├── article.html │ ├── aside.html │ ├── footer.html │ ├── head.html │ ├── header.html │ ├── icon.html │ └── main.html ├── _layouts │ └── home.html ├── _sass │ ├── _additions.scss │ ├── _form.scss │ ├── _grid.scss │ ├── _header.scss │ ├── _helpers.scss │ ├── _icons.scss │ ├── _popups.scss │ ├── _syntax.scss │ └── _typography.scss ├── about.md ├── assets │ ├── bg-layer-1.png │ ├── bg-layer-1.svg │ ├── bg-layer-2.png │ ├── bg-layer-2.svg │ ├── favicon.png │ ├── js │ │ ├── focus.js │ │ ├── scroll-spy.js │ │ └── smooth-scroll.js │ ├── main.scss │ ├── ngrok-init.png │ └── partner-dash-settings.png └── index.md ├── helpers └── index.js ├── middleware └── index.js ├── models ├── Counter.js ├── Shop.js └── index.js ├── package-lock.json ├── package.json ├── public └── stylesheets │ └── style.css ├── routes ├── api.js ├── index.js ├── install.js ├── proxy.js └── webhook.js ├── start.js ├── utils ├── cleardbs.js ├── index.js └── seeddbs.js └── views ├── app └── app.hbs ├── error.hbs ├── index.hbs └── layout.hbs /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import" 5 | ] 6 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | .DS_Store 4 | yarn.lock 5 | .idea/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Shopify Node App V0.2 2 | = 3 | 4 | 10/07/2017 5 | -- 6 | - Moved from ./bin/www to ./start.js for start system. 7 | - Models now need to be referenced in start.js to be initialised. 8 | - Can call models using mongoose.model('ModelName') instead of requiring them in each file. individually. 9 | - General bug fixes -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Elkfox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ IMPORTANT NOTICE 2 | 3 | This project is no longer supported. We recommend quickly generating the structure of your Shopify App projects using the [Shopify App CLI](https://shopify.github.io/shopify-app-cli/) instead. 4 | 5 | ------------------------ 6 | 7 | ## Shopify Node App 8 | 9 | Shopify Node App - A Shopify App framework built on Node Express and Mongo 10 | 11 | #### View the [Documentation](https://elkfox.github.io/shopify-node-app/) for more 12 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Implement promises -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const favicon = require('serve-favicon'); 4 | const logger = require('morgan'); 5 | const cookieParser = require('cookie-parser'); 6 | const bodyParser = require('body-parser'); 7 | const session = require('express-session'); 8 | const mongoose = require('mongoose'); 9 | const MongoStore = require('connect-mongo')(session); 10 | 11 | // Routes 12 | const index = require('./routes/index'); 13 | const install = require('./routes/install'); 14 | const webhook = require('./routes/webhook'); 15 | const proxy = require('./routes/proxy'); 16 | const api = require('./routes/api'); 17 | require('dotenv').config(); 18 | 19 | const app = express(); 20 | 21 | // view engine setup 22 | app.set('views', path.join(__dirname, 'views')); 23 | app.set('view engine', 'hbs'); 24 | app.use(bodyParser.json({ 25 | type:'application/json', 26 | limit: '50mb', 27 | verify: function(req, res, buf) { 28 | if (req.url.startsWith('/webhook')){ 29 | req.rawbody = buf; 30 | } 31 | } 32 | }) 33 | ); 34 | // uncomment after placing your favicon in /public 35 | // app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 36 | app.use(logger('dev')); 37 | app.use(bodyParser.urlencoded({ extended: false })); 38 | app.use(cookieParser()); 39 | app.use(express.static(path.join(__dirname, 'public'))); 40 | app.set('trust proxy', 1); 41 | app.use(session({ 42 | name: 'ShopifyNodeApp', 43 | secret: process.env.SESSION_SECRET || 'coocoocachoo', 44 | cookie: { secure: true, maxAge: (24 * 60 * 60 * 1000) }, 45 | saveUninitialized: true, 46 | resave: false, 47 | store: new MongoStore({ mongooseConnection: mongoose.connection }), 48 | })); 49 | 50 | app.use(express.static(path.join(__dirname, 'public'))); 51 | // Routes 52 | app.use('/', index); 53 | app.use('/install', install); 54 | app.use('/webhook', webhook); 55 | app.use('/proxy', proxy); 56 | app.use('/api', api); 57 | 58 | // catch 404 and forward to error handler 59 | app.use((req, res, next) => { 60 | const err = new Error('Not Found'); 61 | err.status = 404; 62 | next(err); 63 | }); 64 | 65 | // error handler 66 | app.use((err, req, res) => { 67 | // set locals, only providing error in development 68 | res.locals.message = err.message; 69 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 70 | 71 | // render the error page 72 | res.status(err.status || 500); 73 | res.render('error'); 74 | }); 75 | 76 | module.exports = app; 77 | -------------------------------------------------------------------------------- /config/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | APP_URI: 'https://e1e16fdc.ngrok.io', 3 | }; 4 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV; 2 | const production = require('./production'); 3 | const development = require('./development'); 4 | 5 | // You should put any global variables in here. 6 | const config = { 7 | SHOPIFY_API_KEY: process.env.SHOPIFY_API_KEY || '', 8 | SHOPIFY_SHARED_SECRET: process.env.SHOPIFY_SHARED_SECRET || '', 9 | APP_NAME: 'Customer', 10 | APP_STORE_NAME: 'Customer', 11 | APP_SCOPE: 'read_products,write_products,read_customers,write_customers', 12 | DATABASE_NAME: 'shopify_node_app', 13 | }; 14 | 15 | if (env !== 'PRODUCTION') { 16 | module.exports = Object.assign({}, config, development); 17 | } else { 18 | module.exports = Object.assign({}, config, production); 19 | } 20 | -------------------------------------------------------------------------------- /config/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | APP_URI: '', 3 | }; 4 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | Gemfile.lock 5 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby RUBY_VERSION 3 | 4 | # Hello! This is where you manage which Jekyll version is used to run. 5 | # When you want to use a different version, change it below, save the 6 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 7 | # 8 | # bundle exec jekyll serve 9 | # 10 | # This will help ensure the proper Jekyll version is running. 11 | # Happy Jekylling! 12 | 13 | gem "jekyll", "3.4.1" 14 | 15 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 16 | gem "minima", "~> 2.0" 17 | 18 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 19 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 20 | # gem "github-pages", group: :jekyll_plugins 21 | 22 | # If you have any plugins, put them here! 23 | group :jekyll_plugins do 24 | gem "jekyll-feed", "~> 0.6" 25 | end 26 | 27 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 28 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 29 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Meta ======================================================================= 2 | title: Shopify Node App - A Shopify App framework built on Node Express and Mongo 3 | description: A Shopify App framework built on Node Express and Mongo. Includes Embedded app SDK and full tutorial. 4 | # Site ======================================================================= 5 | name: Shopify Node App 6 | subtitle: A thousand hour headstart in building a Shopify Node App 7 | github_url: https://github.com/Elkfox/Shopify-Node-App 8 | download_url: https://github.com/Elkfox/Shopify-Node-App/archive/master.zip 9 | # demo_url: https://concrete-theme.myshopify.com 10 | # Navigation ================================================================= 11 | nav: 12 | - title: About 13 | url: '#about' 14 | - title: 'Getting Started' 15 | url: '#getting-started' 16 | childlinks: 17 | - title: Mongo Setup 18 | url: '#mongo-setup' 19 | - title: Ngrok 20 | url: '#ngrok' 21 | - title: Shopify Partner Dashboard 22 | url: '#shopify-partner-dashboard' 23 | - title: Configuration 24 | url: '#getting-started-configuration' 25 | - title: 'Configuration' 26 | url: '#configuration' 27 | - title: 'Running' 28 | url: '#running' 29 | - title: 'Utils' 30 | url: '#utils' 31 | - title: 'Views' 32 | url: '#views' 33 | - title: 'Routes' 34 | url: '#routes' 35 | - title: 'Applcation Scope' 36 | url: '#application-scope' 37 | - title: 'Helper Functions' 38 | url: '#helper-functions' 39 | 40 | # Collections ================================================================ 41 | collections: 42 | documentation: 43 | title: Documentation 44 | # Build settings ============================================================= 45 | markdown: kramdown 46 | theme: minima 47 | gems: 48 | - jekyll-feed 49 | exclude: 50 | - Gemfile 51 | - Gemfile.lock 52 | -------------------------------------------------------------------------------- /docs/_data/common.yml: -------------------------------------------------------------------------------- 1 | icon: Icon 2 | -------------------------------------------------------------------------------- /docs/_documentation/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "About" 3 | handle: "about" 4 | category: "about" 5 | --- 6 | 7 | Dummy Public App for Shopify built with Node Includes Embedded App SDK [Elkfox](https://www.elkfox.com). 8 | 9 | ### Requirements 10 | 11 | - node.js >= 6.11.0 12 | - mongodb >= 3.2.9 13 | - npm >= 5.0.0 14 | -------------------------------------------------------------------------------- /docs/_documentation/application-scope.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Application Scope" 3 | handle: "application-scope" 4 | category: "application-scope" 5 | --- 6 | 7 | When you develop an app you must tell Shopify and the store owner what parts of the store you want to access and modify. 8 | 9 | To declare application scope you can edit the APP_SCOPE variable in `config/index.js` by listing your scopes seperated by commas. 10 | 11 | ### Available scopes 12 | These are the available scopes for your application 13 | 14 | Scope | Description 15 | -------|------------- 16 | read_content, write_content | Access to Article, Blog, Comment, Page, and Redirect. 17 | read_themes, write_themes | Access to Asset and Theme. 18 | read_products, write_products | Access to Product, product variant, Product Image, Collect, Custom Collection, and Smart Collection. 19 | read_customers, write_customers | Access to Customer and Saved Search. 20 | read_orders, write_orders | Access to Order, Transaction and Fulfillment. 21 | read_draft_orders, write_draft_orders | Access to Draft Order. 22 | read_script_tags, write_script_tags | Access to Script Tag. 23 | read_fulfillments, write_fulfillments | Access to Fulfillment Service. 24 | read_shipping, write_shipping | Access to Carrier Service. 25 | read_analytics | Access to Analytics API. 26 | read_users, write_users | Access to User **SHOPIFY PLUS**. 27 | read_checkouts, write_checkouts | Access to Checkouts. 28 | -------------------------------------------------------------------------------- /docs/_documentation/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Configuration" 3 | handle: "configuration" 4 | category: "configuration" 5 | --- 6 | 7 | Inside the `config/` directory there are three files 8 | - development.js - Development only configuration variables 9 | - index.js - Global configuration variables 10 | - production.js - Production only configuration variables 11 | 12 | This is where you'll place any configuration variables you'll need or any references you'll need to environment variables later on. 13 | When the app is initalised it checks where `process.env.NODE_ENV` is set to production or not and will load the relevant configurations. 14 | 15 | Default configuration located in `config/index.js` variables are: 16 | - SHOPIFY\_API\_KEY - Your apps API Key. Generated when you set up the app and required to run and install the app 17 | - SHOPIFY\_SHARED\_SECRET - Your apps secret key. Generated when you set up the app and required to run and install the app. 18 | - APP_NAME - The name of your app. Can be left blank if you'd prefer to hardcode it in. 19 | - APP_SCOPE - The parts of the Shopify API your app will want access to. See [below](#scopes) for a list of possible scopes. This is required to install the app. You must have at least one scope permission. 20 | -------------------------------------------------------------------------------- /docs/_documentation/getting-started-configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Configuration" 3 | handle: "getting-started-configuration" 4 | category: "getting-started-configuration" 5 | --- 6 | 7 | If you haven't done so already fork and clone [the repository](https://github.com/Elkfox/shopify-node-app) 8 | 9 | And install all the required Node packages 10 | 11 | {% highlight bash %} 12 | npm install 13 | {% endhighlight %} 14 | 15 | 16 | In the config directory we store the variables for the different environments. Production refers to the live environment and development is the local. index.js is the global variables with the current environment. 17 | 18 | Go into config/development.js and change the APP_URI to the your ngrok forwarding address. In my case it's https://32c49948.ngrok.io 19 | 20 | Now ensure that you have .env in your .gitignore because we will be storing sensitive information in this file such as our API secret. if it isn't add it now. 21 | 22 | Now create a new file in your app root called .env 23 | and add the API credentials we created earlier in the Shopify Partner Dashboard. It should look like this: 24 | 25 | {% highlight conf %} 26 | SHOPIFY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 27 | SHOPIFY_SHARED_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 28 | {% endhighlight %} 29 | 30 | If you want to use port other than 7777 you can add that now too. 31 | 32 | {% highlight conf %} 33 | PORT=3000 34 | {% endhighlight %} 35 | 36 | Okay now lets get our app running! 37 | 38 | {% highlight bash %} 39 | npm start 40 | {% endhighlight %} 41 | 42 | You should now be able to install your app if you visit the installation url. 43 | 44 | My Shopify store url is `hello-world.myshopify.com` 45 | and my ngrok forwarding address is `https://32c49948.ngrok.io/`` 46 | therefore my installation url is 47 | `https://32c49948.ngrok.io/?shop=hello-world` 48 | 49 | Congratulations 🙌 50 | -------------------------------------------------------------------------------- /docs/_documentation/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting Started" 3 | handle: "getting-started" 4 | category: "getting-started" 5 | --- 6 | 7 | ### Introduction 8 | This Getting Started section has been written in a tutorial format so that it can be understood by everyone of all skill levels. 9 | That being said a prior knowledge of the terminal and javascript will be required. If you have any questions at all don't hesitate to raise an issue on Github. 10 | 11 | This tutorial makes the assumption that you are using a unix based operating system (Mac/Linux). 12 | -------------------------------------------------------------------------------- /docs/_documentation/helper-functions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Helper Functions" 3 | handle: "helper-functions" 4 | category: "helper-functions" 5 | --- 6 | 7 | There are a number of helper functions available in `helpers/index.js` these make tasks that might need to be repeated multiple times simple and quick to save time. 8 | 9 | | Function | Arguments | Returns | Description | 10 | | -------- | --------- | ------- | ----------- | 11 | | openWebhook | shop(Shop) | Accepts a Shop object from mongoose and returns a new ShopifyAPI session. | 12 | | buildWebhook | topic (String)
address(String, default: APP_URI/webhook)
shop(ShopifyAPI)
callback(err, data, headers) | callback \|\| Boolean | Creates a new webhook on the Shop you're currently working on. Once complete it fires the callback passed to it. | 13 | | generateNonce | length(Int, default: 8) | String |Generates a random string of characters to represent a nonce that meets Shopify's requirements for app installation | 14 | | verifyHmac | data (String)
hmac (String) | Boolean |Generates a hash from the passed data and compares it to the hash sent by Shopify. Returns true or false | 15 | | verifyOAuth | query (Object) | Boolean | Takes a request query and checks to see if it's a request from Shopify. Returns true or false | 16 | -------------------------------------------------------------------------------- /docs/_documentation/mongo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Mongo Setup" 3 | handle: "mongo-setup" 4 | category: "mongo-setup" 5 | --- 6 | 7 | First lets start by opening your terminal and checking to see if we have an installation of by typing in 8 | 9 | {% highlight bash %} 10 | mongod 11 | {% endhighlight %} 12 | 13 | If you do not have Mongo installed the command will not be found. If you do you can go ahead to the Ngrok section. 14 | 15 | Getting started with Mongo is simple click [here to find the instructions for your operating system](https://docs.mongodb.com/manual/administration/install-community/) 16 | 17 | #### Quick guide to install Mongo for Mac users 18 | I recommend using Brew to get started quickly. 19 | 20 | First update Brew. 21 | 22 | {% highlight bash %} 23 | brew update 24 | {% endhighlight %} 25 | 26 | Then install MongoDB 27 | {% highlight bash %} 28 | brew install mongodb 29 | {% endhighlight %} 30 | 31 | Then create the directory where your database will be stored. 32 | {% highlight bash %} 33 | mkdir -p /data/db 34 | {% endhighlight %} 35 | 36 | ##### Important 37 | Before running mongod for the first time, ensure that the user account running mongod has read and write permissions for the data directory. It is not advised to run mongod as root user i.e don't use sudo, setup the permissions correctly using the following command. 38 | 39 | {% highlight bash %} 40 | sudo chown -R `id -un` /data/db 41 | {% endhighlight %} 42 | 43 | Now you should be able to run the Mongo Daemon. 44 | 45 | {% highlight bash %} 46 | mongod 47 | {% endhighlight %} 48 | -------------------------------------------------------------------------------- /docs/_documentation/ngrok.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Ngrok" 3 | handle: "ngrok" 4 | category: "ngrok" 5 | --- 6 | 7 | Your app will be receiving requests from Shopify, but you want to be able to develop your app locally. This is where Ngrok comes in. Ngrok is a tunneling service which will allow you to safely expose your local app to the internet so that it can now communicate with Shopify. 8 | 9 | Download Ngrok 10 | 11 | Assuming you downloaded it to your downloads folder unzip ngrok 12 | 13 | {% highlight bash %} 14 | unzip Downloads/ngrok.zip 15 | {% endhighlight %} 16 | 17 | {% highlight bash %} 18 | cd Downloads 19 | unzip ./ngrok.zip 20 | {% endhighlight %} 21 | 22 | Then run ngrok on port 7777 23 | 24 | {% highlight bash %} 25 | ngrok http 7777 26 | {% endhighlight %} 27 | 28 | If all went well you will see a response like this 29 | 30 | 31 | but the localhost port will be 7777 instead. 32 | 33 | In this case the Ngrok forwarding address is: https://32c49948.ngrok.io 34 | 35 | This is the url that will be used for reference in the rest of this tutorial. If the app were running you could now visit this url in your browser to view the app. 36 | 37 | ##### Note 38 | Your ngrok forwarding address changes every time that you restart ngrok (unless you have a paid plan). To avoid OAuth failures, update your app settings in the Partner Dashboard whenever you restart ngrok. 39 | -------------------------------------------------------------------------------- /docs/_documentation/routes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Routes" 3 | handle: "routes" 4 | category: "routes" 5 | --- 6 | 7 | Routes can be found in the `routes/` folder in the root directory of the app. 8 | 9 | | File | Route | Description | 10 | | ---- | ----- | ----------- | 11 | | `index.js` | `/` | Any non-app related endpoints should go here (General website endpoints such as /about, /contact, etc). | 12 | | `install.js` | `/install/` | Where you should place any methods relevant to the installation of you app. This is also where you should set up any 'post install' methods such as setting up webhooks or adding files to themes.| 13 | | `webhooks.js` | `/webhooks/` | Where you should place any requests to webhooks. This includes a middleware that checks to see if a webhook has made a legitemate request from Shopify.| 14 | | `api.js` | `/api/` | Where you should place any api endpoints you wish to use for your app if you plan on having front end components.| 15 | | `proxy.js` | `/proxy/` | Where you should place any proxy related routes. This is handy if you set up an app proxy when setting up your app. This will serve any files sent from it as liquid which allows Shopify to pass liquid objects to the files and allows you to use liquid inside your templates. | 16 | -------------------------------------------------------------------------------- /docs/_documentation/running.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Running" 3 | handle: "running" 4 | category: "running" 5 | --- 6 | 7 | ### Debug mode 8 | To start the app in debug mode simply run `npm run debug`. This will start the app on port 3000 with debugging turned on. 9 | You can access the app by visiting `localhost:3000` 10 | 11 | Also available is `npm run debugwatch` which will start nodemon and allow you live-restart your server whenever you make changes. 12 | 13 | ### Production mode 14 | You can start the app in production mode by running `npm start` or `npm watch`. 15 | 16 | **Note**: You must set the environment variable `PORT` to whatever port you wish to run your production server on. (80, 8000, 8080, etc) If this is not defined app will run on port 3000 like in development mode. 17 | -------------------------------------------------------------------------------- /docs/_documentation/shopify-partner-dashboard.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Shopify Partner Dashboard" 3 | handle: "shopify-partner-dashboard" 4 | category: "shopify-partner-dashboard" 5 | --- 6 | 7 | Create a new app in your [Partner Dashboard](https://partners.shopify.com/login) using the forwarding address that ngrok creates for you. 8 | 9 | Go to the App info tab and add https://32c49948.ngrok.io/install/callback to the Whitelisted redirection URL(s) **be sure to use the HTTPS url**. 10 | 11 | Add any other relevant whitelisted urls such as https://32c49948.ngrok.io/proxy if you wish to use an app proxy. 12 | 13 | 14 | 15 | **As noted above, unless you have a paid ngrok plan you will need to change this each to you start ngrok to the new url.** 16 | 17 | Below you will see the App credentials section, take note of your API key and and API secret key. You'll use these as environment variables in your app. 18 | -------------------------------------------------------------------------------- /docs/_documentation/utils.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Utils" 3 | handle: "utils" 4 | category: "utils" 5 | --- 6 | There are two util commands available: 7 | 8 | `npm run cleardbs` - Which runs `./utils/cleardbs.js` and removes any entries in the databases you specify. By default it's set up to remove entries from `Shop` and `Counter`. 9 | 10 | `npm run seeddbs` - Seeds databases using your settings inside `./utils/seeddbs.js`. Currently the only database it seeds is Counter. 11 | -------------------------------------------------------------------------------- /docs/_documentation/views.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Views" 3 | handle: "views" 4 | category: "views" 5 | --- 6 | 7 | Node Shopify App uses handlebars as it's template engine. 8 | 9 | Views are located in the `views/` folder and while some use a shared `layout.hbs` file you are under no obligation to use this file and your `.hbs` files can contain it's own html head and body. At the moment views are split into their current folders. The embedded app markup is located inside `apps/app.hbs`. Anything located in the root `views/` folder is for general use while `index.hbs` is what a non-shop should see if they visit `https://` 10 | -------------------------------------------------------------------------------- /docs/_includes/article.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% for link in site.nav %} 4 | {% assign link_handle = link.url | remove: '#' %} 5 | {% for article in site.documentation %} 6 | {% if article.handle == link_handle %} 7 |
8 |

{{ article.title }}

9 | {{ article.content }} 10 | {% if link.childlinks %} 11 | {% for childlink in link.childlinks %} 12 | {% assign childlink_handle = childlink.url | remove: '#' %} 13 | {% for article in site.documentation %} 14 | {% if article.handle == childlink_handle %} 15 |
16 |

{{ article.title }}

17 | {{ article.content }} 18 |
19 | {% endif %} 20 | {% endfor %} 21 | {% endfor %} 22 | {% endif %} 23 |
24 | {% endif %} 25 | {% endfor %} 26 | {% endfor %} 27 | 28 |
29 | -------------------------------------------------------------------------------- /docs/_includes/aside.html: -------------------------------------------------------------------------------- 1 | 22 |
23 |

24 | Made with {% include icon.html icon='heart' icon_size='13' %} by {% include icon.html icon='elkfox' icon_size='35' %} 25 | 26 |

27 |
28 | -------------------------------------------------------------------------------- /docs/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 139 | 140 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{ site.title }} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /docs/_includes/header.html: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 | -------------------------------------------------------------------------------- /docs/_includes/icon.html: -------------------------------------------------------------------------------- 1 | {% assign icon = include.icon %} 2 | {% if include.icon_size %} 3 | {% assign icon_size = include.icon_size %} 4 | {% else %} 5 | {% assign icon_size = '32' %} 6 | {% endif %} 7 | 43 | -------------------------------------------------------------------------------- /docs/_includes/main.html: -------------------------------------------------------------------------------- 1 |
2 | {% include aside.html %} 3 | {% include article.html %} 4 |
5 | -------------------------------------------------------------------------------- /docs/_layouts/home.html: -------------------------------------------------------------------------------- 1 | {% include head.html %} 2 | {% include header.html %} 3 | {% include main.html %} 4 | {% include footer.html %} 5 | -------------------------------------------------------------------------------- /docs/_sass/_additions.scss: -------------------------------------------------------------------------------- 1 | // .parallax { 2 | // transition: background-position-y 0.5s ease-out; 3 | // } 4 | 5 | header { 6 | position: relative; 7 | height: auto; 8 | padding: 0 50px; 9 | 10 | .content { 11 | z-index: 2; 12 | position: relative; 13 | padding: 90px 0 40px; 14 | width: 100%; 15 | top: 50%; 16 | h1 { 17 | line-height: 1.2; 18 | margin-bottom: 0; 19 | @include s() { 20 | font-size: 3rem; 21 | } 22 | } 23 | h2 { 24 | font-size: 1.3rem; 25 | font-weight: 400; 26 | margin-bottom: 0; 27 | color: $colorOne; 28 | } 29 | a { 30 | color: $colorText; 31 | text-decoration: none; 32 | font-size: 0.9rem; 33 | font-weight: 400; 34 | vertical-align: middle; 35 | } 36 | .links { 37 | a { 38 | font-size: 0.8rem; 39 | font-weight: 600; 40 | .icon { 41 | margin-right: $gutter/5; 42 | } 43 | } 44 | } 45 | .button { 46 | background: $colorText; 47 | padding: 0 $gutter/2; 48 | color: $colorBackground; 49 | font-weight: 600; 50 | font-size: 1rem; 51 | display: inline-block; 52 | margin-bottom: $gutter/2.5; 53 | height: 46px; 54 | line-height: 46px; 55 | width: auto; 56 | max-width: 100%; 57 | border: 0; 58 | svg { 59 | margin-right: $gutter/5; 60 | .fill { 61 | fill: $colorBackground; 62 | } 63 | .stroke { 64 | stroke: $colorBackground; 65 | } 66 | } 67 | 68 | } 69 | svg .fill { 70 | fill: $colorWhite; 71 | } 72 | svg.github, svg.shopify, svg.heart { 73 | height: 0.9rem; 74 | width: 0.9rem; 75 | } 76 | svg.shopify { 77 | margin-left: 0.75rem; 78 | @include s() { 79 | margin-left: 0; 80 | } 81 | } 82 | svg.elkfox { 83 | height: 2rem; 84 | width: auto; 85 | } 86 | } 87 | } 88 | 89 | aside.sidebar { 90 | position: relative; 91 | padding: 50px; 92 | li { 93 | margin-bottom: 0; 94 | } 95 | div.fixed { 96 | position: fixed; 97 | top: $gutter; 98 | height: 100%; 99 | } 100 | ul.childlinks { 101 | margin-bottom: 0px; 102 | margin-left: $gutter/4; 103 | display: none; 104 | &.childView { 105 | display: block; 106 | } 107 | a { 108 | font-size: 0.8em; 109 | } 110 | } 111 | } 112 | 113 | article.documentation { 114 | padding: $gutter; 115 | @include m() { 116 | padding: $gutter/2; 117 | } 118 | @include s() { 119 | padding: $gutter/4; 120 | } 121 | h2:first-of-type { 122 | margin-top: 0; 123 | } 124 | } 125 | 126 | .credit { 127 | position: fixed; 128 | bottom: 0px; 129 | background-color: $colorBackground; 130 | bottom: 0px; 131 | left: 0px; 132 | padding-left: $gutter; 133 | z-index: 10; 134 | @include m() { 135 | padding-left: $gutter/2; 136 | } 137 | @include s() { 138 | padding-left: $gutter/4; 139 | } 140 | p { 141 | margin-bottom: 0; 142 | } 143 | a { 144 | text-decoration: none; 145 | font-weight: $fontBold; 146 | font-size: 0.8rem; 147 | } 148 | .icon svg.elkfox .fill { 149 | fill: $colorText; 150 | } 151 | } 152 | .demo-wrapper { 153 | margin-bottom: $gutter; 154 | } 155 | .demo-grid { 156 | background-color: rgba(0,0,0,0.2); 157 | border-style: solid; 158 | border-width: 1px; 159 | border-color: $colorEight; 160 | font-family: Menlo, monospace; 161 | font-weight: 900; 162 | .demo-grid { 163 | border-color: $colorOne; 164 | .demo-grid { 165 | border-color: $colorThree; 166 | } 167 | } 168 | } 169 | // Overrides 170 | .icon { 171 | line-height: 1; 172 | } 173 | -------------------------------------------------------------------------------- /docs/_sass/_form.scss: -------------------------------------------------------------------------------- 1 | // Forms 2 | form { 3 | max-width: 100%; 4 | } 5 | input[type="text"], 6 | input[type="email"], 7 | input[type="number"], 8 | input[type="tel"], 9 | input[type="password"], 10 | input[type="search"], 11 | textarea { 12 | max-width: 100%; 13 | height: $gutter; 14 | line-height: ($gutter - $borderWeight*2); 15 | padding: 0 $gutter/2; 16 | font-size: 1.125rem; 17 | border: $borderWeight $borderStyle $colorTwo; 18 | border-radius: $borderRadius; 19 | -webkit-appearance: none; 20 | @media only screen and (max-width: $s) { 21 | width: 100%; 22 | clear: both; 23 | margin-bottom: $gutter/3; 24 | } 25 | &.error { 26 | border-color: $colorThree; 27 | } 28 | } 29 | textarea { 30 | height: initial; 31 | line-height: $lineHeight; 32 | padding: $gutter/2; 33 | font-family: inherit; // for Firefox 34 | } 35 | label { 36 | line-height: inherit; 37 | } 38 | button, .button, 39 | input[type="button"], 40 | input[type="submit"], 41 | input[type="reset"], 42 | select { 43 | max-width: 100%; 44 | height: $gutter; 45 | line-height: ($gutter - $borderWeight*2); 46 | padding: 0 $gutter/1.5; 47 | font-size: 1.125rem; 48 | color: lighten($colorOne, 25%); 49 | background-color: $colorOne; 50 | border: $borderWeight $borderStyle $colorOne; 51 | border-radius: $borderRadius; 52 | -webkit-appearance: none; 53 | -moz-appearance: none; 54 | @include transition(); 55 | option { 56 | // for Firefox 57 | background-color: $colorBackground; 58 | border: 0; 59 | display: block; 60 | color: $colorText; 61 | } 62 | &:hover { 63 | background-color: darken($colorOne, 5%); 64 | border-color: darken($colorOne, 5%); 65 | color: lighten($colorOne, 35%); 66 | cursor: pointer; 67 | } 68 | &.alternate { 69 | color: lighten($colorTwo, 25%); 70 | background-color: $colorTwo; 71 | border: $borderWeight $borderStyle $colorTwo; 72 | &:hover { 73 | background-color: darken($colorTwo, 5%); 74 | border-color: darken($colorTwo, 5%); 75 | color: lighten($colorTwo, 35%); 76 | } 77 | } 78 | &:disabled, &.disabled { 79 | cursor: default; 80 | background-color: lighten($colorTwo, 30%); 81 | border-color: lighten($colorTwo, 30%); 82 | color: lighten($colorTwo, 0%); 83 | &:hover { 84 | @extend .disabled 85 | } 86 | } 87 | @include s() { 88 | width: 100%; 89 | clear: both; 90 | margin-bottom: $gutter/3; 91 | } 92 | } 93 | a.button { 94 | display: inline-block; 95 | text-decoration: none; 96 | } 97 | .select-wrapper { 98 | display: inline-block; 99 | position: relative; 100 | @media only screen and (max-width: $s) { 101 | width: 100%; 102 | clear: both; 103 | margin-bottom: $gutter/3; 104 | } 105 | } 106 | .errors, .note { 107 | padding: $gutter/3.5; 108 | margin: $gutter/2 auto; 109 | color: lighten($colorThree, 50%); 110 | background-color: $colorThree; 111 | border: $borderWeight $borderStyle $colorThree; 112 | border-radius: $borderRadius; 113 | li { 114 | margin-bottom: $gutter/3; 115 | &:last-of-type { 116 | margin-bottom: 0; 117 | } 118 | } 119 | a { 120 | color: lighten($colorThree, 50%); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /docs/_sass/_grid.scss: -------------------------------------------------------------------------------- 1 | /* Grid System ===============================================================*/ 2 | 3 | .container { 4 | max-width: 100%; 5 | width: 100%; 6 | margin: auto; 7 | padding: 0 $gutter; 8 | &.xl { 9 | width: $xl; 10 | } 11 | &.l { 12 | width: $l; 13 | } 14 | &.m { 15 | width: $m; 16 | } 17 | &.s { 18 | width: $s; 19 | } 20 | @include s() { 21 | padding: 0 $gutter/2; 22 | } 23 | &.collapse { 24 | padding: 0; 25 | } 26 | } 27 | 28 | // Rows 29 | 30 | .row { 31 | margin: 0 (-$gutter/2) $gutter; 32 | // Table Grid 33 | &.table { @include table(); } 34 | &.xl-table { @include xl() { @include table(); } } 35 | &.l-table { @include l() { @include table(); } } 36 | &.m-table { @include m() { @include table(); } } 37 | &.s-table { @include s() { @include table(); } } 38 | 39 | // Reverse Grid 40 | &.reverse { @include reverse(); } 41 | &.xl-reverse { @include xl() { @include reverse(); } } 42 | &.l-reverse { @include l() { @include reverse(); } } 43 | &.m-reverse { @include m() { @include reverse(); } } 44 | &.s-reverse { @include s() { @include reverse(); } } 45 | 46 | // Standard Grid 47 | &:after { 48 | content: ""; 49 | display: table; 50 | clear: both; 51 | } 52 | &.collapse { 53 | margin: 0; 54 | .column { 55 | padding: 0; 56 | } 57 | } 58 | 59 | // No bottom margin 60 | &.no-margin { margin-bottom: 0; } 61 | } 62 | 63 | // Column Grid 64 | 65 | .column { 66 | float: left; 67 | min-height: 0.125rem; 68 | padding: 0 $gutter/2; 69 | } 70 | .l1 { width: percentage(1/12); } 71 | .l2 { width: percentage(2/12); } 72 | .l3 { width: percentage(3/12); } 73 | .l4 { width: percentage(4/12); } 74 | .l5 { width: percentage(5/12); } 75 | .l6 { width: percentage(6/12); } 76 | .l7 { width: percentage(7/12); } 77 | .l8 { width: percentage(8/12); } 78 | .l9 { width: percentage(9/12); } 79 | .l10 { width: percentage(10/12); } 80 | .l11 { width: percentage(11/12); } 81 | .l12 { width: 100%; } 82 | // Custom columns 83 | .lfifth { width: percentage(1/5); } 84 | @include m() { 85 | .m1 { width: percentage(1/12); } 86 | .m2 { width: percentage(2/12); } 87 | .m3 { width: percentage(3/12); } 88 | .m4 { width: percentage(4/12); } 89 | .m5 { width: percentage(5/12); } 90 | .m6 { width: percentage(6/12); } 91 | .m7 { width: percentage(7/12); } 92 | .m8 { width: percentage(8/12); } 93 | .m9 { width: percentage(9/12); } 94 | .m10 { width: percentage(10/12); } 95 | .m11 { width: percentage(11/12); } 96 | .m12 { width: 100%; } 97 | // Custom columns 98 | .mfifth { width: percentage(1/5); } 99 | } 100 | @include s() { 101 | .s1 { width: percentage(1/12); } 102 | .s2 { width: percentage(2/12); } 103 | .s3 { width: percentage(3/12); } 104 | .s4 { width: percentage(4/12); } 105 | .s5 { width: percentage(5/12); } 106 | .s6 { width: percentage(6/12); } 107 | .s7 { width: percentage(7/12); } 108 | .s8 { width: percentage(8/12); } 109 | .s9 { width: percentage(9/12); } 110 | .s10 { width: percentage(10/12); } 111 | .s11 { width: percentage(11/12); } 112 | .s12 { width: 100%; } 113 | } 114 | 115 | // Uniform Height Clearing 116 | 117 | .l2:nth-child(6n+1) { clear: both; } 118 | .l3:nth-child(4n+1) { clear: both; } 119 | .l4:nth-child(3n+1) { clear: both; } 120 | .l6:nth-child(2n+1) { clear: both; } 121 | @include l() { 122 | .l2:nth-child(6n+1) { clear: both; } 123 | .l3:nth-child(4n+1) { clear: both; } 124 | .l4:nth-child(3n+1) { clear: both; } 125 | .l6:nth-child(2n+1) { clear: both; } 126 | } 127 | @include m() { 128 | .m2:nth-child(6n+1) { clear: both; } 129 | .m3:nth-child(4n+1) { clear: both; } 130 | .m4:nth-child(3n+1) { clear: both; } 131 | .m6:nth-child(2n+1) { clear: both; } 132 | } 133 | @include s() { 134 | .s2:nth-child(6n+1) { clear: both; } 135 | .s3:nth-child(4n+1) { clear: both; } 136 | .s4:nth-child(3n+1) { clear: both; } 137 | .s6:nth-child(2n+1) { clear: both; } 138 | } 139 | 140 | .row.non-uniform { 141 | .l2:nth-child(6n+1), 142 | .l3:nth-child(4n+1), 143 | .l4:nth-child(3n+1), 144 | .l6:nth-child(2n+1), 145 | .l2:nth-child(6n+1), 146 | .l3:nth-child(4n+1), 147 | .l4:nth-child(3n+1), 148 | .l6:nth-child(2n+1), 149 | .m2:nth-child(6n+1), 150 | .m3:nth-child(4n+1), 151 | .m4:nth-child(3n+1), 152 | .m6:nth-child(2n+1), 153 | .s2:nth-child(6n+1), 154 | .s3:nth-child(4n+1), 155 | .s4:nth-child(3n+1), 156 | .s6:nth-child(2n+1) { 157 | clear: none; 158 | } 159 | } 160 | 161 | @include l() { 162 | .column.l-clear { clear: both; } 163 | } 164 | @include m() { 165 | .column.m-clear { clear: both; } 166 | } 167 | @include s() { 168 | .column.s-clear { clear: both; } 169 | } 170 | 171 | // Flex Grid 172 | 173 | .flex { 174 | @include flex(); 175 | @include box-orient--vertical(); 176 | @include flex-direction(column); 177 | .row { 178 | @include flex(); 179 | @include flex-direction(row); 180 | .box { 181 | margin: 0 $gutter/2 $gutter; 182 | flex: auto; 183 | align-self: center; 184 | &:first-child { 185 | margin-left: 0; 186 | } 187 | &:last-child { 188 | margin-right: 0; 189 | } 190 | &.center { 191 | text-align: center; 192 | align-items: center; 193 | } 194 | } 195 | @include s() { 196 | @include flex-direction(column); 197 | .box { 198 | width: 100%; 199 | margin: 0 0 $gutter; 200 | } 201 | } 202 | } 203 | } 204 | 205 | @include xl() { 206 | .xl-hide { display: none !important; } 207 | .xl-left { text-align: left; } 208 | .xl-center { text-align: center; } 209 | .xl-right { text-align: right; } 210 | .l-show, .m-show, .s-show { display: none !important; } 211 | } 212 | @include l() { 213 | .l-hide { display: none !important; } 214 | .l-left { text-align: left; } 215 | .l-center { text-align: center; } 216 | .l-right { text-align: right; } 217 | .xl-show, .m-show, .s-show { display: none !important; } 218 | } 219 | @include m() { 220 | .m-hide { display: none !important; } 221 | .m-left { text-align: left; } 222 | .m-center { text-align: center; } 223 | .m-right { text-align: right; } 224 | .l-show { display: none !important; } 225 | .xl-show, .l-show, .s-show { display: none !important; } 226 | } 227 | @include s() { 228 | .s-hide { display: none !important; } 229 | .s-left { text-align: left; } 230 | .s-center { text-align: center; } 231 | .s-right { text-align: right; } 232 | .l-show { display: none !important; } 233 | .xl-show, .l-show, .m-show { display: none !important; } 234 | } 235 | -------------------------------------------------------------------------------- /docs/_sass/_header.scss: -------------------------------------------------------------------------------- 1 | // This is a partial. 2 | // It lies in /_sass, just waiting to be imported. 3 | // It does not contain the YAML front matter and has no corresponding output file in the built site. 4 | 5 | 6 | /*============================================================================== 7 | ___ _ _ _ 8 | / || | | | | | 9 | \__ | | | | | | __ 10 | / |/ |/_) |/ / \_/\/ 11 | \___/|__/| \_/|__/\__/ /\_/ 12 | |\ 13 | |/ 14 | Concrete v0.1 15 | https://elkfox.com 16 | https://experts.shopify.com/elkfox 17 | Copyright 2015 Shopify Inc. & Elkfox Co Pty Ltd 18 | ==============================================================================*/ 19 | 20 | 21 | /* Global ====================================================================*/ 22 | 23 | html { 24 | height: 100% 25 | } 26 | 27 | body { 28 | height: 100%; 29 | color: $colorText; 30 | background-color: $colorBackground; 31 | font: 16px/21px sans-serif; 32 | -webkit-font-smoothing: antialiased; 33 | } 34 | 35 | img { 36 | max-width: 100%; 37 | } 38 | 39 | /* Reset =====================================================================*/ 40 | 41 | html, body, div, span, applet, object, iframe, 42 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 43 | a, abbr, acronym, address, big, cite, code, 44 | del, dfn, em, img, ins, kbd, q, s, samp, 45 | small, strike, strong, sub, sup, tt, var, 46 | b, u, i, center, 47 | dl, dt, dd, ol, ul, li, 48 | fieldset, form, label, legend, 49 | table, caption, tbody, tfoot, thead, tr, th, td, 50 | article, aside, canvas, details, embed, 51 | figure, figcaption, footer, header, hgroup, 52 | menu, nav, output, ruby, section, summary, 53 | time, mark, audio, video { 54 | margin: 0; 55 | padding: 0; 56 | border: 0; 57 | font-size: 100%; 58 | font: inherit; 59 | vertical-align: baseline; 60 | outline: none; 61 | } 62 | article, aside, details, figcaption, figure, 63 | footer, header, hgroup, nav, nav, section, main { 64 | display: block; 65 | } 66 | body { 67 | line-height: 1; 68 | } 69 | ol, ul { 70 | list-style: none; 71 | } 72 | blockquote, q { 73 | quotes: none; 74 | font-size: 1.6em; 75 | font-weight: 600; 76 | line-height: 1.5; 77 | } 78 | blockquote:before, blockquote:after, 79 | q:before, q:after { 80 | content: ''; 81 | content: none; 82 | } 83 | table { 84 | border-collapse: collapse; 85 | border-spacing: 0; 86 | } 87 | th { 88 | font-weight: 800; 89 | font-size: 1.2rem; 90 | text-align: left; 91 | } 92 | td, th { 93 | padding: 10px; 94 | } 95 | form, input, textarea, label, fieldset, legend, select, optgroup, option, button 96 | { 97 | background-image: none; 98 | background-color: transparent; 99 | -webkit-box-shadow: none; 100 | -moz-box-shadow: none; 101 | box-shadow: none; 102 | // line-height: inherit; TBC 103 | line-height: 1; // TBC 104 | display: inline-block; 105 | vertical-align: middle; 106 | border: 0; 107 | outline: none; 108 | color: inherit; 109 | } 110 | *, 111 | *:after, 112 | *:before { 113 | margin: 0; 114 | padding: 0; 115 | -webkit-box-sizing: border-box; 116 | -moz-box-sizing: border-box; 117 | box-sizing: border-box; 118 | -webkit-font-smoothing: antialiased; 119 | -moz-osx-font-smoothing: grayscale; 120 | -webkit-backface-visibility: hidden; 121 | } 122 | -------------------------------------------------------------------------------- /docs/_sass/_helpers.scss: -------------------------------------------------------------------------------- 1 | // Helpers 2 | 3 | .left { 4 | text-align: left; 5 | } 6 | .center { 7 | text-align: center; 8 | margin-left: auto; 9 | margin-right: auto; 10 | } 11 | .right { 12 | text-align: right; 13 | } 14 | .wide { 15 | width: 100%; 16 | } 17 | .hidden { 18 | display: none; 19 | } 20 | .inline { 21 | list-style: none; 22 | display: inline-block; 23 | li { 24 | display: inline-block; 25 | } 26 | } 27 | .clearfix { 28 | @include clearfix(); 29 | } 30 | -------------------------------------------------------------------------------- /docs/_sass/_icons.scss: -------------------------------------------------------------------------------- 1 | @mixin icon { 2 | vertical-align: middle; 3 | display: inline-block; 4 | line-height: 1%; // Makes inline SVGs behave more like text characters 5 | svg { 6 | // vertical-align: middle; 7 | overflow: visible; 8 | fill: none; 9 | stroke: none; 10 | .fill { 11 | fill: $colorOne; 12 | } 13 | .stroke { 14 | stroke: $colorOne; 15 | stroke-width: $borderWeight; 16 | stroke-linecap: $iconLinecap; 17 | stroke-miterlimit: 10; 18 | } 19 | } 20 | .icon-block & { 21 | display: block !important; 22 | } 23 | } 24 | 25 | .icon { 26 | @include icon(); 27 | } 28 | .icon-small .icon { 29 | @include icon(); 30 | svg { 31 | height: $baseIconSize/2; 32 | width: $baseIconSize/2; 33 | .stroke { 34 | stroke-width: $borderWeight*2; 35 | } 36 | } 37 | } 38 | .icon-large .icon { 39 | @include icon(); 40 | svg { 41 | height: $baseIconSize*2; 42 | width: $baseIconSize*2; 43 | .stroke { 44 | stroke-width: $borderWeight/2; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/_sass/_popups.scss: -------------------------------------------------------------------------------- 1 | /* Popups ====================================================================*/ 2 | 3 | /// Basic popup styling 4 | .popup { 5 | position: fixed; 6 | z-index: 9996; 7 | opacity: 0; 8 | visibility: hidden; 9 | @include transition(); 10 | &.visible { 11 | opacity: 1; 12 | visibility: visible; 13 | @include transition(); 14 | } 15 | .popup-content { 16 | display: inline-block; 17 | z-index: 9999; 18 | max-width: 100%; 19 | max-height: 100%; 20 | overflow: auto; // Optional line for overly tall popups 21 | padding: $gutter; 22 | background-color: $colorForeground; 23 | text-align: initial; 24 | cursor: default; 25 | white-space: initial; 26 | color: $colorBackground; 27 | a { 28 | color: $colorBackground; 29 | } 30 | } 31 | &.overlay { 32 | background: rgba($colorForeground, 0.1); 33 | @include overlay(); 34 | .popup-outside { 35 | position: absolute; 36 | z-index: 9998; 37 | @include overlay(); 38 | } 39 | .popup-inner { 40 | -webkit-backface-visibility: hidden; 41 | cursor: pointer; 42 | z-index: 9997; 43 | text-align: center; 44 | white-space: nowrap; 45 | @include overlay(); 46 | @include prefixer(transition-delay, 0.3s, webkit moz spec); 47 | @include prefixer(transform, translateY(-$gutter/2), webkit moz ms spec); 48 | @include transition(); 49 | } 50 | .popup-content { 51 | @include vertical-align(); 52 | } 53 | &.visible { 54 | .popup-inner { 55 | @include prefixer(transform, translateY(0px), webkit moz ms spec); 56 | } 57 | } 58 | } 59 | &.notification { 60 | .popup-content { 61 | position: fixed; 62 | bottom: $gutter; 63 | left: $gutter; 64 | padding: $gutter/2; 65 | @include prefixer(transition-delay, 0.3s, webkit moz spec); 66 | @include prefixer(transform, translateY(-$gutter/2), webkit moz ms spec); 67 | @include transition(); 68 | } 69 | &.visible { 70 | .popup-content { 71 | @include prefixer(transform, translateY(0px), webkit moz ms spec); 72 | } 73 | } 74 | } 75 | .popup-close { 76 | display: block; 77 | position: absolute; 78 | top: $gutter/2; 79 | right: $gutter/2; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/_sass/_syntax.scss: -------------------------------------------------------------------------------- 1 | /* Dracula Theme v1.2.5 2 | * 3 | * https://github.com/zenorocha/dracula-theme 4 | * 5 | * Copyright 2016, All rights reserved 6 | * 7 | * Code licensed under the MIT license 8 | * http://zenorocha.mit-license.org 9 | * 10 | * @author Rob G 11 | * @author Chris Bracco 12 | * @author Zeno Rocha 13 | * @author Piruin Panichphol 14 | */ 15 | 16 | /* 17 | * Variables 18 | */ 19 | 20 | // $dt-gray-dark: #282a36; // Background 21 | // $dt-gray: #44475a; // Current Line & Selection 22 | // $dt-gray-light: #f8f8f2; // Foreground 23 | // $dt-blue: #6272a4; // Comment 24 | // $dt-cyan: #8be9fd; 25 | // $dt-green: #50fa7b; 26 | // $dt-orange: #ffb86c; 27 | // $dt-pink: #ff79c6; 28 | // $dt-purple: #bd93f9; 29 | // $dt-red: #ff5555; 30 | // $dt-yellow: #f1fa8c; 31 | 32 | /* 33 | * Styles 34 | */ 35 | code.highlighter-rouge { 36 | font-family: Menlo, monospace; 37 | font-weight: 900; 38 | color: $colorEight; 39 | display: inline; 40 | } 41 | 42 | @mixin code-language($name) { 43 | code.language-#{$name}:before { 44 | content: '>_#{$name}'; 45 | position: absolute; 46 | @include prefixer(transform, translateY(-200%), webkit moz ms spec); 47 | color: $colorEight; 48 | text-transform: uppercase; 49 | font-family: Menlo, monospace; 50 | font-weight: 900; 51 | font-size: 0.95rem; 52 | } 53 | } 54 | 55 | @include code-language('bash'); 56 | @include code-language('shell'); 57 | @include code-language('liquid'); 58 | @include code-language('javascript'); 59 | @include code-language('plaintext'); 60 | @include code-language('yaml'); 61 | @include code-language('css'); 62 | @include code-language('scss'); 63 | @include code-language('html'); 64 | 65 | .highlight { 66 | pre { 67 | white-space: pre-wrap; 68 | } 69 | font-family: Menlo, monospace; 70 | font-weight: 800; 71 | color: $colorLightGray; 72 | padding-top: $gutter; 73 | margin-bottom: $gutter; 74 | overflow: auto; 75 | 76 | .hll, 77 | .s, 78 | .sa, 79 | .sb, 80 | .sc, 81 | .dl, 82 | .sd, 83 | .s2, 84 | .se, 85 | .sh, 86 | .si, 87 | .sx, 88 | .sr, 89 | .s1, 90 | .ss { 91 | color: $colorThree; 92 | } 93 | 94 | .go { 95 | color: $colorGray; 96 | } 97 | 98 | .err, 99 | .g, 100 | .l, 101 | .n, 102 | .x, 103 | .p, 104 | .ge, 105 | .gr, 106 | .gh, 107 | .gi, 108 | .gp, 109 | .gs, 110 | .gu, 111 | .gt, 112 | .ld, 113 | .no, 114 | .nd, 115 | .ni, 116 | .ne, 117 | .nn, 118 | .nx, 119 | .py, 120 | .w, 121 | .bp { 122 | color: $colorLightGray; 123 | } 124 | 125 | .gh, 126 | .gi, 127 | .gu { 128 | font-weight: bold; 129 | } 130 | 131 | .ge { 132 | text-decoration: underline; 133 | } 134 | 135 | .bp { 136 | font-style: italic; 137 | } 138 | 139 | .c, 140 | .ch, 141 | .cm, 142 | .cpf, 143 | .c1, 144 | .cs { 145 | color: $colorSix; 146 | } 147 | 148 | .kd, 149 | .kt, 150 | .nb, 151 | .nl, 152 | .nv, 153 | .vc, 154 | .vg, 155 | .vi, 156 | .vm { 157 | color: $colorEight; 158 | } 159 | 160 | .kd, 161 | .nb, 162 | .nl, 163 | .nv, 164 | .vc, 165 | .vg, 166 | .vi, 167 | .vm { 168 | font-style: italic; 169 | } 170 | 171 | .na, 172 | .nc, 173 | .nf, 174 | .fm { 175 | color: $colorSeven; 176 | } 177 | 178 | .k, 179 | .o, 180 | .cp, 181 | .kc, 182 | .kn, 183 | .kp, 184 | .kr, 185 | .nt, 186 | .ow { 187 | color: $colorOne; 188 | } 189 | 190 | .m, 191 | .mb, 192 | .mf, 193 | .mh, 194 | .mi, 195 | .mo, 196 | .il { 197 | color: $colorFive; 198 | } 199 | 200 | .gd { 201 | color: $colorTwo; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /docs/_sass/_typography.scss: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: $bodyFontFamily; 4 | line-height: $lineHeight; 5 | letter-spacing: 0.33px; 6 | } 7 | 8 | h1, h2, h3, h4, h5, h6, 9 | .h1, .h2, .h3, .h4, .h5, .h6 { 10 | font-family: $headingFontFamily; 11 | line-height: $lineHeight; 12 | display: block; 13 | margin-bottom: $gutter/4; 14 | a { 15 | text-decoration: none; 16 | } 17 | } 18 | h1, .h1 { 19 | font-size: 4rem; 20 | font-weight: $fontBold; 21 | letter-spacing: 0.5px 22 | } 23 | h2, .h2 { 24 | font-size: 2.31rem; 25 | font-weight: $fontLight; 26 | letter-spacing: 0.5px 27 | } 28 | h3, .h3 { 29 | font-size: 2rem; 30 | } 31 | h4, .h4 { font-size: 1.45rem; } 32 | h5, .h5 { font-size: 1.4rem; } 33 | h6, .h6 { font-size: 1.3rem; } 34 | 35 | p, ul, ol, li { 36 | font-size: 1.07rem; 37 | font-weight: $fontRegular; 38 | line-height: $lineHeight; 39 | margin-bottom: 1.4rem; 40 | } 41 | 42 | a { 43 | color: $colorText; 44 | cursor: pointer; 45 | @include transition(); 46 | &:hover, &:focus { 47 | color: $colorEight; 48 | } 49 | } 50 | 51 | hr { 52 | border: 0; 53 | border-top: $borderWeight $borderStyle $colorText; 54 | margin: $gutter/1.5 0; 55 | } 56 | 57 | strong, b { 58 | font-weight: $fontBold; 59 | } 60 | 61 | small { 62 | font-size: 0.8rem; 63 | } 64 | 65 | i { 66 | font-style: italic; 67 | } 68 | 69 | .rte { 70 | // TBA 71 | h2 { 72 | margin-top: $gutter; 73 | } 74 | ul, ol { 75 | list-style: initial; 76 | list-style-position: inside; 77 | margin-bottom: $gutter/2; 78 | li { 79 | list-style: inherit; 80 | list-style-position: inherit; 81 | margin-bottom: $gutter/10; 82 | } 83 | } 84 | ol { 85 | list-style-type: decimal; 86 | } 87 | } 88 | aside.sidebar { 89 | h3 { 90 | font-weight: $fontBold; 91 | color: $colorOne; 92 | } 93 | h4 { 94 | font-weight: $fontBold; 95 | margin-bottom: 0; 96 | font-size: 1em; 97 | } 98 | li { 99 | font-weight: $fontBold; 100 | .active { 101 | color: $colorEight; 102 | } 103 | } 104 | a { 105 | text-decoration: none; 106 | transition: none; 107 | color: $colorText; 108 | .active { 109 | color: $colorText; 110 | } 111 | } 112 | ul { 113 | margin-bottom: $gutter / 2; 114 | } 115 | } 116 | .anchor { 117 | margin-bottom: $gutter; 118 | max-width: 700px; 119 | } 120 | article.documentation { 121 | h2, h3 { 122 | font-weight: $fontBold; 123 | color: $colorOne; 124 | } 125 | h2 { 126 | font-size: 1.8rem; 127 | margin-bottom: $gutter / 2; 128 | } 129 | h3 { 130 | font-size: 1.5rem; 131 | } 132 | ol, ul { 133 | margin: 0 0 $gutter/2 $gutter/2.5; 134 | list-style: initial; 135 | } 136 | > h1,> h2,> h3,> h4,> h5,> h6,> ul,> ol,> p { 137 | max-width: 700px; 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: About 4 | permalink: /about/ 5 | --- 6 | 7 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/) 8 | 9 | You can find the source code for the Jekyll new theme at: 10 | {% include icon-github.html username="jekyll" %} / 11 | [minima](https://github.com/jekyll/minima) 12 | 13 | You can find the source code for Jekyll at 14 | {% include icon-github.html username="jekyll" %} / 15 | [jekyll](https://github.com/jekyll/jekyll) 16 | -------------------------------------------------------------------------------- /docs/assets/bg-layer-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elkfox/shopify-node-app/ab2c566c8a51112b1c6c17b1efa62c05f74e31fd/docs/assets/bg-layer-1.png -------------------------------------------------------------------------------- /docs/assets/bg-layer-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Desktop HD Copy 6 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/assets/bg-layer-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elkfox/shopify-node-app/ab2c566c8a51112b1c6c17b1efa62c05f74e31fd/docs/assets/bg-layer-2.png -------------------------------------------------------------------------------- /docs/assets/bg-layer-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Desktop HD Copy 6 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elkfox/shopify-node-app/ab2c566c8a51112b1c6c17b1efa62c05f74e31fd/docs/assets/favicon.png -------------------------------------------------------------------------------- /docs/assets/js/focus.js: -------------------------------------------------------------------------------- 1 | /*============================================================================== 2 | ___ _ _ _ 3 | / || | | | | | 4 | \__ | | | | | | __ 5 | / |/ |/_) |/ / \_/\/ 6 | \___/|__/| \_/|__/\__/ /\_/ 7 | |\ 8 | |/ 9 | Focus v1.1 10 | https://github.com/Elkfox/Focus 11 | Copyright (c) 2017 Elkfox Co Pty Ltd 12 | https://elkfox.com 13 | Project lead: George Butter 14 | MIT License 15 | ==============================================================================*/ 16 | var Focus = function(target, config) { 17 | this.target = target; 18 | this.element = jQuery(target); 19 | this.config = { 20 | 'visibleClass': 'visible', 21 | 'innerSelector': '.popup-inner', 22 | 'autoFocusSelector': '[data-auto-focus]' 23 | }; 24 | // Merge configs 25 | if(config) { 26 | for (var key in config) { 27 | this.config[key] = config[key]; 28 | } 29 | } 30 | this.visible = false; 31 | // Bind the functions 32 | this.show = this.show.bind(this); 33 | this.hide = this.hide.bind(this); 34 | this.toggle = this.toggle.bind(this); 35 | // Capture the variable so that we can fire it's proto methods by it's target. 36 | Focus.elements[target] = this; 37 | } 38 | Focus.elements = {}; 39 | Focus.getTarget = function(element, event) { 40 | if (jQuery(element).is('a')) { 41 | event.preventDefault(); 42 | } 43 | const selector = this.getSelectorFromElement(element); 44 | target = selector ? selector : null; 45 | return target; 46 | } 47 | Focus.getSelectorFromElement = function(element) { 48 | var selector = jQuery(element).data('target'); 49 | if (!selector || selector === '#') { 50 | // href can be used as a fallback instead of data target attribute 51 | selector = jQuery(element).attr('href') || null; 52 | } 53 | return selector 54 | } 55 | Focus.eventHandler = function(target, method) { 56 | var element = Focus.elements[target]; 57 | if(!element) { 58 | var element = new Focus(target); 59 | } 60 | method === 'hide' ? element.hide() : element.toggle(); 61 | } 62 | Focus.prototype.show = function() { 63 | var _this = this; 64 | if (!this.visible || !this.element.hasClass(this.config.visibleClass)) { 65 | this.element.addClass(this.config.visibleClass); 66 | this.visible = true; 67 | 68 | // Focus on the an input field 69 | if(jQuery(this.target + ' ' + this.config.autoFocusSelector).length) { 70 | setTimeout(function(){ 71 | jQuery(_this.target + ' ' + _this.config.autoFocusSelector).focus(); 72 | }, 300); 73 | } 74 | // When someone clicks the [data-close] button then we should close the modal. 75 | this.element.one('click', '[data-close]', function (e) { 76 | e.preventDefault(); 77 | _this.hide(); 78 | }); 79 | // When someone clicks on the inner class hide the popup 80 | this.element.one('click', this.config.innerSelector, function (e) { 81 | if (jQuery(e.target).is(_this.config.innerSelector) || jQuery(e.target).parents(_this.config.target).length === 0) { 82 | _this.hide(); 83 | } 84 | }); 85 | // When someone presses esc hide the popup 86 | 87 | this.element.on('keyup', function (e) { 88 | e.preventDefault(); 89 | if (e.keyCode === 27) { 90 | this.element.off('keyup'); 91 | _this.hide(); 92 | } 93 | }); 94 | return jQuery(document).trigger('focus:open', [this.target]); 95 | } 96 | return jQuery(document).trigger('focus:error', { error: 'Popup already open' }); 97 | } 98 | Focus.prototype.hide = function() { 99 | if (this.visible || this.element.hasClass(this.config.visibleClass)) { 100 | this.element.removeClass(this.config.visibleClass); 101 | this.visible = false; 102 | return jQuery(document).trigger('focus:close', [this.target]); 103 | } 104 | return jQuery(document).trigger('focus:error', { error: 'Focus element is already closed' }); 105 | } 106 | Focus.prototype.toggle = function() { 107 | return this.visible ? this.hide() : this.show(); 108 | } 109 | jQuery(document).ready(function(){ 110 | jQuery(document).on('click', '[data-trigger="popup"]', function(event) { 111 | var target = Focus.getTarget(jQuery(this), event); 112 | Focus.eventHandler(target, 'toggle'); 113 | }); 114 | jQuery(document).on('click', '[data-close]', function(event) { 115 | var target = Focus.getTarget(jQuery(this), event); 116 | if(target) Focus.eventHandler(target, 'hide'); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /docs/assets/js/scroll-spy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend jquery with a scrollspy plugin. 3 | * This watches the window scroll and fires events when elements are scrolled into viewport. 4 | * 5 | * throttle() and getTime() taken from Underscore.js 6 | * https://github.com/jashkenas/underscore 7 | * 8 | * @author Copyright 2013 John Smart 9 | * @license https://raw.github.com/thesmart/jquery-scrollspy/master/LICENSE 10 | * @see https://github.com/thesmart 11 | * @version 0.1.2 12 | */ 13 | (function($) { 14 | 15 | var jWindow = $(window); 16 | var elements = []; 17 | var elementsInView = []; 18 | var isSpying = false; 19 | var ticks = 0; 20 | var offset = { 21 | top : 0, 22 | right : 0, 23 | bottom : 0, 24 | left : 0, 25 | } 26 | 27 | /** 28 | * Find elements that are within the boundary 29 | * @param {number} top 30 | * @param {number} right 31 | * @param {number} bottom 32 | * @param {number} left 33 | * @return {jQuery} A collection of elements 34 | */ 35 | function findElements(top, right, bottom, left) { 36 | var hits = $(); 37 | $.each(elements, function(i, element) { 38 | var elTop = element.offset().top, 39 | elLeft = element.offset().left, 40 | elRight = elLeft + element.width(), 41 | elBottom = elTop + element.height(); 42 | 43 | var isIntersect = !(elLeft > right || 44 | elRight < left || 45 | elTop > bottom || 46 | elBottom < top); 47 | 48 | if (isIntersect) { 49 | hits.push(element); 50 | } 51 | }); 52 | 53 | return hits; 54 | } 55 | 56 | /** 57 | * Called when the user scrolls the window 58 | */ 59 | function onScroll() { 60 | 61 | // unique tick id 62 | ++ticks; 63 | 64 | // viewport rectangle 65 | var top = jWindow.scrollTop(), 66 | left = jWindow.scrollLeft(), 67 | right = left + jWindow.width(), 68 | bottom = top + jWindow.height(); 69 | 70 | // determine which elements are in view 71 | var intersections = findElements(top+offset.top, right+offset.right, bottom+offset.bottom, left+offset.left); 72 | $.each(intersections, function(i, element) { 73 | var lastTick = element.data('scrollSpy:ticks'); 74 | if (typeof lastTick != 'number') { 75 | // entered into view 76 | element.triggerHandler('scrollSpy:enter'); 77 | } 78 | 79 | // update tick id 80 | element.data('scrollSpy:ticks', ticks); 81 | }); 82 | 83 | // determine which elements are no longer in view 84 | $.each(elementsInView, function(i, element) { 85 | var lastTick = element.data('scrollSpy:ticks'); 86 | if (typeof lastTick == 'number' && lastTick !== ticks) { 87 | // exited from view 88 | element.triggerHandler('scrollSpy:exit'); 89 | element.data('scrollSpy:ticks', null); 90 | } 91 | }); 92 | 93 | // remember elements in view for next tick 94 | elementsInView = intersections; 95 | } 96 | 97 | /** 98 | * Called when window is resized 99 | */ 100 | function onWinSize() { 101 | jWindow.trigger('scrollSpy:winSize'); 102 | } 103 | 104 | /** 105 | * Get time in ms 106 | * @license https://raw.github.com/jashkenas/underscore/master/LICENSE 107 | * @type {function} 108 | * @return {number} 109 | */ 110 | var getTime = (Date.now || function () { 111 | return new Date().getTime(); 112 | }); 113 | 114 | /** 115 | * Returns a function, that, when invoked, will only be triggered at most once 116 | * during a given window of time. Normally, the throttled function will run 117 | * as much as it can, without ever going more than once per `wait` duration; 118 | * but if you'd like to disable the execution on the leading edge, pass 119 | * `{leading: false}`. To disable execution on the trailing edge, ditto. 120 | * @license https://raw.github.com/jashkenas/underscore/master/LICENSE 121 | * @param {function} func 122 | * @param {number} wait 123 | * @param {Object=} options 124 | * @returns {Function} 125 | */ 126 | function throttle(func, wait, options) { 127 | var context, args, result; 128 | var timeout = null; 129 | var previous = 0; 130 | options || (options = {}); 131 | var later = function () { 132 | previous = options.leading === false ? 0 : getTime(); 133 | timeout = null; 134 | result = func.apply(context, args); 135 | context = args = null; 136 | }; 137 | return function () { 138 | var now = getTime(); 139 | if (!previous && options.leading === false) previous = now; 140 | var remaining = wait - (now - previous); 141 | context = this; 142 | args = arguments; 143 | if (remaining <= 0) { 144 | clearTimeout(timeout); 145 | timeout = null; 146 | previous = now; 147 | result = func.apply(context, args); 148 | context = args = null; 149 | } else if (!timeout && options.trailing !== false) { 150 | timeout = setTimeout(later, remaining); 151 | } 152 | return result; 153 | }; 154 | }; 155 | 156 | /** 157 | * Enables ScrollSpy using a selector 158 | * @param {jQuery|string} selector The elements collection, or a selector 159 | * @param {Object=} options Optional. 160 | throttle : number -> scrollspy throttling. Default: 100 ms 161 | offsetTop : number -> offset from top. Default: 0 162 | offsetRight : number -> offset from right. Default: 0 163 | offsetBottom : number -> offset from bottom. Default: 0 164 | offsetLeft : number -> offset from left. Default: 0 165 | * @returns {jQuery} 166 | */ 167 | $.scrollSpy = function(selector, options) { 168 | selector = $(selector); 169 | selector.each(function(i, element) { 170 | elements.push($(element)); 171 | }); 172 | options = options || { 173 | throttle: 100 174 | }; 175 | 176 | offset.top = options.offsetTop || 0; 177 | offset.right = options.offsetRight || 0; 178 | offset.bottom = options.offsetBottom || 0; 179 | offset.left = options.offsetLeft || 0; 180 | 181 | var throttledScroll = throttle(onScroll, options.throttle || 100); 182 | var readyScroll = function(){ 183 | $(document).ready(throttledScroll); 184 | }; 185 | 186 | if (!isSpying) { 187 | jWindow.on('scroll', readyScroll); 188 | jWindow.on('resize', readyScroll); 189 | isSpying = true; 190 | } 191 | 192 | // perform a scan once, after current execution context, and after dom is ready 193 | setTimeout(readyScroll, 0); 194 | 195 | return selector; 196 | }; 197 | 198 | /** 199 | * Listen for window resize events 200 | * @param {Object=} options Optional. Set { throttle: number } to change throttling. Default: 100 ms 201 | * @returns {jQuery} $(window) 202 | */ 203 | $.winSizeSpy = function(options) { 204 | $.winSizeSpy = function() { return jWindow; }; // lock from multiple calls 205 | options = options || { 206 | throttle: 100 207 | }; 208 | return jWindow.on('resize', throttle(onWinSize, options.throttle || 100)); 209 | }; 210 | 211 | /** 212 | * Enables ScrollSpy on a collection of elements 213 | * e.g. $('.scrollSpy').scrollSpy() 214 | * @param {Object=} options Optional. 215 | throttle : number -> scrollspy throttling. Default: 100 ms 216 | offsetTop : number -> offset from top. Default: 0 217 | offsetRight : number -> offset from right. Default: 0 218 | offsetBottom : number -> offset from bottom. Default: 0 219 | offsetLeft : number -> offset from left. Default: 0 220 | * @returns {jQuery} 221 | */ 222 | $.fn.scrollSpy = function(options) { 223 | return $.scrollSpy($(this), options); 224 | }; 225 | 226 | })(jQuery); 227 | -------------------------------------------------------------------------------- /docs/assets/js/smooth-scroll.js: -------------------------------------------------------------------------------- 1 | /*! smooth-scroll v10.3.1 | (c) 2017 Chris Ferdinandi | MIT License | http://github.com/cferdinandi/smooth-scroll */ 2 | !(function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.smoothScroll=t(e)})("undefined"!=typeof global?global:this.window||this.global,(function(e){"use strict";var t,n,o,r,a,c,l,i={},u="querySelector"in document&&"addEventListener"in e,s={selector:"[data-scroll]",selectorHeader:null,speed:500,easing:"easeInOutCubic",offset:0,callback:function(){}},f=function(){var e={},t=!1,n=0,o=arguments.length;"[object Boolean]"===Object.prototype.toString.call(arguments[0])&&(t=arguments[0],n++);for(;n=0&&t.item(n)!==this;);return n>-1});e&&e!==document;e=e.parentNode)if(e.matches(t))return e;return null},m=function(e){"#"===e.charAt(0)&&(e=e.substr(1));for(var t,n=String(e),o=n.length,r=-1,a="",c=n.charCodeAt(0);++r=1&&t<=31||127==t||0===r&&t>=48&&t<=57||1===r&&t>=48&&t<=57&&45===c?"\\"+t.toString(16)+" ":t>=128||45===t||95===t||t>=48&&t<=57||t>=65&&t<=90||t>=97&&t<=122?n.charAt(r):"\\"+n.charAt(r)}return"#"+a},p=function(e,t){var n;return"easeInQuad"===e&&(n=t*t),"easeOutQuad"===e&&(n=t*(2-t)),"easeInOutQuad"===e&&(n=t<.5?2*t*t:(4-2*t)*t-1),"easeInCubic"===e&&(n=t*t*t),"easeOutCubic"===e&&(n=--t*t*t+1),"easeInOutCubic"===e&&(n=t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1),"easeInQuart"===e&&(n=t*t*t*t),"easeOutQuart"===e&&(n=1- --t*t*t*t),"easeInOutQuart"===e&&(n=t<.5?8*t*t*t*t:1-8*--t*t*t*t),"easeInQuint"===e&&(n=t*t*t*t*t),"easeOutQuint"===e&&(n=1+--t*t*t*t*t),"easeInOutQuint"===e&&(n=t<.5?16*t*t*t*t*t:1+16*--t*t*t*t*t),n||t},g=function(e,t,n){var o=0;if(e.offsetParent)do{o+=e.offsetTop,e=e.offsetParent}while(e);return o=Math.max(o-t-n,0),Math.min(o,y()-b())},b=function(){return Math.max(document.documentElement.clientHeight,e.innerHeight||0)},y=function(){return Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,document.body.offsetHeight,document.documentElement.offsetHeight,document.body.clientHeight,document.documentElement.clientHeight)},v=function(e){return e&&"object"==typeof JSON&&"function"==typeof JSON.parse?JSON.parse(e):{}},O=function(e){return e?d(e)+e.offsetTop:0},S=function(t,n,o){o||(t.focus(),document.activeElement.id!==t.id&&(t.setAttribute("tabindex","-1"),t.focus(),t.style.outline="none"),e.scrollTo(0,n))};i.animateScroll=function(n,o,c){var i=v(o?o.getAttribute("data-options"):null),u=f(t||s,c||{},i),d="[object Number]"===Object.prototype.toString.call(n),h=d||!n.tagName?null:n;if(d||h){var m=e.pageYOffset;u.selectorHeader&&!r&&(r=document.querySelector(u.selectorHeader)),a||(a=O(r));var b,E,I=d?n:g(h,a,parseInt("function"==typeof u.offset?u.offset():u.offset,10)),H=I-m,A=y(),j=0,C=function(t,r,a){var c=e.pageYOffset;(t==r||c==r||e.innerHeight+c>=A)&&(clearInterval(a),S(n,r,d),u.callback(n,o))},M=function(){j+=16,b=j/parseInt(u.speed,10),b=b>1?1:b,E=m+H*p(u.easing,b),e.scrollTo(0,Math.floor(E)),C(E,I,l)};0===e.pageYOffset&&e.scrollTo(0,0),(function(){clearInterval(l),l=setInterval(M,16)})()}};var E=function(t){try{m(decodeURIComponent(e.location.hash))}catch(t){m(e.location.hash)}n&&(n.id=n.getAttribute("data-scroll-id"),i.animateScroll(n,o),n=null,o=null)},I=function(r){if(0===r.button&&!r.metaKey&&!r.ctrlKey&&(o=h(r.target,t.selector))&&"a"===o.tagName.toLowerCase()&&o.hostname===e.location.hostname&&o.pathname===e.location.pathname&&/#/.test(o.href)){var a;try{a=m(decodeURIComponent(o.hash))}catch(e){a=m(o.hash)}if("#"===a){r.preventDefault(),n=document.body;var c=n.id?n.id:"smooth-scroll-top";return n.setAttribute("data-scroll-id",c),n.id="",void(e.location.hash.substring(1)===c?E():e.location.hash=c)}n=document.querySelector(a),n&&(n.setAttribute("data-scroll-id",n.id),n.id="",o.hash===e.location.hash&&(r.preventDefault(),E()))}},H=function(e){c||(c=setTimeout((function(){c=null,a=O(r)}),66))};return i.destroy=function(){t&&(document.removeEventListener("click",I,!1),e.removeEventListener("resize",H,!1),t=null,n=null,o=null,r=null,a=null,c=null,l=null)},i.init=function(n){u&&(i.destroy(),t=f(s,n||{}),r=t.selectorHeader?document.querySelector(t.selectorHeader):null,a=O(r),document.addEventListener("click",I,!1),e.addEventListener("hashchange",E,!1),r&&e.addEventListener("resize",H,!1))},i})); 3 | -------------------------------------------------------------------------------- /docs/assets/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # this ensures Jekyll reads the file to be transformed into CSS later 3 | # only Main files contain this front matter, not partials. 4 | --- 5 | 6 | // Variables =================================================================== 7 | 8 | // Palette 9 | $colorWhite: #fbfbfb; 10 | $colorLightGray: #f8f8f2; 11 | $colorGray: #44475a; 12 | $colorDarkGray: #21212a; 13 | 14 | $colorPink: #b08cff; 15 | $colorPurple: #854AA0; 16 | $colorCyan: #68C7ED; 17 | $colorBlue: #6272a4; 18 | $colorYellow: #E7DC64; 19 | $colorGreen: #71AA62; 20 | $colorOrange: #ffb86c; 21 | $colorRed: #F2608A; 22 | 23 | // Variables in use 24 | $colorOne: $colorGreen; 25 | $colorTwo: $colorGreen; 26 | $colorThree: $colorBlue; 27 | $colorFour: $colorGreen; 28 | $colorFive: $colorBlue; 29 | $colorSix: $colorRed; 30 | $colorSeven: $colorOrange; 31 | $colorEight: $colorPink; 32 | 33 | $colorText: $colorLightGray; 34 | $colorBackground: $colorDarkGray; 35 | $colorForeground: $colorLightGray; 36 | 37 | // Typography 38 | $fontLight: 300; 39 | $fontRegular: 400; 40 | $fontBold: 600; 41 | $lineHeight: 1.75; 42 | 43 | $bodyFontFamily: 'Oxygen', Helvetica, Arial, sans-serif; 44 | $headingFontFamily: 'Oxygen', Helvetica, Arial, sans-serif; 45 | 46 | // Space & Borders 47 | $gutter: 50px; 48 | $borderRadius: 2px; 49 | $borderWeight: 4px; 50 | $borderStyle: solid; 51 | $borderColor: $colorYellow; 52 | 53 | // Screen Sizes 54 | $s: 650px; 55 | $m: 850px; 56 | $l: 1050px; 57 | $xl: 1450px; 58 | 59 | // Miscellaneous 60 | $transition: all .3s ease-out; 61 | $transitionDelay: 0.5s; 62 | 63 | // Icons 64 | $baseIconSize: 32px; 65 | $iconLinecap: square; 66 | 67 | // Mixins =================================================================== 68 | 69 | // Prefixer (See https://github.com/thoughtbot/bourbon/blob/master/app/assets/stylesheets/addons/_prefixer.scss) 70 | @mixin prefixer($property, $value, $prefixes) { 71 | @each $prefix in $prefixes { 72 | @if $prefix == webkit { 73 | -webkit-#{$property}: $value; 74 | } @else if $prefix == moz { 75 | -moz-#{$property}: $value; 76 | } @else if $prefix == ms { 77 | -ms-#{$property}: $value; 78 | } @else if $prefix == o { 79 | -o-#{$property}: $value; 80 | } @else if $prefix == spec { 81 | #{$property}: $value; 82 | } @else { 83 | @warn "Unrecognized prefix: #{$prefix}"; 84 | } 85 | } 86 | } 87 | 88 | // Clearfix 89 | @mixin clearfix() { 90 | display: block; // TBC - is this OK? Seems to help 91 | &:after { 92 | content: ''; 93 | display: table; 94 | clear: both; 95 | } 96 | *zoom: 1; 97 | } 98 | 99 | // Verically Align 100 | @mixin vertical-align() { 101 | position: relative; 102 | top: 50%; 103 | -webkit-transform: perspective(1px) translateY(-50%); 104 | -ms-transform: perspective(1px) translateY(-50%); 105 | transform: perspective(1px) translateY(-50%); 106 | } 107 | 108 | // Transition Property (for legacy support) 109 | @mixin transition-property() { 110 | -webkit-transition-property: -webkit-transform; 111 | -moz-transition-property: -moz-transform; 112 | transition-property: transform; 113 | } 114 | 115 | // Transitions 116 | @mixin transition() { 117 | @include prefixer(transition, $transition, webkit moz ms spec); 118 | } 119 | 120 | // Overlays 121 | @mixin overlay() { 122 | top: 0; 123 | left: 0; 124 | right: 0; 125 | bottom: 0; 126 | width: 100%; 127 | height: 100%; 128 | } 129 | 130 | // Flex 131 | @mixin flex() { 132 | display: -webkit-box; 133 | display: -moz-box; 134 | display: -ms-flexbox; 135 | display: -webkit-box; 136 | display: -webkit-flexbox; 137 | display: -webkit-flex; 138 | display: flex; 139 | } 140 | @mixin box-orient--vertical() { 141 | @include prefixer(box-orient, vertical, webkit moz spec); 142 | } 143 | @mixin flex-direction($flex-direction) { 144 | @include prefixer(flex-direction, $flex-direction, webkit moz ms spec); 145 | } 146 | 147 | // Screen Sizes & Types 148 | @mixin s() { 149 | @media (max-width: $s) { 150 | @content; 151 | } 152 | } 153 | @mixin m() { 154 | @media (min-width: $s) and (max-width: $l - 1px) { 155 | @content; 156 | } 157 | } 158 | @mixin l() { 159 | @media (min-width: $l) and (max-width: $xl - 1px) { 160 | @content; 161 | } 162 | } 163 | @mixin xl() { 164 | @media (min-width: $xl) { 165 | @content; 166 | } 167 | } 168 | 169 | // Table Grids 170 | @mixin table() { 171 | display: table; 172 | table-layout: fixed; 173 | width: 100%; 174 | margin: 0 0 $gutter; 175 | .column { 176 | float: none; 177 | display: table-cell; 178 | vertical-align: middle; 179 | &.middle { 180 | vertical-align: middle; 181 | } 182 | &.top { 183 | vertical-align: top; 184 | } 185 | &.bottom { 186 | vertical-align: bottom; 187 | } 188 | &:first-of-type { 189 | padding-left: 0; 190 | } 191 | &:last-of-type { 192 | padding-right: 0; 193 | } 194 | } 195 | } 196 | 197 | // Reverse Grids 198 | @mixin reverse() { 199 | direction: rtl; 200 | .column { 201 | direction: ltr; 202 | float: right; 203 | } 204 | } 205 | 206 | // Front matter dependent 207 | body { 208 | background-image: url('{{ site.github.url }}/assets/bg-layer-1.svg'); 209 | background-repeat: repeat-x; 210 | } 211 | .body-wrapper { 212 | background-image: url('{{ site.github.url }}/assets/bg-layer-2.svg'); 213 | background-repeat: repeat-x; 214 | overflow: auto; 215 | } 216 | 217 | 218 | // Imports =================================================================== 219 | 220 | @import "header"; 221 | @import "helpers"; 222 | @import "grid"; 223 | @import "typography"; 224 | @import "form"; 225 | @import "syntax"; 226 | @import "icons"; 227 | @import "popups"; 228 | @import "additions"; 229 | -------------------------------------------------------------------------------- /docs/assets/ngrok-init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elkfox/shopify-node-app/ab2c566c8a51112b1c6c17b1efa62c05f74e31fd/docs/assets/ngrok-init.png -------------------------------------------------------------------------------- /docs/assets/partner-dash-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elkfox/shopify-node-app/ab2c566c8a51112b1c6c17b1efa62c05f74e31fd/docs/assets/partner-dash-settings.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # You don't need to edit this file, it's empty on purpose. 3 | # Edit theme's home layout instead if you wanna make some changes 4 | # See: https://jekyllrb.com/docs/themes/#overriding-theme-defaults 5 | layout: home 6 | --- 7 | -------------------------------------------------------------------------------- /helpers/index.js: -------------------------------------------------------------------------------- 1 | const ShopifyAPI = require('shopify-node-api'); 2 | const config = require('../config'); 3 | const crypto = require('crypto'); 4 | /** 5 | * A file full of helper functions that I found useful for building shopify apps. 6 | */ 7 | module.exports = { 8 | /** 9 | * openSession - Opens a Shopify Session which allows Shopify API calls. 10 | * @param {string} shop - The name of the shop object you want to open a new session for. 11 | * @returns {object} An active shopify session with access to the API and such. 12 | */ 13 | openSession(shop) { 14 | return new ShopifyAPI({ 15 | shop: shop.shopify_domain, 16 | shopify_api_key: config.SHOPIFY_API_KEY, 17 | shopify_shared_secret: config.SHOPIFY_SHARED_SECRET, 18 | access_token: shop.accessToken, 19 | }); 20 | }, 21 | 22 | /** 23 | * buildWebhook() 24 | * @param {String} topic - The topic of the webhook you wish to create 25 | * @param {String} address - The URL you want the webhook data sent to 26 | * @param {ShopifyAPI} shop - ShopifyAPI instance for the Shop you're creating the webhook for 27 | * @param {function} callback - A callback url for when the request is complete 28 | * @returns function or false if unsuccesful. 29 | */ 30 | buildWebhook(topic = '', address = `${config.APP_URI}/webhook/`, shop = {}, callback) { 31 | if (topic.length === 0) { 32 | return false; 33 | } else if (address.length === 0) { 34 | return false; 35 | } else if (typeof shop !== 'object') { 36 | return false; 37 | } else if (typeof callback !== 'function') { 38 | return false; 39 | } 40 | const data = { 41 | webhook: { 42 | topic, 43 | address, 44 | format: 'json', 45 | }, 46 | }; 47 | shop.post('/admin/webhooks.json', data, (err, response, headers) => { 48 | if (err) { 49 | if (typeof callback === 'function') { 50 | return callback(err, response, headers); 51 | } 52 | return false; 53 | } 54 | return typeof callback === 'function' ? callback(null, response, headers) : true; 55 | }); 56 | return typeof callback === 'function' ? callback('Could not create webhook', null, null) : false; 57 | }, 58 | 59 | generateNonce(bits = 64) { 60 | let text = ''; 61 | const possible = 'ABCDEFGHIJKLMOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'; 62 | 63 | for (let i = 0; i < bits; i += 1) { 64 | text += possible.charAt(Math.floor(Math.random() * bits)); 65 | } 66 | return text; 67 | }, 68 | 69 | verifyHmac(data, hmac) { 70 | if (!hmac) { 71 | return false; 72 | } else if (!data || typeof data !== 'object') { 73 | return false; 74 | } 75 | 76 | const sharedSecret = config.SHOPIFY_SHARED_SECRET; 77 | const calculatedSignature = crypto.createHmac('sha256', sharedSecret) 78 | .update(Buffer.from(data), 'utf8') 79 | .digest('base64'); 80 | return calculatedSignature === hmac; 81 | }, 82 | 83 | verifyOAuth(query) { 84 | if (!query.hmac) { 85 | return false; 86 | } 87 | const hmac = query.hmac; 88 | const sharedSecret = config.SHOPIFY_SHARED_SECRET; 89 | delete query.hmac; 90 | const sortedQuery = Object.keys(query).map(key => `${key}=${Array(query[key]).join(',')}`).sort().join('&'); 91 | const calculatedSignature = crypto.createHmac('sha256', sharedSecret).update(sortedQuery).digest('hex'); 92 | if (calculatedSignature === hmac) { 93 | return true; 94 | } 95 | 96 | return false; 97 | }, 98 | }; 99 | 100 | 101 | -------------------------------------------------------------------------------- /middleware/index.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const config = require('../config'); 3 | const verifyHmac = require('../helpers').verifyHmac; 4 | /** 5 | * Express middleware to verify hmac and requests from shopify. 6 | * This middleware adds two items to the req object: 7 | * req.topic - A string containing the topic of the middlware 8 | * req.shop - The shop url of the store posting to the webhook url 9 | * @param {object} req - Express/Node request object 10 | * @param {object} res - Expres/Node response object 11 | * @param {function} next - Function that represents the next piece of middleware. 12 | */ 13 | function verifyWebhook(req, res, next) { 14 | let hmac; 15 | let data; 16 | try { 17 | hmac = req.get('X-Shopify-Hmac-SHA256'); 18 | data = req.rawbody; 19 | } catch (e) { 20 | console.log(`Webhook request failed from: ${req.get('X-Shopify-Shop-Domain')}`); 21 | res.sendStatus(200); 22 | } 23 | 24 | if (verifyHmac(JSON.stringify(data), hmac)) { 25 | req.topic = req.get('X-Shopify-Topic'); 26 | req.shop = req.get('X-Shopify-Shop-Domain'); 27 | return next(); 28 | } 29 | 30 | return res.sendStatus(200); 31 | } 32 | 33 | module.exports = { 34 | verifyWebhook, 35 | }; 36 | -------------------------------------------------------------------------------- /models/Counter.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const CounterSchema = mongoose.Schema({ 4 | _id: { type: String, required: true }, 5 | seq: { type: Number, default: 1000 }, 6 | }); 7 | 8 | const Counter = mongoose.model('counter', CounterSchema); 9 | 10 | module.exports = Counter; 11 | -------------------------------------------------------------------------------- /models/Shop.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Counter = require('./Counter'); 3 | 4 | 5 | const Shop = mongoose.Schema({ 6 | shopId: Number, 7 | shopify_domain: String, // Shopify domain without the .myshopify.com on the end. 8 | name: String, 9 | domain: String, 10 | supportEmail: String, 11 | nonce: String, 12 | accessToken: String, 13 | isActive: { type: Boolean, default: false }, 14 | }); 15 | 16 | module.exports = mongoose.model('Shop', Shop); 17 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elkfox/shopify-node-app/ab2c566c8a51112b1c6c17b1efa62c05f74e31fd/models/index.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-node-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "cleardbs": "node ./utils/cleardbs.js", 7 | "seeddbs": "node ./utils/seeddbs.js", 8 | "debug": "DEBUG=shopify-node-app:* npm start", 9 | "debugwatch": "DEBUG=shopify-node-app:* npm watch", 10 | "watch": "nodemon ./start.js", 11 | "start": "node ./start.js" 12 | }, 13 | "dependencies": { 14 | "body-parser": "~1.16.0", 15 | "connect-mongo": "^1.3.2", 16 | "cookie-parser": "~1.4.3", 17 | "debug": "~2.6.0", 18 | "dotenv": "^4.0.0", 19 | "express": "~4.14.1", 20 | "express-session": "^1.0.6", 21 | "hbs": "~4.0.1", 22 | "mongoose": "^4.9.3", 23 | "morgan": "~1.7.0", 24 | "serve-favicon": "~2.3.2", 25 | "shopify-node-api": "^1.7.6", 26 | "throng": "^4.0.0" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^3.19.0", 30 | "eslint-config-airbnb-base": "^11.1.3", 31 | "eslint-plugin-import": "^2.2.0", 32 | "nodemon": "^1.11.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ./routes/api.js 3 | * This is where you'll set up any REST api endpoints you plan on using. 4 | */ 5 | const express = require('express'); 6 | 7 | const router = express.Router(); 8 | 9 | router.get('/', (req, res, next) => { 10 | res.sendStatus(200); 11 | }); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const verifyOAuth = require('../helpers').verifyOAuth; 3 | const mongoose = require('mongoose'); 4 | const config = require('../config'); 5 | 6 | const Shop = mongoose.model('Shop'); 7 | 8 | const router = express.Router(); 9 | 10 | /* GET home page. */ 11 | router.get('/', (req, res, next) => { 12 | const query = Object.keys(req.query).map((key) => `${key}=${req.query[key]}`).join('&'); 13 | if (req.query.shop) { 14 | Shop.findOne({ shopify_domain: req.query.shop, isActive: true }, (err, shop) => { 15 | if (!shop) { 16 | return res.redirect(`/install/?${query}`); 17 | } 18 | if (verifyOAuth(req.query)) { 19 | return res.render('app/app', { apiKey: config.SHOPIFY_API_KEY, appName: config.APP_NAME, shop }); 20 | } 21 | return res.render('index', { title: req.query.shop }); 22 | }); 23 | } else { 24 | return res.render('index', { title: 'Welcome to your example app' }); 25 | } 26 | }); 27 | 28 | router.get('/error', (req, res) => res.render('error', { message: 'Something went wrong!' })); 29 | 30 | module.exports = router; 31 | -------------------------------------------------------------------------------- /routes/install.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Shop = require('../models/Shop'); 3 | const Shopify = require('shopify-node-api'); 4 | const config = require('../config'); 5 | const generateNonce = require('../helpers').generateNonce; 6 | const buildWebhook = require('../helpers').buildWebhook; 7 | 8 | const router = express.Router(); 9 | 10 | router.get('/', (req, res) => { 11 | const shopName = req.query.shop; 12 | const nonce = generateNonce(); 13 | const query = Shop.findOne({ shopify_domain: shopName }).exec(); 14 | const shopAPI = new Shopify({ 15 | shop: shopName, 16 | shopify_api_key: config.SHOPIFY_API_KEY, 17 | shopify_shared_secret: config.SHOPIFY_SHARED_SECRET, 18 | shopify_scope: config.APP_SCOPE, 19 | nonce, 20 | redirect_uri: `${config.APP_URI}/install/callback`, 21 | }); 22 | const redirectURI = shopAPI.buildAuthURL(); 23 | 24 | query.then((response) => { 25 | let save; 26 | const shop = response; 27 | if (!shop) { 28 | save = new Shop({ shopify_domain: shopName, nonce }).save(); 29 | } else { 30 | shop.shopify_domain = shopName; 31 | shop.nonce = nonce; 32 | save = shop.save(); 33 | } 34 | return save.then(() => res.redirect(redirectURI)); 35 | }); 36 | }); 37 | 38 | router.get('/callback', (req, res) => { 39 | const params = req.query; 40 | const query = Shop.findOne({ shopify_domain: params.shop }).exec(); 41 | query.then((result) => { 42 | const shop = result; 43 | const shopAPI = new Shopify({ 44 | shop: params.shop, 45 | shopify_api_key: config.SHOPIFY_API_KEY, 46 | shopify_shared_secret: config.SHOPIFY_SHARED_SECRET, 47 | nonce: shop.nonce, 48 | }); 49 | shopAPI.exchange_temporary_token(params, (error, data) => { 50 | if (error) { 51 | console.log(error); 52 | res.redirect('/error'); 53 | } 54 | console.log(data); 55 | shop.accessToken = data.access_token; 56 | shop.isActive = true; 57 | shop.save((saveError) => { 58 | if (saveError) { 59 | console.log('Cannot save shop: ', saveError); 60 | res.redirect('/error'); 61 | } 62 | if (config.APP_STORE_NAME) { 63 | res.redirect(`https://${shop.shopify_domain}/admin/apps/${config.APP_STORE_NAME}`); 64 | } else { 65 | res.redirect(`https://${shop.shopify_domain}/admin/apps`); 66 | } 67 | }); 68 | }); 69 | }); 70 | }); 71 | 72 | module.exports = router; 73 | -------------------------------------------------------------------------------- /routes/proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ./routes/proxy.js 3 | * This is where you'll set up anything to do with your app proxy if you have one set up. 4 | */ 5 | const express = require('express'); 6 | 7 | const router = express.Router(); 8 | 9 | // Send everything from this route back as liquid. 10 | router.use((req, res, next) => { 11 | res.set('Content-Type', 'application/liquid'); 12 | return next(); 13 | }); 14 | 15 | router.get('/', (req, res, next) => { 16 | res.sendStatus(200); 17 | next(); 18 | }); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /routes/webhook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ./routes/webhooks.js 3 | * This is where you'll set up any webhook endpoints you plan on using. This includes the middleware 4 | * to check if a request from a webhook is legitemate. 5 | */ 6 | const express = require('express'); 7 | const verifyWebhook = require('../middleware').verifyWebhook; 8 | 9 | const router = express.Router(); 10 | 11 | router.post('/', verifyWebhook, (req, res) => { 12 | // This is where you should do something with your webhooks. Filter by Topic etc. 13 | return res.sendStatus(200); 14 | }); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | // start.js 2 | const mongoose = require('mongoose'); 3 | const throng = require('throng'); 4 | require('dotenv').config({ path: '.env' }); 5 | 6 | mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost/purchaseOrders', { 7 | useMongoClient: true, 8 | }); 9 | mongoose.Promise = require('bluebird'); 10 | 11 | mongoose.connection.on('error', (err) => { 12 | console.error(`🚫 Database Error 🚫 → ${err}`); 13 | }); 14 | 15 | function start() { 16 | /* You should require your models here so you don't have to initialise them all the time in 17 | different controlers*/ 18 | require('./models/Shop'); 19 | 20 | const app = require('./app'); 21 | app.set('port', process.env.PORT || 7777); 22 | const server = app.listen(app.get('port'), () => { 23 | console.log(`Express running → PORT ${server.address().port}`); 24 | }); 25 | } 26 | 27 | 28 | throng({ 29 | workers: process.env.WEB_CONCURRENCY || 1, 30 | }, start); 31 | -------------------------------------------------------------------------------- /utils/cleardbs.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Shop = require('../models/Shop'); 3 | const Counter = require('../models/Counter'); 4 | const config = require('../config'); 5 | 6 | mongoose.connect(process.env.MONGODB_URI || `mongodb://localhost/${config.DATABASE_NAME}`); 7 | 8 | Shop.remove({}, (error) => { 9 | if (error) { 10 | console.error('Could not remove Shop database entries: ', error); 11 | } else { 12 | console.log('Successfully cleared Shop'); 13 | } 14 | }); 15 | 16 | Counter.remove({}, (error) => { 17 | if (error) { 18 | console.error('Could not remove Counter database entries: ', error); 19 | } else { 20 | console.log('Successfully cleared Counter'); 21 | } 22 | }); 23 | 24 | 25 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Elkfox/shopify-node-app/ab2c566c8a51112b1c6c17b1efa62c05f74e31fd/utils/index.js -------------------------------------------------------------------------------- /utils/seeddbs.js: -------------------------------------------------------------------------------- 1 | // This is the file where you should seed the databases if you want to have any data in them. 2 | // By default it's just set up to seed the "Counter" model which is how we count stores. 3 | // But the same function runs when the app is first started as well. 4 | const mongoose = require('mongoose'); 5 | const Counter = require('../models/Counter'); 6 | const config = require('../config'); 7 | 8 | mongoose.connect(process.env.MONGODB_URI || `mongodb://localhost/${config.DATABASE_NAME}`); 9 | 10 | // Initialise the counter if it hasn't been initialised yet. 11 | Counter.remove({}, (error) => { 12 | if (error) { 13 | console.error('Could not clear Counter database: ', error); 14 | } else { 15 | new Counter({ _id: 'storeId' }).save((err) => { 16 | if (err) { 17 | console.error('Could not seed Counter Database: ', err); 18 | } 19 | }); 20 | } 21 | }); 22 | 23 | Counter.findById({ _id: 'storeCount' }, (err, id) => { 24 | if (id === null) { 25 | const storeCount = new Counter({ _id: 'storeIds' }); 26 | storeCount.save((error) => { 27 | if (err) { 28 | console.log('Error populating counter database: ', error); 29 | } 30 | }); 31 | } else if (err) { 32 | console.log('Cannot find ID? ', err); 33 | } else { 34 | console.log('Database already populated'); 35 | } 36 | }); -------------------------------------------------------------------------------- /views/app/app.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{appName}} 8 | 9 | 15 | 16 | 17 |

Welcome to {{ appName }}

18 |

You're currently logged in as: {{ shop.shopify_domain }}

19 | 20 | -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |

{{message}}

2 |

{{error.status}}

3 |
{{error.stack}}
4 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |

Welcome to {{title}}

3 | -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{title}} 5 | 6 | 7 | 8 | {{{body}}} 9 | 10 | 11 | --------------------------------------------------------------------------------