diff --git a/.env.testing.example b/.env.testing.example index 934905d..cbe4134 100644 --- a/.env.testing.example +++ b/.env.testing.example @@ -1,2 +1,3 @@ STRIPE_TEST_KEY= STRIPE_TEST_SECRET= +STRIPE_API_BASE="stripemock:12111" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index efe6738..86170ff 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -30,6 +30,12 @@ jobs: testbench: 6.* phpunit: 9.* + services: + stripemock: + image: stripemock/stripe-mock:latest + ports: + - 12111 + steps: - name: Checkout code uses: actions/checkout@v1 @@ -54,6 +60,7 @@ jobs: run: vendor/bin/phpunit env: STRIPE_TEST_SECRET: "${{ secrets.STRIPE_SECRET }}" + STRIPE_API_BASE: "127.0.0.1:${{ job.services.stripemock.ports[12111] }}" - name: Send Slack notification uses: 8398a7/action-slack@v3 @@ -63,6 +70,6 @@ jobs: author_name: PHP ${{ matrix.php }}, Laravel ${{ matrix.laravel }} fields: repo,message,commit,author,eventName,workflow env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MATRIX_CONTEXT: ${{ toJson(matrix) }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..34f1db2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,57 @@ +name: Lint + +on: + push: + branches: [ main ] + pull_request: + +jobs: + phpcs: + name: PHPCS + + continue-on-error: false + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.0 + extensions: posix, dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer config "http-basic.nova.laravel.com" "${{ secrets.NOVA_USERNAME }}" "${{ secrets.NOVA_PASSWORD }}" + composer upgrade --no-interaction --no-suggest + + - name: PHP-8 compatible PHPCS + run: vendor/bin/phpcs + + tlint: + name: TLint + + continue-on-error: false + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.0 + extensions: posix, dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer config "http-basic.nova.laravel.com" "${{ secrets.NOVA_USERNAME }}" "${{ secrets.NOVA_PASSWORD }}" + composer install --no-interaction --no-suggest + + - name: Tlint Lint + run: vendor/bin/tlint diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..f18a9bf --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,11 @@ + + + src + routes + tests + + + + *.php + + diff --git a/charges-detail.png b/charges-detail.png index 21a0281..7c87f65 100644 Binary files a/charges-detail.png and b/charges-detail.png differ diff --git a/charges-index.png b/charges-index.png index 634e99a..2bf2ade 100644 Binary files a/charges-index.png and b/charges-index.png differ diff --git a/composer.json b/composer.json index 0ec513e..cdecca3 100644 --- a/composer.json +++ b/composer.json @@ -3,6 +3,7 @@ "description": "A tool to create a quick Stripe dashboard in your Laravel Nova admin panels", "keywords": [ "tightenco", + "tighten", "laravel", "nova", "stripe" @@ -12,6 +13,10 @@ { "name": "Samantha Geitz", "email": "samantha@tighten.co" + }, + { + "name": "Alison Kirk", + "email": "alison.kirk@tighten.co" } ], "repositories": [ @@ -25,10 +30,11 @@ "stripe/stripe-php": ">=5.0" }, "require-dev": { - "phpunit/phpunit": "9.5.*", - "orchestra/testbench": ">=3.6.x-dev", "laravel/framework": ">=6.20.26", - "laravel/nova": "*@dev" + "laravel/nova": "<4", + "orchestra/testbench": ">=3.6.x-dev", + "phpunit/phpunit": "9.5.*", + "tightenco/duster": "^0.3.2" }, "autoload": { "psr-4": { @@ -48,7 +54,10 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } }, "minimum-stability": "dev", "prefer-stable": true diff --git a/customers-detail.png b/customers-detail.png new file mode 100644 index 0000000..40ff7a0 Binary files /dev/null and b/customers-detail.png differ diff --git a/customers-index.png b/customers-index.png new file mode 100644 index 0000000..c4ecb57 Binary files /dev/null and b/customers-index.png differ diff --git a/dist/js/tool.js b/dist/js/tool.js index 34ac37f..37eb38e 100644 --- a/dist/js/tool.js +++ b/dist/js/tool.js @@ -1,3193 +1 @@ -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { -/******/ configurable: false, -/******/ enumerable: true, -/******/ get: getter -/******/ }); -/******/ } -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 4); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports) { - -/* globals __VUE_SSR_CONTEXT__ */ - -// IMPORTANT: Do NOT use ES2015 features in this file. -// This module is a runtime utility for cleaner component module output and will -// be included in the final webpack user bundle. - -module.exports = function normalizeComponent ( - rawScriptExports, - compiledTemplate, - functionalTemplate, - injectStyles, - scopeId, - moduleIdentifier /* server only */ -) { - var esModule - var scriptExports = rawScriptExports = rawScriptExports || {} - - // ES6 modules interop - var type = typeof rawScriptExports.default - if (type === 'object' || type === 'function') { - esModule = rawScriptExports - scriptExports = rawScriptExports.default - } - - // Vue.extend constructor export interop - var options = typeof scriptExports === 'function' - ? scriptExports.options - : scriptExports - - // render functions - if (compiledTemplate) { - options.render = compiledTemplate.render - options.staticRenderFns = compiledTemplate.staticRenderFns - options._compiled = true - } - - // functional template - if (functionalTemplate) { - options.functional = true - } - - // scopedId - if (scopeId) { - options._scopeId = scopeId - } - - var hook - if (moduleIdentifier) { // server build - hook = function (context) { - // 2.3 injection - context = - context || // cached call - (this.$vnode && this.$vnode.ssrContext) || // stateful - (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional - // 2.2 with runInNewContext: true - if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { - context = __VUE_SSR_CONTEXT__ - } - // inject component styles - if (injectStyles) { - injectStyles.call(this, context) - } - // register component module identifier for async chunk inferrence - if (context && context._registeredComponents) { - context._registeredComponents.add(moduleIdentifier) - } - } - // used by ssr in case component is cached and beforeCreate - // never gets called - options._ssrRegister = hook - } else if (injectStyles) { - hook = injectStyles - } - - if (hook) { - var functional = options.functional - var existing = functional - ? options.render - : options.beforeCreate - - if (!functional) { - // inject component registration as beforeCreate hook - options.beforeCreate = existing - ? [].concat(existing, hook) - : [hook] - } else { - // for template-only hot-reload because in that case the render fn doesn't - // go through the normalizer - options._injectStyles = hook - // register for functioal component in vue file - options.render = function renderWithStyleInjection (h, context) { - hook.call(context) - return existing(h, context) - } - } - } - - return { - esModule: esModule, - exports: scriptExports, - options: options - } -} - - -/***/ }), -/* 1 */ -/***/ (function(module, exports) { - -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -// css base code, injected by the css-loader -module.exports = function(useSourceMap) { - var list = []; - - // return the list of modules as css string - list.toString = function toString() { - return this.map(function (item) { - var content = cssWithMappingToString(item, useSourceMap); - if(item[2]) { - return "@media " + item[2] + "{" + content + "}"; - } else { - return content; - } - }).join(""); - }; - - // import a list of modules into the list - list.i = function(modules, mediaQuery) { - if(typeof modules === "string") - modules = [[null, modules, ""]]; - var alreadyImportedModules = {}; - for(var i = 0; i < this.length; i++) { - var id = this[i][0]; - if(typeof id === "number") - alreadyImportedModules[id] = true; - } - for(i = 0; i < modules.length; i++) { - var item = modules[i]; - // skip already imported module - // this implementation is not 100% perfect for weird media query combinations - // when a module is imported multiple times with different media queries. - // I hope this will never occur (Hey this way we have smaller bundles) - if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) { - if(mediaQuery && !item[2]) { - item[2] = mediaQuery; - } else if(mediaQuery) { - item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; - } - list.push(item); - } - } - }; - return list; -}; - -function cssWithMappingToString(item, useSourceMap) { - var content = item[1] || ''; - var cssMapping = item[3]; - if (!cssMapping) { - return content; - } - - if (useSourceMap && typeof btoa === 'function') { - var sourceMapping = toComment(cssMapping); - var sourceURLs = cssMapping.sources.map(function (source) { - return '/*# sourceURL=' + cssMapping.sourceRoot + source + ' */' - }); - - return [content].concat(sourceURLs).concat([sourceMapping]).join('\n'); - } - - return [content].join('\n'); -} - -// Adapted from convert-source-map (MIT) -function toComment(sourceMap) { - // eslint-disable-next-line no-undef - var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))); - var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64; - - return '/*# ' + data + ' */'; -} - - -/***/ }), -/* 2 */ -/***/ (function(module, exports, __webpack_require__) { - -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra - Modified by Evan You @yyx990803 -*/ - -var hasDocument = typeof document !== 'undefined' - -if (typeof DEBUG !== 'undefined' && DEBUG) { - if (!hasDocument) { - throw new Error( - 'vue-style-loader cannot be used in a non-browser environment. ' + - "Use { target: 'node' } in your Webpack config to indicate a server-rendering environment." - ) } -} - -var listToStyles = __webpack_require__(9) - -/* -type StyleObject = { - id: number; - parts: Array -} - -type StyleObjectPart = { - css: string; - media: string; - sourceMap: ?string -} -*/ - -var stylesInDom = {/* - [id: number]: { - id: number, - refs: number, - parts: Array<(obj?: StyleObjectPart) => void> - } -*/} - -var head = hasDocument && (document.head || document.getElementsByTagName('head')[0]) -var singletonElement = null -var singletonCounter = 0 -var isProduction = false -var noop = function () {} -var options = null -var ssrIdKey = 'data-vue-ssr-id' - -// Force single-tag solution on IE6-9, which has a hard limit on the # of diff --git a/resources/js/components/ColumnSelect.vue b/resources/js/components/ColumnSelect.vue new file mode 100644 index 0000000..5452718 --- /dev/null +++ b/resources/js/components/ColumnSelect.vue @@ -0,0 +1,34 @@ + + + diff --git a/resources/js/components/CustomerDetailCard.vue b/resources/js/components/CustomerDetailCard.vue new file mode 100644 index 0000000..377fa0f --- /dev/null +++ b/resources/js/components/CustomerDetailCard.vue @@ -0,0 +1,112 @@ + + + diff --git a/resources/js/components/CustomersTable.vue b/resources/js/components/CustomersTable.vue new file mode 100644 index 0000000..faa7438 --- /dev/null +++ b/resources/js/components/CustomersTable.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/resources/js/components/ChargesPaginationLinks.vue b/resources/js/components/PaginationLinks.vue similarity index 93% rename from resources/js/components/ChargesPaginationLinks.vue rename to resources/js/components/PaginationLinks.vue index 13c1c74..cf72cd6 100644 --- a/resources/js/components/ChargesPaginationLinks.vue +++ b/resources/js/components/PaginationLinks.vue @@ -1,6 +1,6 @@ - - diff --git a/resources/views/navigation.blade.php b/resources/views/navigation.blade.php index c1f7fc8..31665e8 100644 --- a/resources/views/navigation.blade.php +++ b/resources/views/navigation.blade.php @@ -14,3 +14,9 @@ class="cursor-pointer flex items-center font-normal dim text-white mb-6 text-bas Stripe + + + + Customers + + diff --git a/routes/api.php b/routes/api.php index ddc57aa..a489bb8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,9 +1,9 @@ $this->apiKey]); } catch (Exception $e) { - } } @@ -41,7 +41,6 @@ public function getCharge($id) try { return Charge::retrieve(['id' => $id, 'expand' => ['balance_transaction']], ['api_key' => $this->apiKey]); } catch (Exception $e) { - } } @@ -50,7 +49,14 @@ public function getBalance() try { return Balance::retrieve(['api_key' => $this->apiKey]); } catch (Exception $e) { + } + } + public function listCustomers($options = []) + { + try { + return Customer::all($options, ['api_key' => $this->apiKey]); + } catch (Exception $e) { } } @@ -59,7 +65,14 @@ public function refundCharge($chargeId) try { return Refund::create(['charge' => $chargeId], ['api_key' => $this->apiKey]); } catch (Exception $e) { + } + } + public function getCustomer($id) + { + try { + return Customer::retrieve($id, ['api_key' => $this->apiKey]); + } catch (Exception $e) { } } @@ -68,7 +81,6 @@ public function createCharge(array $params) try { return Charge::create($params, ['api_key' => $this->apiKey]); } catch (Exception $e) { - } } } diff --git a/src/Http/StripeBalanceController.php b/src/Http/StripeBalanceController.php index dba36e8..ba6eb1c 100644 --- a/src/Http/StripeBalanceController.php +++ b/src/Http/StripeBalanceController.php @@ -9,6 +9,6 @@ class StripeBalanceController extends Controller { public function index() { - return response()->json(['balance' => (new StripeClient)->getBalance()]); + return response()->json(['balance' => (new StripeClient())->getBalance()]); } } diff --git a/src/Http/StripeChargesController.php b/src/Http/StripeChargesController.php index b9eef91..283d75b 100644 --- a/src/Http/StripeChargesController.php +++ b/src/Http/StripeChargesController.php @@ -3,14 +3,13 @@ namespace Tighten\NovaStripe\Http; use Illuminate\Routing\Controller; -use Stripe\Charge; use Tighten\NovaStripe\Clients\StripeClient; class StripeChargesController extends Controller { public function index() { - $charges = (new StripeClient)->listCharges( + $charges = (new StripeClient())->listCharges( request()->only('created', 'customer', 'ending_before', 'limit', 'source', 'starting_after', 'transfer_group') ); @@ -19,7 +18,7 @@ public function index() public function show($id) { - return response()->json(['charge' => (new StripeClient)->getCharge($id)]); + return response()->json(['charge' => (new StripeClient())->getCharge($id)]); } public function refund($id) diff --git a/src/Http/StripeCustomersController.php b/src/Http/StripeCustomersController.php new file mode 100644 index 0000000..a606df9 --- /dev/null +++ b/src/Http/StripeCustomersController.php @@ -0,0 +1,21 @@ +json(['customers' => (new StripeClient)->listCustomers()]); + } + + public function show($id) + { + $customer = (new StripeClient)->getCustomer($id); + + return response()->json(['customer' => $customer]); + } +} diff --git a/src/NovaStripe.php b/src/NovaStripe.php index b7ff272..0847d0a 100644 --- a/src/NovaStripe.php +++ b/src/NovaStripe.php @@ -14,8 +14,8 @@ class NovaStripe extends Tool */ public function boot() { - Nova::script('nova-stripe', __DIR__.'/../dist/js/tool.js'); - Nova::style('nova-stripe', __DIR__.'/../dist/css/tool.css'); + Nova::script('nova-stripe', __DIR__ . '/../dist/js/tool.js'); + Nova::style('nova-stripe', __DIR__ . '/../dist/css/tool.css'); } /** diff --git a/src/ToolServiceProvider.php b/src/ToolServiceProvider.php index 84b0d9f..41dad20 100644 --- a/src/ToolServiceProvider.php +++ b/src/ToolServiceProvider.php @@ -2,10 +2,10 @@ namespace Tighten\NovaStripe; -use Laravel\Nova\Nova; -use Laravel\Nova\Events\ServingNova; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; +use Laravel\Nova\Events\ServingNova; +use Laravel\Nova\Nova; use Tighten\NovaStripe\Http\Middleware\Authorize; class ToolServiceProvider extends ServiceProvider @@ -17,7 +17,7 @@ class ToolServiceProvider extends ServiceProvider */ public function boot() { - $this->loadViewsFrom(__DIR__.'/../resources/views', 'nova-stripe'); + $this->loadViewsFrom(__DIR__ . '/../resources/views', 'nova-stripe'); $this->app->booted(function () { $this->routes(); @@ -41,7 +41,7 @@ protected function routes() Route::middleware(['nova', Authorize::class]) ->prefix('nova-vendor/nova-stripe') - ->group(__DIR__.'/../routes/api.php'); + ->group(__DIR__ . '/../routes/api.php'); } /**