├── .gitignore
├── .idea
├── laravel-early-access.iml
└── php.xml
├── .scrutinizer.yml
├── .styleci.yml
├── .travis.yml
├── art
└── logo.png
├── changelog.md
├── composer.json
├── config
└── early-access.php
├── contributing.md
├── database
└── migrations
│ └── 2019_01_03_114743_create_early_access_table.php
├── license.md
├── mix-manifest.json
├── package.json
├── phpunit.xml.dist
├── public
├── css
│ └── early-access.css
└── svg
│ └── placeholder.svg
├── readme.md
├── resources
├── lang
│ └── en
│ │ ├── common.php
│ │ ├── mail.php
│ │ └── messages.php
├── sass
│ └── early-access.scss
├── svg
│ └── placeholder.svg
└── views
│ ├── index.blade.php
│ ├── layouts
│ └── main.blade.php
│ └── unsubscribe.blade.php
├── routes
└── web.php
├── src
├── Console
│ └── Commands
│ │ └── EarlyAccess.php
├── Contracts
│ └── Subscription
│ │ ├── SubscriptionProvider.php
│ │ └── SubscriptionRepository.php
├── EarlyAccess.php
├── EarlyAccessServiceProvider.php
├── Events
│ ├── UserSubscribed.php
│ └── UserUnsubscribed.php
├── Facades
│ └── EarlyAccess.php
├── Http
│ ├── Controllers
│ │ └── EarlyAccessController.php
│ └── Middleware
│ │ └── CheckForEarlyAccessMode.php
├── Listeners
│ ├── SendSubscriptionNotification.php
│ └── SendUnsubscribeNotification.php
├── Notifications
│ ├── UserSubscribed.php
│ └── UserUnsubscribed.php
├── Subscriber.php
├── SubscriptionServices
│ ├── DatabaseService.php
│ └── Repositories
│ │ └── Database
│ │ └── EloquentRepository.php
└── Traits
│ └── InteractsWithEarlyAccess.php
├── tailwind.js
├── tests
├── Feature
│ └── CommandsTest.php
├── TestCase.php
└── Unit
│ ├── EarlyAccessTest.php
│ ├── SubscribeTest.php
│ └── SubscriberTest.php
└── webpack.mix.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
3 | /vendor
4 | phpunit.xml
5 | yarn-error.log
6 | yarn.lock
7 |
--------------------------------------------------------------------------------
/.idea/laravel-early-access.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | filter:
2 | excluded_paths: [tests/*]
3 |
4 | checks:
5 | php:
6 | code_rating: true
7 | remove_extra_empty_lines: true
8 | remove_php_closing_tag: true
9 | remove_trailing_whitespace: true
10 | fix_use_statements:
11 | remove_unused: true
12 | preserve_multiple: false
13 | preserve_blanklines: true
14 | order_alphabetically: true
15 | fix_php_opening_tag: true
16 | fix_linefeed: true
17 | fix_line_ending: true
18 | fix_identation_4spaces: true
19 | fix_doc_comments: true
20 |
21 | tools:
22 | php_analyzer: true
23 | php_code_coverage: true
24 | php_code_sniffer:
25 | config:
26 | standard: PSR4
27 | filter:
28 | paths: ['src']
29 | php_loc:
30 | enabled: true
31 | excluded_dirs: [vendor, tests]
32 | php_cpd:
33 | enabled: true
34 | excluded_dirs: [vendor, tests]
35 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: laravel
2 |
3 | disabled:
4 | - concat_without_spaces
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.1
5 | - 7.2
6 | - 7.3
7 |
8 | cache:
9 | directories:
10 | - $HOME/.composer/cache
11 |
12 | env:
13 | - TESTBENCH_VERSION=3.6.* PHPUNIT_VERSION=~7 STABILITY=stable
14 | - TESTBENCH_VERSION=dev-master PHPUNIT_VERSION=~7 STABILITY=dev
15 |
16 | matrix:
17 | allow_failures:
18 | - env: TESTBENCH_VERSION=dev-master PHPUNIT_VERSION=~7 STABILITY=dev
19 | - php: nightly
20 | fast_finish: true
21 |
22 | before_script:
23 | - travis_retry composer self-update
24 | - composer config minimum-stability ${STABILITY}
25 | - travis_retry composer require "orchestra/testbench:${TESTBENCH_VERSION}" "phpunit/phpunit:${PHPUNIT_VERSION}" --no-update
26 | - travis_retry composer update --no-interaction --prefer-source
27 |
28 | script:
29 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
30 |
31 | after_script:
32 | - wget https://scrutinizer-ci.com/ocular.phar
33 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover
34 |
--------------------------------------------------------------------------------
/art/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neoighodaro/laravel-early-access/c28e2f5ed9bf3683f7ce384be5bda2bb32710901/art/logo.png
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes will be documented in this file.
4 |
5 | ## Version v1.1.1
6 |
7 | ## Fixed
8 |
9 | - Typo in the translations file.
10 | - Fixed bug where wrong name space was used for publishing some vendor files.
11 |
12 | ### Fixed
13 |
14 | ## Version 1.0.2
15 |
16 | ### Fixed
17 |
18 | - Error with setting `/` as the early access mode URL.
19 |
20 | ## Version 1.0.1
21 |
22 | ### Fixed
23 |
24 | - Error that occurs when you run the early access activate Artisan command twice in a row with empty allowed networks.
25 |
26 | ## Version 1.0.0
27 |
28 | ### Added
29 |
30 | - Everything
31 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "neo/laravel-early-access",
3 | "description": "Adds an early access page to your Laravel application.",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Neo Ighodaro",
8 | "email": "neo@creativitykills.co",
9 | "homepage": "https://creativitykills.co",
10 | "role": "Developer"
11 | }
12 | ],
13 | "homepage": "https://github.com/neo/laravel-early-access",
14 | "keywords": [
15 | "ck",
16 | "creativitykills",
17 | "early-access",
18 | "landing-page"
19 | ],
20 | "require": {
21 | "php": "^7.2|^8.0",
22 | "ext-json": "*",
23 | "illuminate/support": "^6.0|^7.0|^8.0"
24 | },
25 | "require-dev": {
26 | "phpunit/phpunit": "^8.0|^9.0",
27 | "mockery/mockery": "^1.1",
28 | "orchestra/testbench": "^4.0|^5.0",
29 | "sempro/phpunit-pretty-print": "^1.0"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "Neo\\EarlyAccess\\": "src/"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "Neo\\EarlyAccess\\Tests\\": "tests"
39 | }
40 | },
41 | "extra": {
42 | "laravel": {
43 | "providers": [
44 | "Neo\\EarlyAccess\\EarlyAccessServiceProvider"
45 | ],
46 | "aliases": {
47 | "EarlyAccess": "Neo\\EarlyAccess\\Facades\\EarlyAccess"
48 | }
49 | }
50 | },
51 | "config": {
52 | "sort-packages": true
53 | },
54 | "minimum-stability": "dev",
55 | "prefer-stable": true
56 | }
57 |
--------------------------------------------------------------------------------
/config/early-access.php:
--------------------------------------------------------------------------------
1 | env('EARLY_ACCESS_ENABLED', false),
9 |
10 | /**
11 | * The URL to use as the early access page
12 | */
13 | 'url' => env('EARLY_ACCESS_URL', '/early-access'),
14 |
15 | /**
16 | * URL to your applications login page.
17 | */
18 | 'login_url' => env('EARLY_ACCESS_LOGIN_URL', '/login'),
19 |
20 | /**
21 | * Twitter handle without the @. This will be added to the share message included with the subscription message.
22 | */
23 | 'twitter_handle' => env('EARLY_ACCESS_TWITTER_HANDLE'),
24 |
25 | /**
26 | * The early access view to load.
27 | */
28 | 'view' => env('EARLY_ACCESS_VIEW', 'early-access::index'),
29 |
30 | /**
31 | * Service driver to use.
32 | *
33 | * Supported: database
34 | *
35 | * To add your own driver, create a class that implements the `Neo\EarlyAccess\Contracts\Subscription\SubscriptionProvider`
36 | * contract. Register the class in your `AppServiceProvider` making sure to prepend the name with 'early-access.'.
37 | * Example:
38 | *
39 | * $this->app->instance('early-access.service-name', function() {
40 | * return new ServiceClass;
41 | * });
42 | *
43 | * To set your custom service, change the value below to your service name (without the 'early-access.').
44 | */
45 | 'service' => env('EARLY_ACCESS_SERVICE_DRIVER', 'database'),
46 |
47 | 'services' => [
48 |
49 | /**
50 | * Database settings...
51 | */
52 | 'database' => [
53 | 'table_name' => env('EARLY_ACCESS_SERVICE_DB_TABLE', 'subscribers'),
54 | ],
55 |
56 | ],
57 |
58 | /**
59 | * Notification classes to be called when a user completes an action.
60 | */
61 | 'notifications' => [
62 | 'subscribed' => Neo\EarlyAccess\Notifications\UserSubscribed::class,
63 | 'unsubscribed' => Neo\EarlyAccess\Notifications\UserUnsubscribed::class,
64 | ],
65 |
66 | ];
67 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are welcome and will be fully credited.
4 |
5 | Contributions are accepted via Pull Requests on [Github](https://github.com/neoighodaro/laravel-early-access).
6 |
7 | # Things you could do
8 | If you want to contribute but do not know where to start, this list provides some starting points.
9 | - Set up TravisCI, StyleCI, ScrutinizerCI
10 | - Write a more comprehensive ReadMe
11 |
12 | ## Pull Requests
13 |
14 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
15 |
16 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date.
17 |
18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
19 |
20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
21 |
22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
23 |
24 |
25 | **Happy coding**!
26 |
--------------------------------------------------------------------------------
/database/migrations/2019_01_03_114743_create_early_access_table.php:
--------------------------------------------------------------------------------
1 | table = config('early-access.services.database.table_name');
20 | }
21 |
22 | /**
23 | * Run the migrations.
24 | *
25 | * @return void
26 | */
27 | public function up()
28 | {
29 | Schema::create($this->table, function (Blueprint $table) {
30 | $table->increments('id');
31 | $table->string('name')->nullable();
32 | $table->string('email');
33 | $table->dateTime('subscribed_at')->nullable();
34 | $table->dateTime('verified_at')->nullable();
35 | $table->softDeletes();
36 |
37 | $table->unique(['email', 'deleted_at']);
38 | });
39 | }
40 |
41 | /**
42 | * Reverse the migrations.
43 | *
44 | * @return void
45 | */
46 | public function down()
47 | {
48 | Schema::dropIfExists($this->table);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2018 CreativityKills. http://creativitykills.co
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/public/css/early-access.css": "/public/css/early-access.css"
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "tailwind.js",
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
7 | "hot": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
8 | "prod": "npm run production",
9 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
10 | },
11 | "devDependencies": {
12 | "cross-env": "^5.2.0",
13 | "laravel-mix": "^4.0.13",
14 | "laravel-mix-tailwind": "^0.1.0",
15 | "resolve-url-loader": "2.3.1",
16 | "sass": "^1.15.3",
17 | "sass-loader": "7.*",
18 | "tailwindcss": "^0.7.3",
19 | "vue-template-compiler": "^2.5.21"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 | ./tests/Feature
16 |
17 |
18 |
19 |
20 | src/
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/public/svg/placeholder.svg:
--------------------------------------------------------------------------------
1 | social dashboard
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | This package makes it easy to add early access mode to your existing application. This is useful for when you want to launch a product and need to gather the email addresses of people who want early access to the application.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | > Take a look at [contributing.md](contributing.md) to see a to do list.
19 |
20 | >⚠️ This version supports Laravel 6 and above. Use version 1.x if you require Laravel 5 support.
21 |
22 | ## Installation
23 |
24 | #### Via Composer
25 |
26 | To install via composer, run the following command in the root of your Laravel application:
27 |
28 | ```bash
29 | $ composer require neo/laravel-early-access
30 | ```
31 |
32 | Register the middleware `Neo\EarlyAccess\Http\Middleware\CheckForEarlyAccessMode` at the bottom of your `web` group
33 | middleware in `app/Http/Middleware/Kernel.php`.
34 |
35 | ```php
36 | [
40 | \App\Http\Middleware\EncryptCookies::class,
41 |
42 | // [...]
43 |
44 | \Neo\EarlyAccess\Http\Middleware\CheckForEarlyAccessMode::class,
45 | ],
46 |
47 | // [...]
48 | ```
49 |
50 | Next, add/update the `MAIL_*` keys in your `.env` file. Make sure to include `MAIL_FROM_*` keys as it is required when
51 | sending welcome or goodbye emails to subscribers.
52 |
53 | Also, you can optionally add the following environment variables to your `.env` file:
54 |
55 | ```
56 | EARLY_ACCESS_ENABLED=true
57 | EARLY_ACCESS_URL="/early-access"
58 | EARLY_ACCESS_LOGIN_URL="/login"
59 | EARLY_ACCESS_TWITTER_HANDLE=NeoIghodaro
60 | EARLY_ACCESS_VIEW="early-access::index"
61 | EARLY_ACCESS_SERVICE_DRIVER=database
62 | EARLY_ACCESS_SERVICE_DB_TABLE=subscribers
63 | ```
64 |
65 | Now migrate the required tables:
66 |
67 | ```shell
68 | $ php artisan migrate
69 | ```
70 |
71 | And publish the required assets:
72 |
73 | ```shell
74 | $ php artisan vendor:publish --provider="Neo\EarlyAccess\EarlyAccessServiceProvider"
75 | ```
76 |
77 | This will make the config, migrations, views, and assets available inside your applications directory so you can customise them.
78 |
79 | > **TIP:** You can append the `--tag=assets` flag to publish only the asset files which is required. Other available tag
80 | > values are: `config`, `translations`, `migrations`, `views` and `assets`.
81 |
82 | To activate early access, you can do either of the following:
83 |
84 | - Run the command `$ php artisan early-access --activate`
85 | - Set the `EARLY_ACCESS_ENABLED` to true in your `.env` file
86 |
87 | > **TIP:** Using the artisan command allows you to add IP addresses that are allowed to bypass the early access screen altogether.
88 | >
89 | > `$ php artisan early-access --allow=127.0.0.1 --allow=0.0.0.0`
90 | >
91 | > Note that logged in users will also bypass the early access screen.
92 |
93 | ## Configuration
94 |
95 | ```shell
96 | $ php artisan vendor:publish --provider="Neo\EarlyAccess\EarlyAccessServiceProvider" --tag=config
97 | ```
98 |
99 | #### Configuration options
100 |
101 | - `enabled` - Sets whether the mode is enabled or not. In terms of priority, this is the last thing that is checked to
102 | see if the early access screen should be shown. Login status is checked, then artisan command status is checked, then
103 | this value is checked. `default: false`
104 |
105 | - `url` - The URL the early access screen will be shown at. The client will be redirected to this URL if they do not have
106 | access and the mode is enabled. You can set the value to `/` or any other existing routes. `default: /early-access`
107 |
108 | - `login_url` - The URL to your application's login page. This URL will automatically be bypassed even if early access
109 | mode is turned on. `default: /login`
110 |
111 | - `twitter_handle` - This is used when sending subscription confirmation via email. The user will have the option to tweet
112 | with the handle you specify tagged.
113 |
114 | - `view` - The early access screen view to be loaded. You can publish the views and customise it, or leave the default.
115 | `default: early-access::index`.
116 |
117 | - `service` - This is the subscription driver. See below for how to create your own driver. `default: database`.
118 |
119 | - `services.database.table_name` - The database table name. This is useful is you want to change the name of the database
120 | table. You need to do this before you run the migration though. `default: subscribers`
121 |
122 | - `notifications` - The default notification classes. You can use your own notification classes if you would like to
123 | change how users will be notified when they subscribe or unsubscribe.
124 |
125 | ## Using `/` or an existing route as the early access URL
126 |
127 | To use `/` or an existing route in your application as the early access URL, you need to do the following:
128 |
129 | First, register the service provider manually below the `App\Providers\RouteServiceProvider::class` in `config/app.php`.
130 |
131 | ```php
132 | [
137 |
138 | // [...]
139 |
140 | App\Providers\RouteServiceProvider::class,
141 | Neo\EarlyAccess\EarlyAccessServiceProvider::class,
142 |
143 | // [...]
144 |
145 | ],
146 |
147 | // [...]
148 | ];
149 | ```
150 |
151 | Next, open your `composer.json` file and add the package in the `dont-discover` array:
152 |
153 | ```
154 | // [...]
155 |
156 | "laravel": {
157 | "dont-discover": [
158 | "neo/laravel-early-access"
159 | ]
160 | },
161 |
162 | // [...]
163 | ```
164 |
165 | Now run `composer dump-autoload -o` and it should work.
166 |
167 | ## Creating your own subscription service driver
168 |
169 | By default, there is a database driver that manages all the users. You can decide to create your own driver though for other
170 | services like Mailchimp etc. (If you do, please consider submitting a PR with the driver).
171 |
172 | To get started, you need to create a new class that implements the service provider class:
173 |
174 | ```php
175 | app->bind('early-access.mailchimp', function () {
216 | return new \App\Services\SubscriptionServices\MailchimpService;
217 | });
218 |
219 | // [...]
220 | ```
221 |
222 | > **NOTE:** Leave the `early-access.` namespace. It is required. Just append the name of your service to the namespace
223 | > as seen above.
224 |
225 | Next, go to your published configuration and change the service driver from `database` to `mailchimp`. That's all.
226 |
227 | ## Change log
228 |
229 | Please see the [changelog](changelog.md) for more information on what has changed recently.
230 |
231 | ## Testing
232 |
233 | ```bash
234 | $ composer test
235 | ```
236 |
237 | ## Contributing
238 |
239 | Please see [contributing.md](contributing.md) for details and a todolist.
240 |
241 | ## Security
242 |
243 | If you discover any security related issues, please email author email instead of using the issue tracker.
244 |
245 | ## Credits
246 |
247 | - [Neo Ighodaro][link-author]
248 | - [Caneco](https://twitter.com/caneco) (for the logo)
249 | - [All Contributors][link-contributors]
250 |
251 | ## License
252 |
253 | Please see the [license file](license.md) for more information.
254 |
255 | [link-author]: https://github.com/neoighodaro
256 | [link-contributors]: ../../contributors
257 |
--------------------------------------------------------------------------------
/resources/lang/en/common.php:
--------------------------------------------------------------------------------
1 | '© :year :name. All rights reserved.',
12 | 'early_access' => 'Early access',
13 | 'email_address' => 'Email address',
14 | 'login' => 'Log in',
15 |
16 | ];
17 |
--------------------------------------------------------------------------------
/resources/lang/en/mail.php:
--------------------------------------------------------------------------------
1 | [
12 | 'subject' => 'Early access to :name',
13 | 'message' => [
14 | 'intro' => 'Thanks for requesting early access to :name! We will email you with more information when we are ready to have you try it out.',
15 | 'share' => 'Want to share our project with the world? Please help us reach more people by sharing on Twitter: :url',
16 | 'unsubscribe' => 'Unsubscribe from emails',
17 | ],
18 | ],
19 | 'unsubscribed' => [
20 | 'subject' => 'Early access to :name',
21 | 'message' => [
22 | 'intro' => 'You have successfully unsubscribed from our early access list and will not be notified when the product is released.',
23 | ],
24 | ],
25 |
26 | ];
27 |
--------------------------------------------------------------------------------
/resources/lang/en/messages.php:
--------------------------------------------------------------------------------
1 | [
11 | 'error' => 'An error occurred. Please try again.',
12 | 'success' => 'Thanks for subscribing. We will be in touch.',
13 | ],
14 | 'description' => 'Welcome to :name. You can customise this message in the language directory. You can start by reading the installation guide in the readme file.',
15 | 'get_early_access' => 'Get early access',
16 | 'no_spam' => 'No spam mail. We promise.',
17 | 'welcome' => 'Welcome to :name',
18 | 'twitter_share_text' => '.@:handle is coming soon. Request early access to be one of the first people to try it out :url #:handle',
19 |
20 | 'unsubscribe' => [
21 | 'title' => 'Unsubscribe from :name',
22 | 'description' => 'We\'re sorry to se you go. Please confirm with your email.',
23 | 'button' => 'Unsubscribe',
24 | 'alerts' => [
25 | 'error' => 'An error occurred. Please try again.',
26 | 'success' => 'You have been successfully unsubscribed.',
27 | ],
28 |
29 | ],
30 | ];
31 |
--------------------------------------------------------------------------------
/resources/sass/early-access.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * This injects Tailwind's base styles, which is a combination of
3 | * Normalize.css and some additional base styles.
4 | *
5 | * You can see the styles here:
6 | * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css
7 | *
8 | * If using `postcss-import`, use this import instead:
9 | *
10 | * @import "tailwindcss/preflight";
11 | */
12 | @tailwind preflight;
13 |
14 | /**
15 | * This injects any component classes registered by plugins.
16 | *
17 | * If using `postcss-import`, use this import instead:
18 | *
19 | * @import "tailwindcss/components";
20 | */
21 | @tailwind components;
22 |
23 | /**
24 | * Here you would add any of your custom component classes; stuff that you'd
25 | * want loaded *before* the utilities so that the utilities could still
26 | * override them.
27 | *
28 | * Example:
29 | *
30 | * .btn { ... }
31 | * .form-input { ... }
32 | *
33 | * Or if using a preprocessor or `postcss-import`:
34 | *
35 | * @import "components/buttons";
36 | * @import "components/forms";
37 | */
38 |
39 | /**
40 | * This injects all of Tailwind's utility classes, generated based on your
41 | * config file.
42 | *
43 | * If using `postcss-import`, use this import instead:
44 | *
45 | * @import "tailwindcss/utilities";
46 | */
47 | @tailwind utilities;
48 |
49 | /**
50 | * Here you would add any custom utilities you need that don't come out of the
51 | * box with Tailwind.
52 | *
53 | * Example :
54 | *
55 | * .bg-pattern-graph-paper { ... }
56 | * .skew-45 { ... }
57 | *
58 | * Or if using a preprocessor or `postcss-import`:
59 | *
60 | * @import "utilities/background-patterns";
61 | * @import "utilities/skew-transforms";
62 | */
63 |
64 | body {
65 | @apply font-sans text-base text-grey-soft bg-cover bg-no-repeat;
66 | }
67 |
--------------------------------------------------------------------------------
/resources/views/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('early-access::layouts.main')
2 |
3 | @section('content')
4 |
5 |
6 |
7 |
8 | @lang('early-access::messages.welcome', ['name' => config('app.name')])
9 |
10 |
11 | @lang('early-access::messages.description', ['name' => config('app.name')])
12 |
13 |
14 |
15 | @if (session('success'))
16 |
18 | @lang('early-access::messages.alerts.success')
19 |
20 | @endif
21 |
22 | @if ($errors->has('email') or session('error'))
23 |
25 | {{ $errors->has('email') ? $errors->first('email') : trans('early-access::messages.alerts.error') }}
26 |
27 | @endif
28 |
29 |
47 |
48 |
49 | † @lang('early-access::messages.no_spam')
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | @endsection
59 |
--------------------------------------------------------------------------------
/resources/views/layouts/main.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ $page_title ?? trans('early-access::common.early_access') }} | {{ config('app.name') }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
22 |
23 | @if ($loginUrl = config('early-access.login_url'))
24 |
25 |
26 | @lang('early-access::common.login')
27 |
28 |
29 | @endif
30 |
31 |
32 | @yield('content')
33 |
34 |
35 |
36 | @lang('early-access::common.copyright', ['year' => date('Y'), 'name' => config('app.name')])
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/resources/views/unsubscribe.blade.php:
--------------------------------------------------------------------------------
1 | @extends('early-access::layouts.main')
2 |
3 | @section('content')
4 |
5 |
6 |
7 |
8 | @lang('early-access::messages.unsubscribe.title', ['name' => config('app.name')])
9 |
10 |
11 | @lang('early-access::messages.unsubscribe.description', ['name' => config('app.name')])
12 |
13 |
14 |
15 | @if (session('success'))
16 |
18 | @lang('early-access::messages.unsubscribe.alerts.success')
19 |
20 | @endif
21 |
22 | @if ($errors->has('email') or session('error'))
23 |
25 | {{ $errors->has('email') ? $errors->first('email') : trans('early-access::messages.unsubscribe.alerts.error') }}
26 |
27 | @endif
28 |
29 |
48 |
49 |
50 | † @lang('early-access::messages.no_spam')
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | @endsection
60 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | name('early-access.share');
19 |
20 | Route::get($basePath, 'EarlyAccessController@index')
21 | ->name('early-access.index');
22 |
23 | Route::post($basePath, 'EarlyAccessController@subscribe')
24 | ->name('early-access.subscribe');
25 |
26 | Route::get("{$basePath}/unsubscribe", 'EarlyAccessController@unsubscribe')
27 | ->name('early-access.unsubscribe');
28 |
29 | Route::delete("{$basePath}/unsubscribe", 'EarlyAccessController@verifyUnsubscription')
30 | ->name('early-access.verify-unsubscription');
31 | }
32 |
--------------------------------------------------------------------------------
/src/Console/Commands/EarlyAccess.php:
--------------------------------------------------------------------------------
1 | storage = $storage;
47 | }
48 |
49 | /**
50 | * Execute the console command.
51 | *
52 | * @return mixed
53 | */
54 | public function handle()
55 | {
56 | if ($this->argument('status')) {
57 | return $this->getStatus();
58 | }
59 |
60 | $activate = $this->option('activate');
61 |
62 | $deactivate = $this->option('deactivate');
63 |
64 | if (! $activate && ! $deactivate) {
65 | $intent = $this->option('allow')
66 | ? 'activate'
67 | : $this->choice('What do you want to do', ['1' => 'activate', '2' => 'deactivate']);
68 |
69 | ${$intent} = true;
70 | }
71 |
72 | return $activate ? $this->activate() : $this->deactivate();
73 | }
74 |
75 | /**
76 | * Deactivate early access.
77 | */
78 | protected function activate()
79 | {
80 | $this->saveBeacon($this->option('allow') ?? [])
81 | ? $this->comment('Early access activated.')
82 | : $this->error('Unable to activate early access.');
83 | }
84 |
85 | /**
86 | * Activate early access.
87 | */
88 | protected function deactivate()
89 | {
90 | $this->deleteBeacon()
91 | ? $this->comment('Early access deactivated.')
92 | : $this->comment('Could not deactivate. Possibly not active.');
93 |
94 | if (config('early-access.enabled')) {
95 | $this->comment('Set EARLY_ACCESS_ENABLED to false in your .env to fully deactivate it.');
96 | }
97 | }
98 |
99 | /**
100 | * Gets the status of the early access mode.
101 | */
102 | protected function getStatus()
103 | {
104 | $allowedNetworks = with(($data = $this->getBeaconDetails()), function ($details) {
105 | return (isset($details['allowed']) && count($details['allowed']))
106 | ? implode(', ', $details['allowed'])
107 | : 'none';
108 | });
109 |
110 | $this->info($data ? "Active. Allowed networks: {$allowedNetworks}" : 'Not active');
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/Contracts/Subscription/SubscriptionProvider.php:
--------------------------------------------------------------------------------
1 | auth = $auth;
31 |
32 | $this->storage = $storage;
33 | }
34 |
35 | /**
36 | * Checks if early access is enabled or not.
37 | *
38 | * @return bool
39 | */
40 | public function isEnabled(): bool
41 | {
42 | if ($this->auth->user()) {
43 | return false;
44 | }
45 |
46 | return (bool) $this->getBeaconDetails() ?: config('early-access.enabled');
47 | }
48 |
49 | /**
50 | * Returns a list of allowed networks.
51 | *
52 | * @return array
53 | */
54 | public function allowedNetworks(): array
55 | {
56 | $data = $this->getBeaconDetails() ?? [];
57 |
58 | return array_get($data, 'allowed', []);
59 | }
60 |
61 | /**
62 | * Adds a network to the list of allowed networks.
63 | *
64 | * @param array $networks
65 | */
66 | public function addAllowedNetworksToBeacon(array $networks)
67 | {
68 | if ($data = $this->getBeaconDetails()) {
69 | if (! empty($networks)) {
70 | array_push($data['allowed'], ...$networks);
71 | }
72 |
73 | $data['allowed'] = array_unique($data['allowed']);
74 |
75 | return $this->saveBeaconFileWithData($data);
76 | }
77 | }
78 |
79 | /**
80 | * Save the beacon file.
81 | *
82 | * @param array $allowed
83 | * @return bool
84 | */
85 | public function saveBeacon(array $allowed = []): bool
86 | {
87 | if ($this->getBeaconDetails()) {
88 | return (bool) $this->addAllowedNetworksToBeacon($allowed);
89 | }
90 |
91 | return $this->saveBeaconFileWithData([
92 | 'allowed' => array_unique($allowed),
93 | 'time' => $this->currentTime(),
94 | ]);
95 | }
96 |
97 | /**
98 | * Deletes the beacon file.
99 | *
100 | * @return bool
101 | */
102 | public function deleteBeacon(): bool
103 | {
104 | return $this->storage->delete('early-access');
105 | }
106 |
107 | /**
108 | * Get the beacon file details.
109 | *
110 | * @return false|array
111 | */
112 | public function getBeaconDetails()
113 | {
114 | if (! $this->storage->exists('early-access')) {
115 | return false;
116 | }
117 |
118 | return json_decode($this->storage->get('early-access'), true);
119 | }
120 |
121 | /**
122 | * Saves the beacon file.
123 | *
124 | * @param array $data
125 | * @return bool
126 | */
127 | private function saveBeaconFileWithData(array $data): bool
128 | {
129 | return $this->storage->put('early-access', json_encode($data, JSON_PRETTY_PRINT));
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/EarlyAccessServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadMigrationsFrom(__DIR__ . '/../database/migrations');
25 |
26 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'early-access');
27 |
28 | $this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'early-access');
29 |
30 | $this->app['router']->namespace('Neo\\EarlyAccess\\Http\\Controllers')
31 | ->middleware(['web'])
32 | ->group(function () {
33 | $this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
34 | });
35 |
36 | $this->registerEventListeners();
37 |
38 | if ($this->app->runningInConsole()) {
39 | $this->bootForConsole();
40 | }
41 | }
42 |
43 | /**
44 | * Register any package services.
45 | *
46 | * @return void
47 | */
48 | public function register()
49 | {
50 | $this->mergeConfigFrom(__DIR__ . '/../config/early-access.php', 'early-access');
51 |
52 | $this->app->singleton('early-access', function ($app) {
53 | return new EarlyAccess($app['filesystem.disk'], $app['auth.driver']);
54 | });
55 |
56 | $this->app->bind('early-access.database', function () {
57 | return new DatabaseService(new EloquentRepository);
58 | });
59 |
60 | $this->app->bind(SubscriptionProvider::class, function () {
61 | return app('early-access.' . config('early-access.service'));
62 | });
63 | }
64 |
65 | /**
66 | * Get the services provided by the provider.
67 | *
68 | * @return array
69 | */
70 | public function provides()
71 | {
72 | return ['early-access'];
73 | }
74 |
75 | /**
76 | * Console-specific booting.
77 | *
78 | * @return void
79 | */
80 | protected function bootForConsole()
81 | {
82 | $this->publishes([
83 | __DIR__ . '/../config/early-access.php' => config_path('early-access.php'),
84 | ], 'config');
85 |
86 | $this->publishes([
87 | __DIR__ . '/../resources/views' => base_path('resources/views/vendor/early-access'),
88 | ], 'views');
89 |
90 | $this->publishes([
91 | __DIR__ . '/../public' => public_path('vendor/early-access'),
92 | ], 'assets');
93 |
94 | $this->publishes([
95 | __DIR__ . '/../database/migrations/' => database_path('migrations'),
96 | ], 'migrations');
97 |
98 | $this->publishes([
99 | __DIR__ . '/../resources/lang' => resource_path('lang/vendor/early-access'),
100 | ], 'translations');
101 |
102 | $this->commands([
103 | Commands\EarlyAccess::class,
104 | ]);
105 | }
106 |
107 | /**
108 | * Register the packages event listeners.
109 | */
110 | protected function registerEventListeners()
111 | {
112 | $this->app['events']->listen(UserSubscribed::class, SendSubscriptionNotification::class);
113 |
114 | $this->app['events']->listen(UserUnsubscribed::class, SendUnsubscribeNotification::class);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/Events/UserSubscribed.php:
--------------------------------------------------------------------------------
1 | subscriber = $subscriber;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Events/UserUnsubscribed.php:
--------------------------------------------------------------------------------
1 | subscriber = $subscriber;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Facades/EarlyAccess.php:
--------------------------------------------------------------------------------
1 | getBeaconDetails();
22 |
23 | return view(config('early-access.view'), compact('details'));
24 | }
25 |
26 | /**
27 | * Subscribe.
28 | *
29 | * @param \Illuminate\Http\Request $request
30 | * @return \Illuminate\Http\JsonResponse
31 | */
32 | public function subscribe(Request $request)
33 | {
34 | $data = $request->validate([
35 | 'email' => 'required|email',
36 | 'name' => 'string|between:3,100',
37 | ]);
38 |
39 | if (!$subscriber = Subscriber::make()->findByEmail($data['email'])) {
40 | $subscriber = Subscriber::make($data);
41 | $subscriber->subscribe();
42 | }
43 |
44 | return redirect()->route('early-access.index')->withSuccess(true);
45 | }
46 |
47 | /**
48 | * Show the early access page.
49 | *
50 | * @return \Illuminate\Http\Response
51 | */
52 | public function unsubscribe()
53 | {
54 | $details = $this->getBeaconDetails();
55 |
56 | return view('early-access::unsubscribe', compact('details'));
57 | }
58 |
59 | /**
60 | * Unsubscribe.
61 | *
62 | * @param \Neo\EarlyAccess\Subscriber $subscriber
63 | * @param \Illuminate\Http\Request $request
64 | * @return \Illuminate\Http\JsonResponse
65 | */
66 | public function verifyUnsubscription(Subscriber $subscriber, Request $request)
67 | {
68 | $data = $request->validate(['email' => 'required|email']);
69 |
70 | $unsubscribed = with($subscriber->findByEmail($data['email']), function ($user) {
71 | return $user ? $user->unsubscribe() : false;
72 | });
73 |
74 | return redirect()->route('early-access.unsubscribe')->with([
75 | ($unsubscribed ? 'success' : 'error') => true,
76 | ]);
77 | }
78 |
79 | /**
80 | * Share to twitter.
81 | *
82 | * @return \Illuminate\Http\RedirectResponse
83 | */
84 | public function shareOnTwitter()
85 | {
86 | if ($handle = config('early-access.twitter_handle')) {
87 | $shareText = rawurlencode(
88 | trans('early-access::messages.twitter_share_text', [
89 | 'handle' => $handle,
90 | 'url' => route('early-access.index'),
91 | ])
92 | );
93 |
94 | $url = "https://twitter.com/intent/tweet?text={$shareText}&related={$handle}&handle={$handle}";
95 | }
96 |
97 | return redirect($url ?? route('early-access.index'));
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Http/Middleware/CheckForEarlyAccessMode.php:
--------------------------------------------------------------------------------
1 | baseUrl = config('early-access.url');
30 | }
31 |
32 | /**
33 | * Handle the incoming request.
34 | *
35 | * @param \Illuminate\Http\Request $request
36 | * @param \Closure $next
37 | * @return mixed
38 | */
39 | public function handle(Request $request, Closure $next)
40 | {
41 | if (EarlyAccess::isEnabled()) {
42 | $data = EarlyAccess::getBeaconDetails();
43 |
44 | if (isset($data['allowed']) && IpUtils::checkIp($request->ip(), (array) $data['allowed'])) {
45 | return $next($request);
46 | }
47 |
48 | if ($this->inExceptArray($request)) {
49 | return $next($request);
50 | }
51 |
52 | return redirect(config('early-access.url'));
53 | }
54 |
55 | return $next($request);
56 | }
57 |
58 | /**
59 | * Determine if the request has a URI that should be accessible in maintenance mode.
60 | *
61 | * @param \Illuminate\Http\Request $request
62 | * @return bool
63 | */
64 | protected function inExceptArray($request)
65 | {
66 | $defaultExceptions = [
67 | $this->baseUrl,
68 | $this->baseUrl . '/*',
69 | config('early-access.login_url'),
70 | ];
71 |
72 | $defaultExceptions = array_filter($defaultExceptions, function ($item) {
73 | return trim($item, '/') !== '*';
74 | });
75 |
76 | array_push($this->except, ...$defaultExceptions);
77 |
78 | foreach (array_unique($this->except) as $except) {
79 | if ($except !== '/') {
80 | $except = trim($except, '/');
81 | }
82 |
83 | if ($request->fullUrlIs($except) || $request->is($except)) {
84 | return true;
85 | }
86 | }
87 |
88 | return false;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Listeners/SendSubscriptionNotification.php:
--------------------------------------------------------------------------------
1 | subscriber->notify(
20 | new $notifierClass($event->subscriber)
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Listeners/SendUnsubscribeNotification.php:
--------------------------------------------------------------------------------
1 | subscriber->notify(
20 | new $notifierClass($event->subscriber)
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Notifications/UserSubscribed.php:
--------------------------------------------------------------------------------
1 | subscriber = $subscriber;
28 | }
29 |
30 | /**
31 | * Get the notification's delivery channels.
32 | *
33 | * @return array
34 | */
35 | public function via()
36 | {
37 | return ['mail'];
38 | }
39 |
40 | /**
41 | * Get the mail representation of the notification.
42 | *
43 | * @return \Illuminate\Notifications\Messages\MailMessage
44 | */
45 | public function toMail()
46 | {
47 | return (new MailMessage)
48 | ->subject(trans('early-access::mail.subscribed.subject', ['name' => config('app.name')]))
49 | ->line(trans('early-access::mail.subscribed.message.intro', ['name' => config('app.name')]))
50 | ->line(trans('early-access::mail.subscribed.message.share', ['url' => route('early-access.share')]))
51 | ->action(trans('early-access::mail.subscribed.message.unsubscribe'), route('early-access.unsubscribe'));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Notifications/UserUnsubscribed.php:
--------------------------------------------------------------------------------
1 | subscriber = $subscriber;
28 | }
29 |
30 | /**
31 | * Get the notification's delivery channels.
32 | *
33 | * @return array
34 | */
35 | public function via()
36 | {
37 | return ['mail'];
38 | }
39 |
40 | /**
41 | * Get the mail representation of the notification.
42 | *
43 | * @return \Illuminate\Notifications\Messages\MailMessage
44 | */
45 | public function toMail()
46 | {
47 | return (new MailMessage)
48 | ->subject(trans('early-access::mail.unsubscribed.subject', ['name' => config('app.name')]))
49 | ->line(trans('early-access::mail.unsubscribed.message.intro'));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Subscriber.php:
--------------------------------------------------------------------------------
1 | subscriber = $subscriber;
44 | }
45 |
46 | /**
47 | * Shortcut for instantiating the object.
48 | *
49 | * @param array $attributes
50 | * @return \Neo\EarlyAccess\Subscriber
51 | */
52 | public static function make(array $attributes = [])
53 | {
54 | /** @var self $instance */
55 | $instance = resolve(self::class);
56 | $instance->fillAttributes($attributes);
57 |
58 | return $instance;
59 | }
60 |
61 | /**
62 | * Subscribe the user.
63 | *
64 | * @return bool
65 | */
66 | public function subscribe(): bool
67 | {
68 | if ($this->subscribed()) {
69 | return true;
70 | }
71 |
72 | $subscribed = $this->subscriber->add($this->email, $this->name);
73 |
74 | $this->exists = $subscribed;
75 |
76 | $this->subscribed_at = (string) now();
77 |
78 | event(new UserSubscribed($this));
79 |
80 | return $subscribed;
81 | }
82 |
83 | /**
84 | * Unsubscribe the user.
85 | *
86 | * @return bool
87 | */
88 | public function unsubscribe(): bool
89 | {
90 | $unsubscribed = $this->subscriber->remove($this->email);
91 |
92 | if ($this->exists && $unsubscribed) {
93 | event(new UserUnsubscribed($this));
94 | }
95 |
96 | $this->exists = $unsubscribed === false;
97 |
98 | $this->subscribed_at = null;
99 |
100 | return $unsubscribed;
101 | }
102 |
103 | /**
104 | * Checks if user is subscribed.
105 | *
106 | * @return bool
107 | */
108 | public function subscribed(): bool
109 | {
110 | return $this->exists;
111 | }
112 |
113 | /**
114 | * Set the subscription status.
115 | *
116 | * @param bool $subscribed
117 | * @return $this
118 | */
119 | public function setSubscribed(bool $subscribed = true)
120 | {
121 | $this->exists = $subscribed;
122 |
123 | return $this;
124 | }
125 |
126 | /**
127 | * Find a user by email.
128 | *
129 | * @param string|null $email
130 | * @return \Neo\EarlyAccess\Subscriber|null
131 | */
132 | public function findByEmail(string $email = null): ?self
133 | {
134 | if (! $email and ! $this->email) {
135 | return null;
136 | }
137 |
138 | if ($this->subscribed() and ! $email) {
139 | return $this;
140 | }
141 |
142 | $subscriber = $this->subscriber->findByEmail($email ?? $this->email);
143 |
144 | return $subscriber ? $subscriber->setSubscribed() : null;
145 | }
146 |
147 | /**
148 | * Verify the subscriber.
149 | *
150 | * @return bool
151 | */
152 | public function verify()
153 | {
154 | $this->verified = $this->subscriber->verify($this->email);
155 |
156 | if (! $this->exists and $this->verified) {
157 | $this->exists = true;
158 | }
159 |
160 | return $this->verified;
161 | }
162 |
163 | /**
164 | * Get the instance as an array.
165 | *
166 | * @return array
167 | */
168 | public function toArray()
169 | {
170 | return $this->attributes;
171 | }
172 |
173 | /**
174 | * @return mixed
175 | */
176 | public function getKey()
177 | {
178 | return $this->email;
179 | }
180 |
181 | /**
182 | * @param $name
183 | * @return mixed
184 | */
185 | public function __get($name)
186 | {
187 | return $this->attributes[$name] ?? null;
188 | }
189 |
190 | /**
191 | * @param $name
192 | * @param $value
193 | */
194 | public function __set($name, $value)
195 | {
196 | if (array_key_exists($name, $this->attributes)) {
197 | $this->attributes[$name] = $value;
198 | }
199 | }
200 |
201 | public function fillAttributes(array $attributes): self
202 | {
203 | $this->attributes = $attributes;
204 | return $this;
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/SubscriptionServices/DatabaseService.php:
--------------------------------------------------------------------------------
1 | repository = $repository;
24 | }
25 |
26 | /**
27 | * Adds a new email to the subscribers list.
28 | *
29 | * @param string $email
30 | * @param string|null $name
31 | * @return bool
32 | */
33 | public function add(string $email, string $name = null): bool
34 | {
35 | return (bool) $this->repository->addSubscriber($email, $name);
36 | }
37 |
38 | /**
39 | * Removes an email from the subscribers list.
40 | *
41 | * @param string $email
42 | * @return bool
43 | */
44 | public function remove(string $email): bool
45 | {
46 | return $this->repository->removeSubscriber($email);
47 | }
48 |
49 | /**
50 | * Verifies a subscribers email address.
51 | *
52 | * @param string $email
53 | * @return bool
54 | */
55 | public function verify(string $email): bool
56 | {
57 | return $this->repository->verify($email);
58 | }
59 |
60 | /**
61 | * Find a subscriber using their email address.
62 | *
63 | * @param string $email
64 | * @return \Neo\EarlyAccess\Subscriber|false
65 | */
66 | public function findByEmail(string $email)
67 | {
68 | return with($this->repository->findByEmail($email), function ($user) {
69 | return $user ? Subscriber::make($user) : false;
70 | });
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/SubscriptionServices/Repositories/Database/EloquentRepository.php:
--------------------------------------------------------------------------------
1 | setTable(config('early-access.services.database.table_name'));
39 |
40 | parent::__construct($attributes);
41 | }
42 |
43 | /**
44 | * Adds a new subscriber to the repository.
45 | *
46 | * @param string $email
47 | * @param string|null $name
48 | * @return bool
49 | */
50 | public function addSubscriber(string $email, ?string $name): bool
51 | {
52 | return (bool) static::firstOrCreate(
53 | ['email' => $email],
54 | ['name' => $name, 'subscribed_at' => now()]
55 | );
56 | }
57 |
58 | /**
59 | * Removes a subscriber.
60 | *
61 | * @param string $email
62 | * @return bool
63 | */
64 | public function removeSubscriber(string $email): bool
65 | {
66 | return with(static::byEmail($email)->first(), function (?self $subscriber) {
67 | return $subscriber ? $subscriber->delete() : false;
68 | });
69 | }
70 |
71 | /**
72 | * Verify the subscriber.
73 | *
74 | * @param string $email
75 | * @return bool
76 | */
77 | public function verify(string $email): bool
78 | {
79 | return with(static::byEmail($email)->first(), function (?self $subscriber) {
80 | return $subscriber ? $subscriber->update(['verified_at' => now()]) : false;
81 | });
82 | }
83 |
84 | /**
85 | * Find subscriber by email.
86 | *
87 | * @param string $email
88 | * @return array|false
89 | */
90 | public function findByEmail(string $email)
91 | {
92 | return with(static::byEmail($email)->first(), function (?self $subscriber) {
93 | return $subscriber ? $subscriber->toArray() : false;
94 | });
95 | }
96 |
97 | /**
98 | * Query by email.
99 | *
100 | * @param \Illuminate\Database\Eloquent\Builder $query
101 | * @param string $email
102 | * @return \Illuminate\Database\Eloquent\Builder
103 | */
104 | public function scopeByEmail($query, string $email)
105 | {
106 | return $query->where('email', $email);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Traits/InteractsWithEarlyAccess.php:
--------------------------------------------------------------------------------
1 | artisan('early-access', ['--activate' => true]);
17 |
18 | $this->assertTrue($this->isEarlyAccessEnabled());
19 | }
20 |
21 | /** @test */
22 | public function deactivates_when_artisan_command_is_called()
23 | {
24 | $this->artisan('early-access', ['--deactivate' => true]);
25 |
26 | $this->assertFalse($this->isEarlyAccessEnabled());
27 | }
28 |
29 | /** @test */
30 | public function adds_current_ip_to_allowed_list()
31 | {
32 | $this->artisan('early-access', [
33 | '--activate' => true,
34 | '--allow' => ['127.0.0.1', '1.1.1.1'],
35 | ]);
36 |
37 | $this->assertArraySubset(['127.0.0.1', '1.1.1.1'], $this->getAllowedNetworks());
38 | }
39 |
40 | /** @test */
41 | public function can_check_early_access_status()
42 | {
43 | $this->artisan('early-access', ['--deactivate' => true]);
44 |
45 | $this->artisan('early-access', ['status' => true])
46 | ->expectsOutput('Not active');
47 |
48 | $this->artisan('early-access', ['--activate' => true]);
49 |
50 | $this->artisan('early-access', ['status' => true])
51 | ->expectsOutput('Active. Allowed networks: none');
52 |
53 | $this->artisan('early-access', ['--deactivate' => true]);
54 | $this->artisan('early-access', ['--allow' => ['127.0.0.1', '1.1.1.1']]);
55 |
56 | $this->artisan('early-access', ['status' => true])
57 | ->expectsOutput('Active. Allowed networks: 127.0.0.1, 1.1.1.1');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | resetTestingStage();
33 | }
34 |
35 | /**
36 | * Clean up the testing environment before the next test.
37 | *
38 | * @return void
39 | */
40 | protected function tearDown(): void
41 | {
42 | $this->resetTestingStage();
43 |
44 | parent::tearDown();
45 | }
46 |
47 | /**
48 | * @param $abstract
49 | * @param \Closure|null $mock
50 | * @return \Mockery\MockInterface
51 | */
52 | protected function mock($abstract, ?Closure $mock = null)
53 | {
54 | $this->mock = Mockery::mock($abstract);
55 |
56 | $this->app->instance($abstract, $this->mock);
57 |
58 | return $this->mock;
59 | }
60 |
61 | /**
62 | * @param string|null $name
63 | * @param string $email
64 | * @param bool $verified
65 | * @return \Neo\EarlyAccess\Subscriber
66 | */
67 | protected function createSubscriberInstance(string $name = null, string $email = null, bool $verified = true)
68 | {
69 | return Subscriber::make([
70 | 'name' => $name ?? 'John Doe',
71 | 'email' => $email ?? 'john@doe.com',
72 | 'subscribed_at' => (string) now(),
73 | 'verified' => $verified,
74 | ]);
75 | }
76 |
77 | /**
78 | * Activates early access.
79 | */
80 | protected function activateEarlyAccessConfiguration()
81 | {
82 | app('config')->set('early-access.enabled', true);
83 | }
84 |
85 | /**
86 | * Deactivates early access.
87 | */
88 | protected function deactivateEarlyAccessConfiguration()
89 | {
90 | app('config')->set('early-access.enabled', false);
91 | }
92 |
93 | /**
94 | * Resets the stage so we can carry out tests.
95 | */
96 | protected function resetTestingStage()
97 | {
98 | $this->deactivateEarlyAccessConfiguration();
99 |
100 | Storage::disk('local')->delete('early-access');
101 | }
102 |
103 | /**
104 | * @param $uri
105 | * @param array $headers
106 | * @return \Illuminate\Foundation\Testing\TestResponse
107 | */
108 | public function get($uri, array $headers = [])
109 | {
110 | return parent::get($uri, $headers);
111 | }
112 |
113 | /**
114 | * Get package providers.
115 | *
116 | * @param \Illuminate\Foundation\Application $app
117 | *
118 | * @return array
119 | */
120 | protected function getPackageProviders($app)
121 | {
122 | return [
123 | EarlyAccessServiceProvider::class,
124 | ];
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/tests/Unit/EarlyAccessTest.php:
--------------------------------------------------------------------------------
1 | activateEarlyAccessConfiguration();
17 |
18 | $this->assertTrue($this->isEarlyAccessEnabled());
19 |
20 | $this->deactivateEarlyAccessConfiguration();
21 |
22 | $this->assertFalse($this->isEarlyAccessEnabled());
23 | }
24 |
25 | /** @test */
26 | public function can_save_and_delete_beacon_file()
27 | {
28 | $this->saveBeacon();
29 |
30 | $this->assertTrue(Storage::disk('local')->exists('early-access'));
31 |
32 | $this->deleteBeacon();
33 |
34 | $this->assertFalse(Storage::disk('local')->exists('early-access'));
35 | }
36 |
37 | /** @test */
38 | public function can_save_a_list_of_networks_to_beacon_file()
39 | {
40 | $this->saveBeacon(['127.0.0.1']);
41 |
42 | $this->addAllowedNetworksToBeacon(['0.0.0.0', '1.1.1.1']);
43 |
44 | $this->assertArraySubset(['127.0.0.1', '0.0.0.0', '1.1.1.1'], $this->getAllowedNetworks());
45 | }
46 |
47 | /** @test */
48 | public function can_get_beacon_details()
49 | {
50 | $this->assertFalse($this->getBeaconDetails());
51 |
52 | $this->saveBeacon(['127.0.0.1']);
53 |
54 | $details = $this->getBeaconDetails();
55 |
56 | $this->assertArrayHasKey('time', $details);
57 |
58 | $this->assertArraySubset(['allowed' => ['127.0.0.1']], $details);
59 | }
60 |
61 | /** @test */
62 | public function adding_multiple_allowed_appends_to_allowed_list()
63 | {
64 | $this->saveBeacon(['127.0.0.1']);
65 |
66 | $this->saveBeacon(['1.1.1.1']);
67 |
68 | $this->assertArraySubset(['127.0.0.1', '1.1.1.1'], $this->getAllowedNetworks());
69 | }
70 |
71 | /** @test */
72 | public function removes_duplicate_networks_from_list()
73 | {
74 | $this->saveBeacon(['127.0.0.1', '0.0.0.0', '0.0.0.0']);
75 |
76 | $this->saveBeacon(['1.1.1.1']);
77 |
78 | $this->saveBeacon(['1.1.1.1']);
79 |
80 | $this->assertArraySubset(['127.0.0.1', '0.0.0.0', '1.1.1.1'], $this->getAllowedNetworks());
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/tests/Unit/SubscribeTest.php:
--------------------------------------------------------------------------------
1 | mock(SubscriptionProvider::class);
22 | }
23 |
24 | /**
25 | * Clean up the testing environment before the next test.
26 | *
27 | * @return void
28 | */
29 | public function tearDown(): void
30 | {
31 | Mockery::close();
32 | }
33 |
34 | /** @test */
35 | public function can_subscribe_and_unsubscribe_to_provider_using_email()
36 | {
37 | $subscriber = Subscriber::make(['email' => 'john@doe.com']);
38 |
39 | $this->mock->shouldReceive(['add' => true, 'remove' => true])->once()->andReturnTrue();
40 |
41 | $this->assertTrue($subscriber->subscribe());
42 |
43 | $this->assertTrue($subscriber->unsubscribe());
44 | }
45 |
46 | /** @test */
47 | public function can_verify_email_address()
48 | {
49 | $this->mock->shouldReceive('verify')->once()->andReturnTrue();
50 |
51 | $this->assertTrue(Subscriber::make(['email' => 'neo@ck.co'])->verify());
52 | }
53 |
54 | /** @test */
55 | public function can_find_subscriber_by_email_address()
56 | {
57 | $subscriber = $this->createSubscriberInstance('John', 'john@doe.com');
58 |
59 | $this->mock->shouldReceive('findByEmail')->with('john@doe.com')->once()->andReturn($subscriber);
60 |
61 | $this->assertEquals($subscriber, $subscriber->findByEmail());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/Unit/SubscriberTest.php:
--------------------------------------------------------------------------------
1 | mock(SubscriptionProvider::class);
30 |
31 | $this->subscriber = $this->createSubscriberInstance();
32 | }
33 |
34 | /** @test */
35 | public function can_get_subscriber_details()
36 | {
37 | $this->assertEquals('John Doe', $this->subscriber->name);
38 |
39 | $this->assertEquals('john@doe.com', $this->subscriber->email);
40 |
41 | $this->assertIsString($this->subscriber->subscribed_at);
42 |
43 | $this->assertTrue($this->subscriber->verified);
44 | }
45 |
46 | /** @test */
47 | public function can_subscribe_and_unsubscribe_from_subscriber_instance()
48 | {
49 | $this->mock->shouldReceive(['add' => true, 'remove' => true]);
50 |
51 | $this->assertTrue($this->subscriber->subscribe());
52 |
53 | $this->assertTrue($this->subscriber->subscribed());
54 |
55 | $this->assertTrue($this->subscriber->unsubscribe());
56 |
57 | $this->assertFalse($this->subscriber->subscribed());
58 | }
59 |
60 | /** @test */
61 | public function event_is_fired_on_subscribe()
62 | {
63 | $this->mock->shouldReceive(['add' => true]);
64 |
65 | $this->subscriber->subscribe();
66 |
67 | Event::assertDispatched(UserSubscribed::class, function ($e) {
68 | return $e->subscriber->email === $this->subscriber->email;
69 | });
70 | }
71 |
72 | /** @test */
73 | public function event_is_fired_on_unsubscribe()
74 | {
75 | $this->mock->shouldReceive(['remove' => true]);
76 |
77 | $this->subscriber->setSubscribed()->unsubscribe();
78 |
79 | Event::assertDispatched(UserUnsubscribed::class, function ($e) {
80 | return $e->subscriber->email === $this->subscriber->email;
81 | });
82 | }
83 |
84 | /** @test */
85 | public function can_set_subscribed_status()
86 | {
87 | $this->mock->shouldReceive(['add' => true, 'remove' => true]);
88 |
89 | $this->subscriber->subscribe();
90 |
91 | $this->assertTrue($this->subscriber->subscribed());
92 |
93 | $this->subscriber->setSubscribed(false);
94 |
95 | $this->assertFalse($this->subscriber->subscribed());
96 | }
97 |
98 | /** @test */
99 | public function can_find_by_email()
100 | {
101 | //
102 | // Find user by supplying email using empty Subscriber object
103 | //
104 |
105 | $this->mock->shouldReceive('findByEmail')
106 | ->with($this->subscriber->email)
107 | ->once()
108 | ->andReturn($this->subscriber);
109 |
110 | $this->assertEquals($this->subscriber, Subscriber::make()->findByEmail($this->subscriber->email));
111 |
112 | //
113 | // Find user by not supplying email using empty Subscriber object
114 | //
115 |
116 | $this->mock->shouldReceive('findByEmail')->times(0);
117 |
118 | $this->assertNull(Subscriber::make()->findByEmail());
119 |
120 | //
121 | // Find user by supplying email using loaded Subscriber object
122 | //
123 |
124 | $this->mock->shouldReceive('findByEmail')
125 | ->with($this->subscriber->email)
126 | ->times(2)
127 | ->andReturn($this->subscriber);
128 |
129 | $subscriber = $this->createSubscriberInstance('Neo Ighodaro', 'neo@ck.co', true);
130 |
131 | $subscriber->setSubscribed(false);
132 |
133 | $this->assertEquals($this->subscriber, $subscriber->findByEmail($this->subscriber->email));
134 |
135 | $subscriber->setSubscribed(true);
136 |
137 | $this->assertEquals($this->subscriber, $subscriber->findByEmail($this->subscriber->email));
138 | }
139 |
140 | /** @test */
141 | public function can_use_local_instance_to_find_email_if_existing()
142 | {
143 | $this->subscriber->setSubscribed();
144 |
145 | $this->mock->shouldReceive('findByEmail')->times(0);
146 |
147 | $this->assertEquals($this->subscriber, $this->subscriber->findByEmail());
148 |
149 | $this->subscriber->setSubscribed(false);
150 |
151 | $this->mock->shouldReceive('findByEmail')->once()->andReturn($this->subscriber);
152 |
153 | $this->assertEquals($this->subscriber, $this->subscriber->findByEmail());
154 | }
155 |
156 | /** @test */
157 | public function can_verify_subscriber()
158 | {
159 | $this->assertFalse($this->subscriber->subscribed());
160 |
161 | $this->mock->shouldReceive('verify')->once()->with($this->subscriber->email)->andReturn(true);
162 |
163 | $this->subscriber->verify();
164 |
165 | $this->assertTrue($this->subscriber->verified);
166 |
167 | $this->assertTrue($this->subscriber->subscribed());
168 | }
169 |
170 | /** @test */
171 | public function notifications_can_be_sent_from_subscriber_instance()
172 | {
173 | $this->subscriber->notify(new UserSubscribedNotification($this->subscriber));
174 |
175 | Notification::assertSentTo(
176 | $this->subscriber,
177 | UserSubscribedNotification::class,
178 | function ($notification) {
179 | return $notification->subscriber->email === $this->subscriber->email;
180 | }
181 | );
182 |
183 | $this->subscriber->notify(new UserUnsubscribedNotification($this->subscriber));
184 |
185 | Notification::assertSentTo(
186 | $this->subscriber,
187 | UserUnsubscribedNotification::class,
188 | function ($notification) {
189 | return $notification->subscriber->email === $this->subscriber->email;
190 | }
191 | );
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | let mix = require('laravel-mix');
2 | require('laravel-mix-tailwind');
3 |
4 | mix.sass('resources/sass/early-access.scss', 'public/css')
5 | .copyDirectory('resources/svg', 'public/svg')
6 | .tailwind();
7 |
8 | if (mix.production) {
9 | mix.version();
10 | }
11 |
--------------------------------------------------------------------------------