Closed tobiasdalhof closed 6 years ago
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use OhMyBrew\ShopifyApp\Facades\ShopifyApp;
use OhMyBrew\ShopifyApp\Models\Charge;
class Billable
{
/**
* Checks if a shop has paid for access.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$shopifyPlanName = $request->session()->get('shopify.shop.plan_name');
if (empty($shopifyPlanName)) {
$api = ShopifyApp::shop()->api();
$shopifyPlanName = $api->rest('GET', '/admin/shop.json')->body->shop->plan_name;
$request->session()->put('shopify.shop.plan_name', $shopifyPlanName);
}
if (
$shopifyPlanName === 'affiliate' ||
$shopifyPlanName === 'staff_business'
) {
// Free access for partners and Shopify employees
return $next($request);
}
if (config('shopify-app.billing_enabled') === true) {
// Grab the shop and last recurring or one-time charge
$shop = ShopifyApp::shop();
$lastCharge = $shop->charges()
->whereIn('type', [Charge::CHARGE_RECURRING, Charge::CHARGE_ONETIME])
->orderBy('created_at', 'desc')
->first();
if (
!$shop->isGrandfathered() &&
(is_null($lastCharge) || $lastCharge->isDeclined() || $lastCharge->isCancelled())
) {
// They're not grandfathered in, and there is no charge or charge was declined... redirect to billing
return redirect()->route('billing');
}
}
// Move on, everything's fine
return $next($request);
}
}
@tobiasdalhof Wonder if theres a better way... doing an API call on the middleware seems harsh. Hmm... what if we did the check in the method which creates the shop... check the plan there and if they're staff or affiliate, set grandfathered to true on create. I can possibily look into this as a feature addition, what do you think?
Good morning @ohmybrew
I agree with you, API calls on the middleware are harsh. I reopened the issue.
In my opinion it would make perfect sense to add this feature to this package since Shopify recommends app developers to give partners and Shopify employees free app access.
ShopifyApp@shop() is the method, right? https://github.com/ohmybrew/laravel-shopify/blob/e55c3e80a17e958f296c823644d2573ee742c3da/src/ShopifyApp/ShopifyApp.php#L41
Should we introduce a new config key for free shopify plan names?
Something like this?
/*
|--------------------------------------------------------------------------
| Billing Free Access
|--------------------------------------------------------------------------
|
| This option is for enabling free access for
| specific Shopify plan names.
|
| The Billable middleware will grandfather all
| shops with matching plan name.
|
| Partners === 'affiliate'
| Shopify employees === 'staff_business'
|
*/
'billing_free_access' => [
/*
env('SHOPIFY_BILLING_FREE_ACCESS_PARTNERS', 'affiliate'),
env('SHOPIFY_BILLING_FREE_ACCESS_EMPLOYEES', 'staff_business'),
*/
],
Here is a list of possbile plan names which i found @ https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/enumeration-of-plan_name-from-get-admin-shop-json-352847
"affiliate",
"staff",
"professional",
"custom",
"shopify_plus",
"unlimited",
"basic",
"cancelled",
"staff_business",
"trial",
"dormant",
"frozen",
"singtel_unlimited",
"npo_lite",
"singtel_professional",
"singtel_trial",
"npo_full",
"business",
"singtel_basic",
"uafrica_professional",
"sales_training",
"singtel_starter",
"uafrica_basic",
"fraudulent",
"enterprise",
"starter",
"comped",
"shopify_alumni"
Im not quite sure what the difference between staff
and staff_business
is? Maybe staff
is showing up in the app review process and staff_business
are regular stores by Shopify employees?
Edit: staff_business
should be good according to https://help.shopify.com/en/api/app-store/being-successful-in-the-app-store/offering-employee-discounts
Could this be simplified by providing a method where the developer simply returns true or false as to whether the shop should be billed? There are cases where a developer may want to give free access based on the Shopify domain or other criteria. By implementing a method that returns a boolean, the developer can implement their own logic.
Just a thought.
@brianakidd You can already do that by setting grandfathered
to true
and use the isGrandfathered
method on your shop model. Am I missing something here?
https://github.com/ohmybrew/laravel-shopify/blob/master/src/ShopifyApp/Models/Shop.php#L62
I still struggle to find a proper place for this kind of actions.
@tobiasdalhof Yes, you can, but during installation, the shop record doesn't exist yet so currently I'm starting the installation process, once the shop is created in the database, then I set the flag, then I continue the installation process.
My point is rather than explicitly setting it based on the shop plan or some other specific criteria, let the developer make the decision. This is what happens when implementing a Policy, for example - we're not using configuration to determine if a user can perform a certain action, we just implement a method that returns true or false.
Seems like letting the developer decide in this manner will keep the project from getting bloated.
Ah, that makes sense...
Hmm.. How about adding events to the installation process? Something like BeforeInstallation
and AfterInstallation
?
Edit: We could listen to Eloquent Events https://laravel.com/docs/5.6/eloquent#events
Example with Observer:
Listen for creating
and created
<?php
namespace App\Observers;
use OhMyBrew\ShopifyApp\Facades\ShopifyApp;
use OhMyBrew\ShopifyApp\Models\Shop;
class ShopObserver
{
/**
* Before installation.
*
* @param \OhMyBrew\ShopifyApp\Models\Shop $shop
* @return void
*/
public function creating(Shop $shop)
{
$api = ShopifyApp::shop()->api();
$shopifyPlanName = $api->rest('GET', '/admin/shop.json')->body->shop->plan_name;
if (
$shopifyPlanName === 'affiliate' ||
$shopifyPlanName === 'staff_business'
) {
// Free access for partners and Shopify employees
$shop->grandfathered = true;
}
}
/**
* After installation.
*
* @param \OhMyBrew\ShopifyApp\Models\Shop $shop
* @return void
*/
public function created(Shop $shop)
{
//
}
}
Boot ShopObserver
in your AppServiceProvider
<?php
namespace App\Providers;
use App\Observers\ShopObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
\OhMyBrew\ShopifyApp\Models\Shop::observe(ShopObserver::class);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
//
}
}
Just an idea - not tested. This wouldn't require any changes to the package. What do you think? @brianakidd @ohmybrew
I like the idea of the events, yes it would keep things not so bloated then and give full freedom to the developer. Any recommendations on a simple event system for Laravel? Would rather not roll my own if one exists..
On Wed., Aug. 8, 2018, 7:31 a.m. Tobias Dalhof, notifications@github.com wrote:
Ah, that makes sense...
Hmm.. How about adding events to the installation process? Something like BeforeInstallation and AfterInstallation?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ohmybrew/laravel-shopify/issues/63#issuecomment-411354053, or mute the thread https://github.com/notifications/unsubscribe-auth/ACTuOunaeDyQ-x_Wu4Od_5h1gECyJnzsks5uOrb8gaJpZM4VxsT3 .
ping @ohmybrew i have updated my last post; the quoted message in your mail is outdated
@tobiasdalhof Ahh yes, damn email. Observers... completely forgot about them! Works well in my mind for the use-case.
I could go the wiki route with this and provide a page on how to do this, or I can also provide an observer by default which is enabled through config... the latter would certainly be easier for someone setting up the package out of the box... what do you think?
Honestly i'd just provide some nice documentation about this topic and keep the package lean
@tobiasdalhof Yeah, true, thats been my goal. I'll write up something nice based on your above, thanks kindly 🥇, will post it here once complete and close the issue (unless someone else has anything to add that is :))
@tobiasdalhof Yes, I like listening for the event.
@ohmybrew Thanks for considering this.
I ran into trouble when observing the creating
and created
events.
$shop->api()
will fail because the shopify_token
column is empty at this point. In my case I have to observe the saved
event and execute my code after shopify_token
is added by AuthController
.
I also added a finished_setup
flag to my shops
table:
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddFinishedSetupToShopsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('shops', function (Blueprint $table) {
$table->boolean('finished_setup')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('shops', function (Blueprint $table) {
$table->dropColumn('finished_setup');
});
}
}
Now I am able to observe the saved
event on Shop
in order to initialize my app.
<?php
namespace App\Observers;
use App\Shop;
use OhMyBrew\ShopifyApp\Facades\ShopifyApp;
class ShopObserver
{
/**
* @var object
*/
public $shopApi;
/**
* @param Shop $shop
* @return void
*/
public function saved(Shop $shop)
{
if (
!empty($shop->shopify_token) &&
$shop->finished_setup === false
) {
$shop->grandfathered = $this->allowFreeAccess($shop);
$this->initUser($shop);
$shop->finished_setup = true;
$shop->save();
}
}
/**
* @return object
*/
public function shopApi(Shop $shop)
{
if (!empty($this->shopApi)) {
return $this->shopApi;
}
$api = $shop->api();
$this->shopApi = $api->rest('GET', '/admin/shop.json')->body->shop;
return $this->shopApi;
}
/**
* @param Shop $shop
* @return void
*/
public function initUser(Shop $shop)
{
$shopApi = $this->shopApi($shop);
$user = $shop->user()->create([
'name' => $shopApi->shop_owner,
'email' => $shopApi->email,
'timezone' => $shopApi->iana_timezone,
'template_default' => 'test',
]);
}
/**
* @return boolean
*/
public function allowFreeAccess(Shop $shop)
{
$shopifyPlanName = $this->shopApi($shop)->plan_name;
if (
$shopifyPlanName === 'affiliate' ||
$shopifyPlanName === 'staff_business'
) {
// Free access for partners and Shopify employees
return true;
}
return false;
}
}
Ah right, we need a token to do API calls...
There is an afterAuthenticateJob feature built into the code which fires after every auth. Line 147 of AuthControllerTrait handles this. It passed the Shop object into the job, if you tell the config to run your job inline, it will run right away. Which will happen before billing screen. I think maybe this is the best spot to do this free-business in since we'd have the token at that point.
/*
|--------------------------------------------------------------------------
| After Authenticate Job
|--------------------------------------------------------------------------
|
| This option is for firing a job after a shop has been authenticated.
| This, like webhooks and scripttag jobs, will fire every time a shop
| authenticates, not just once.
|
*/
'after_authenticate_job' => [
/*
'job' => env('AFTER_AUTHENTICATE_JOB'), // example: \App\Jobs\AfterAuthenticateJob::class
'inline' => env('AFTER_AUTHENTICATE_JOB_INLINE', false) // False = execute inline, true = dispatch job for later
*/
],
If you do something like:
'after_authenticate_job' => [
'job' => \App\Jobs\AfterAuthenticateJob::class
'inline' => true
],
Then your job with:
<?php
namespace App\Jobs;
use OhMyBrew\ShopifyApp\Models\Shop;
class AfterAuthenticateJob
{
/**
* Shop's instance.
*
* @var string
*/
protected $shop;
/**
* Create a new job instance.
*
* @param object $shop The shop's object
*
* @return void
*/
public function __construct(Shop $shop)
{
$this->shop = $shop;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if (!$this->shop->isGrandfathered()) {
$planName = $this->shop->api()->rest('GET', '/admin/shop.json')->body->shop->plan_name;
if ($planName === 'affiliate' || $planName === 'staff_business') {
$shop->grandfathered = true;
$shop->save();
}
}
/*
* OR with `finished_setup` so we don't redo this logic every auth...
*
* if (!$this->shop->finished_setup && !$this->shop->isGrandfathered()) {
* $planName = $this->shop->api()->rest('GET', '/admin/shop.json')->body->shop->plan_name;
* if ($planName === 'affiliate' || $planName === 'staff_business') {
* $shop->grandfathered = true;
* $shop->finished_setup = true;
* $shop->save();
* }
* }
*/
}
}
Maybe keep your finished_setup column so you can not run the code in handle()
if setup is done.. just an idea off the top of my head...
Oh this is great! Thanks for the hint. I will move my code to AfterAuthenticateJob
then.
Great - yeah, oversight - completely forgot we need a token, I'll update my wiki draft for this and get it published soon.
Closing per: https://github.com/ohmybrew/laravel-shopify/wiki/Free-Access-Based-on-Shopify-Plan
Thank you everyone.
@osiset @tobidalhof I want to give free access to the app for development stores, but when they transfer ownership of the store to the client and client choose a paid plan, I want to force them to buy a plan to use the app. Can you tell me what can I keep checking in that case and redirect them to billing?
@osiset @tobidalhof I want to give free access to the app for development stores, but when they transfer ownership of the store to the client and client choose a paid plan, I want to force them to buy a plan to use the app. Can you tell me what can I keep checking in that case and redirect them to billing?
Have you solved it Me also getting same issue ?
@osiset @tobidalhof I want to give free access to the app for development stores, but when they transfer ownership of the store to the client and client choose a paid plan, I want to force them to buy a plan to use the app. Can you tell me what can I keep checking in that case and redirect them to billing?
Have you solved it Me also getting same issue ?
Not using this package anymore
@osiset @tobidalhof I want to give free access to the app for development stores, but when they transfer ownership of the store to the client and client choose a paid plan, I want to force them to buy a plan to use the app. Can you tell me what can I keep checking in that case and redirect them to billing?
Have you solved it Me also getting same issue ?
Not using this package anymore
Then what do you use ? Can you suggest me something better then this package. And you may have solved this issue can you please tell me how you did that ?
@osiset @tobidalhof I want to give free access to the app for development stores, but when they transfer ownership of the store to the client and client choose a paid plan, I want to force them to buy a plan to use the app. Can you tell me what can I keep checking in that case and redirect them to billing?
Hey Kushal , Can you please tell me how did you solved.
What is the best way to enable free access for partners and Shopify employees?
My first idea:
Partners
Shopify employees
So i would need a way to skip or extend the billable middleware and call the shop.json endpoint (and store the plan_name in session?) before the billable middleware.
Should i add my own billable middleware to
App/Http/Kernel.php
?Any ideas?