├── .github ├── FUNDING.yml └── workflows │ └── update-stripe-submodule.yml ├── .gitattributes ├── .gitmodules ├── snippets.php ├── composer.json ├── index.php ├── options.php ├── LICENSE ├── snippets └── stripe-checkout-button.php ├── usersMethods.php ├── siteMethods.php ├── hooks.php ├── userMethods.php ├── routes.php └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kreativ-anders] 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/stripe"] 2 | path = lib/stripe 3 | url = https://github.com/stripe/stripe-php.git 4 | -------------------------------------------------------------------------------- /snippets.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/snippets/stripejs.php', 13 | 'stripe-checkout-button' => __DIR__ . '/snippets/stripe-checkout-button.php' 14 | 15 | ]; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kreativ-anders/kirby-memberkit", 3 | "description": "Kirby Memberkit - A versatile Kirby User Membership Plug-In powered by Stripe Subscriptions for Kirby CMS.", 4 | "homepage:": "https://github.com/kreativ-anders/kirby-memberkit", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "kreativ-anders", 9 | "email": "superwoman@example.com" 10 | } 11 | ], 12 | "support": { 13 | "docs": "https://github.com/kreativ-anders/kirby-memberkit/blob/main/README.md", 14 | "source": "https://github.com/kreativ-anders/kirby-memberkit" 15 | } 16 | } -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | include_once __DIR__ . '/options.php', 9 | 'snippets' => include_once __DIR__ . '/snippets.php', 10 | 'hooks' => include_once __DIR__ . '/hooks.php', 11 | 'routes' => include_once __DIR__ . '/routes.php', 12 | 'userMethods' => include_once __DIR__ . '/userMethods.php', 13 | 'usersMethods' => include_once __DIR__ . '/usersMethods.php', 14 | 'siteMethods' => include_once __DIR__ . '/siteMethods.php', 15 | ]); 16 | ?> -------------------------------------------------------------------------------- /options.php: -------------------------------------------------------------------------------- 1 | 'sk_test_xxx', 13 | 'publicKey' => 'pk_test_xxx', 14 | 'webhookSecret' => 'whsec_xxx', 15 | 'stripeURLSlug' => 'checkout', 16 | 'successURL' => '../success', 17 | 'cancelURL' => '../cancel', 18 | 'tiers' => [ 19 | // INDEX 0 20 | [ 'name' => 'Free' 21 | ,'price' => null], 22 | // INDEX 1 23 | [ 'name' => 'Basic' 24 | ,'price' => 'price_xxxx'], 25 | // INDEX 2 26 | [ 'name' => 'Premium' 27 | ,'price' => 'price_xxxx'], 28 | // INDEX X 29 | ] 30 | 31 | ]; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 kreativ-anders | Manuel Steinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /snippets/stripe-checkout-button.php: -------------------------------------------------------------------------------- 1 | user()->stripe_subscription()->isEmpty()): ?> 4 | 5 | 6 | 7 | 8 | 9 | 39 | 40 | -------------------------------------------------------------------------------- /usersMethods.php: -------------------------------------------------------------------------------- 1 | function () { 13 | 14 | $users = kirby()->users(); 15 | $counter = 0; 16 | 17 | // THIS IS A TASK FOR THE ADMIN 18 | if (kirby()->user()->isAdmin()) { 19 | 20 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 21 | 22 | // NO TRY CATCH BLOCK - LET EXCEPTION ARISE 23 | foreach($users as $user) { 24 | 25 | if ($user->stripe_customer()->isEmpty()) { 26 | 27 | // CREATE STRIPE CUSTOMER 28 | $customer = $stripe->customers->create([ 29 | 'email' => $user->email() 30 | ]); 31 | 32 | // UPDATE KIRBY USER - ROOT TIER (INDEX=0) 33 | $kirby = kirby(); 34 | $kirby->impersonate('kirby'); 35 | 36 | $kirby->user($user->email())->update([ 37 | 'stripe_customer' => $customer->id, 38 | 'tier' => option('kreativ-anders.memberkit.tiers')[0]['name'] 39 | ]); 40 | 41 | $kirby->impersonate(); 42 | 43 | $counter++; 44 | } 45 | } 46 | 47 | } else { 48 | 49 | throw new Exception('This is an admin task!'); 50 | } 51 | 52 | return [ 53 | 'users' => count($users), 54 | 'migrations' => $counter 55 | ]; 56 | } 57 | ]; -------------------------------------------------------------------------------- /.github/workflows/update-stripe-submodule.yml: -------------------------------------------------------------------------------- 1 | name: Update Stripe Submodule 2 | 3 | on: 4 | schedule: 5 | - cron: '0 6 1 * *' # Monthly on 1st at 06:00 UTC 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-stripe: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | with: 16 | submodules: recursive 17 | fetch-depth: 0 18 | 19 | - name: Configure Git (main + submodule) 20 | run: | 21 | git config user.name "github-actions" 22 | git config user.email "github-actions@github.com" 23 | cd lib/stripe 24 | git config user.name "github-actions" 25 | git config user.email "github-actions@github.com" 26 | cd - 27 | 28 | - name: Get previous stripe tag 29 | id: previous 30 | run: | 31 | cd lib/stripe 32 | git fetch --tags 33 | prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") 34 | echo "prev_tag=$prev_tag" >> $GITHUB_OUTPUT 35 | 36 | - name: Update stripe submodule 37 | run: | 38 | git submodule update --remote --checkout lib/stripe 39 | 40 | - name: Get latest stripe tag after update 41 | id: latest 42 | run: | 43 | cd lib/stripe 44 | git fetch --tags 45 | latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") 46 | echo "latest_tag=$latest_tag" >> $GITHUB_OUTPUT 47 | 48 | - name: Skip if no tag or no change 49 | if: steps.latest.outputs.latest_tag == '' || steps.latest.outputs.latest_tag == steps.previous.outputs.prev_tag 50 | run: | 51 | echo "No new tag or tag unchanged — skipping PR creation." 52 | exit 0 53 | 54 | - name: Create Pull Request 55 | uses: peter-evans/create-pull-request@v6 56 | with: 57 | commit-message: "chore: update stripe to ${{ steps.latest.outputs.latest_tag }}" 58 | title: "Update stripe submodule to ${{ steps.latest.outputs.latest_tag }}" 59 | body: | 60 | This PR updates the `lib/stripe` submodule from **${{ steps.previous.outputs.prev_tag }}** to **${{ steps.latest.outputs.latest_tag }}**. 61 | 62 | 🔍 [View changes on GitHub](https://github.com/stripe/stripe-php/compare/${{ steps.previous.outputs.prev_tag }}...${{ steps.latest.outputs.latest_tag }}) 63 | 64 | branch: "update-stripe-submodule" 65 | delete-branch: true 66 | -------------------------------------------------------------------------------- /siteMethods.php: -------------------------------------------------------------------------------- 1 | function ($subscription) { 13 | 14 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 15 | 16 | try { 17 | 18 | // RETRIEVE STRIPE CUSTOMER 19 | $customer = $stripe->customers->retrieve( 20 | $subscription->customer, 21 | [] 22 | ); 23 | 24 | } catch (Exception $e) { 25 | 26 | // LOG ERROR SOMEWHERE 27 | throw new Exception('Could not retrieve stripe customer!'); 28 | } 29 | 30 | // DETERMINE TIER NAME BY STRIPE PRICE ID 31 | $price = $subscription->items['data'][0]->price->id; 32 | $priceIndex = array_search($price, array_column(option('kreativ-anders.memberkit.tiers'), 'price'), false); 33 | $tier = option('kreativ-anders.memberkit.tiers')[$priceIndex]['name']; 34 | 35 | $kirby = kirby(); 36 | $kirby->impersonate('kirby'); 37 | 38 | try { 39 | 40 | // UPDATE KIRBY USER SUBSCRIPTION INFORMATION 41 | kirby()->user($customer->email)->update([ 42 | 'stripe_subscription' => $subscription->id, 43 | 'stripe_status' => $subscription->status, 44 | 'tier' => $tier 45 | ]); 46 | 47 | } catch (Exception $e) { 48 | 49 | // LOG ERROR SOMEWHERE 50 | throw new Exception('Could not update kirby user!'); 51 | } 52 | 53 | $kirby->impersonate(); 54 | }, 55 | // CANCEL STRIPE SUBSCRIPTION VIA WEBHOOK ----------------------------------------------------------------------------------------- 56 | 'cancelStripeSubscriptionWebhook' => function ($subscription) { 57 | 58 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 59 | 60 | // GET STRIPE CUSTOMER 61 | $customer = $stripe->customers->retrieve( 62 | $subscription->customer, 63 | [] 64 | ); 65 | 66 | // DETERMINE TIER NAME BY STRIPE PRICE ID 67 | $price = $subscription->items['data'][0]->price->id; 68 | $priceIndex = array_search($price, array_column(option('kreativ-anders.memberkit.tiers'), 'price'), false); 69 | $tier = option('kreativ-anders.memberkit.tiers')[$priceIndex]['name']; 70 | 71 | $kirby = kirby(); 72 | $kirby->impersonate('kirby'); 73 | 74 | try { 75 | 76 | // RESET KIRBY USER SUBSCRIPTION INFORMATION 77 | kirby()->user($customer->email)->update([ 78 | 'stripe_subscription' => null, 79 | 'stripe_status' => null, 80 | 'tier' => option('kreativ-anders.memberkit.tiers')[0]['name'] 81 | ]); 82 | 83 | } catch (Exception $e) { 84 | 85 | // LOG ERROR SOMEWHERE 86 | throw new Exception('Could not reset kirby user!'); 87 | } 88 | 89 | $kirby->impersonate(); 90 | }, 91 | // UPDATE KIRBY USER EMAIL VIA STRIPE WEBHOOK -------------------------------------------------------------------------------------- 92 | 'updateStripeEmailWebhook' => function ($customer) { 93 | 94 | $kirby = kirby(); 95 | $kirby->impersonate('kirby'); 96 | 97 | try { 98 | 99 | // UPDATE KIRBY USER EMAIL 100 | $kirby->users()->findBy('stripe_customer', $customer->id)->changeEmail($customer->email); 101 | 102 | } catch (Exception $e) { 103 | 104 | // LOG ERROR SOMEWHERE 105 | throw new Exception('Could not change kirby user email!'); 106 | } 107 | 108 | $kirby->impersonate(); 109 | } 110 | 111 | ]; -------------------------------------------------------------------------------- /hooks.php: -------------------------------------------------------------------------------- 1 | function ($user) { 14 | 15 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 16 | 17 | try { 18 | 19 | // CREATE STRIPE CUSTOMER 20 | $customer = $stripe->customers->create([ 21 | 'email' => $user->email() 22 | ]); 23 | 24 | } catch (Exception $e) { 25 | 26 | // LOG ERROR SOMEWHERE 27 | throw new Exception('Could not create stripe customer!'); 28 | } 29 | 30 | try { 31 | 32 | // UPDATE KIRBY USER - ROOT TIER (INDEX=0) 33 | kirby()->user($user->email())->update([ 34 | 'stripe_customer' => $customer->id, 35 | 'tier' => option('kreativ-anders.memberkit.tiers')[0]['name'] 36 | ]); 37 | 38 | } catch (Exception $e) { 39 | 40 | // LOG ERROR SOMEWHERE !!! 41 | throw new Exception('Could not update kirby user!'); 42 | } 43 | }, 44 | // CHANGE STRIPE USER EMAIL ------------------------------------------------------------------------------------- 45 | // https://stripe.com/docs/api/customers/update 46 | 'user.changeEmail:after' => function ($newUser, $oldUser) { 47 | 48 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 49 | 50 | try { 51 | 52 | // UPDATE STRIPE CUSTOMER 53 | $stripe->customers->update( 54 | $oldUser->stripe_customer(), 55 | ['email' => $newUser->email()] 56 | ); 57 | 58 | } catch(Exception $e) { 59 | 60 | // LOG ERROR SOMEWHERE !!! 61 | throw new Exception('Could not update stripe customer!'); 62 | } 63 | }, 64 | // DELETE KIRBY USER & CANCEL ALL STRIPE SUBSCRIPTIONS ------------------------------------------------------------ 65 | // https://stripe.com/docs/api/customers/delete 66 | 'user.delete:after' => function ($status, $user) { 67 | 68 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 69 | 70 | try { 71 | 72 | // DELETE STRIPE CUSTOMER 73 | $stripe->customers->delete( 74 | $user->stripe_customer(), 75 | [] 76 | ); 77 | 78 | } catch(Exception $e) { 79 | 80 | // LOG ERROR SOMEWHERE !!! 81 | throw new Exception('Could not delete stripe customer!'); 82 | } 83 | }, 84 | // RESERVE STRIPE ROUTES TO LOGGED-IN USERS 85 | // https://getkirby.com/docs/guide/routing#before-and-after-hooks__route-before 86 | 'route:before' => function ($route, $path, $method) { 87 | 88 | // DETERMINE ROUTE PATH AS BEST AS POSSIBLE (TRUE = MATCH) 89 | $subscribe = Str::contains($path, Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/subscribe/'); 90 | $portal = Str::contains($path, Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/portal'); 91 | $checkout = Str::contains($path, Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/success'); 92 | $cancel = Str::contains($path, Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/cancel/subscription'); 93 | 94 | // CANCEL ROUTE IS DEBUG MODE EXCLUSIVE 95 | if ($cancel && !option('debug')) { 96 | 97 | throw new Exception('Cancel stripe subscription via URL is only available in debug mode!'); 98 | } 99 | 100 | // REDIRECT TO HOMEPAGE WHEN USER IS NOT LOGGED-IN 101 | if (($subscribe || $portal || $checkout || $cancel) && !kirby()->user()) { 102 | go(); 103 | } 104 | } 105 | 106 | ]; 107 | -------------------------------------------------------------------------------- /userMethods.php: -------------------------------------------------------------------------------- 1 | function () { 13 | 14 | if ($this->stripe_subscription()->isEmpty()) { 15 | 16 | throw new Exception('No subscription to cancel!'); 17 | } 18 | 19 | // BUILD URL => STRIPE SLUG / ACTION NAME (CANCEL) / TYPE NAME (SUBSCRIPTION) 20 | $url = Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')); 21 | $url .= '/cancel/subscription'; 22 | 23 | return $url; 24 | }, 25 | // RETURN STRIPE WEBHOOK URL 26 | 'getStripeWebhookURL' => function () { 27 | 28 | // BUILD URL => STRIPE SLUG / ACTION NAME (WEBHOOK) 29 | $url = Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')); 30 | $url .= '/webhook'; 31 | 32 | return $url; 33 | }, 34 | // RETURN STRIPE SUBSCRIPTION CHECKOUT URL FOR TIER X (NAME AS PARAMETER) ----------------------------------------------------- 35 | 'getStripeCheckoutURL' => function ($tier) { 36 | 37 | // SEARCH TIER NAME AND CHECK FOR EXISTENCE 38 | $tierIndex = array_search($tier, array_column(option('kreativ-anders.memberkit.tiers'), 'name'), false); 39 | if (!$tierIndex || $tierIndex < 1) { 40 | 41 | throw new Exception('Tier does not exist!'); 42 | } 43 | 44 | // BUILD URL => STRIPE SLUG / ACTION NAME (SUBSCRIBE) / STRIPE TIER NAME (RAWURLENCODED) 45 | $url = Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')); 46 | $url .= '/subscribe'; 47 | $url .= '/' . rawurlencode(Str::lower(Str::trim(option('kreativ-anders.memberkit.tiers')[$tierIndex]['name']))); 48 | 49 | return $url; 50 | }, 51 | // RETURN STRIPE CUSTOMER PORTAL URL ------------------------------------------------------------------------------------------- 52 | 'getStripePortalURL' => function () { 53 | 54 | // BUILD URL => STRIPE SLUG / ACTION NAME (PORTAL) 55 | $url = Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')); 56 | $url .= '/portal'; 57 | 58 | return $url; 59 | }, 60 | // RETRIEVE STRIPE CUSTOMER (WITH SUBSCRIPTIONS) ------------------------------------------------------------------------------- 61 | 'retrieveStripeCustomer' => function () { 62 | 63 | if (!option('debug')) { 64 | 65 | throw new Exception('Retrieve stripe customer is only available in debug mode!'); 66 | } 67 | 68 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 69 | $customer = null; 70 | 71 | try { 72 | 73 | // RETRIEVE STRIPE CUSTOMER 74 | $customer = $stripe->customers->retrieve( 75 | $this->stripe_customer(), 76 | ['expand' => ['subscriptions']] 77 | ); 78 | 79 | } catch(Exception $e) { 80 | 81 | // LOG ERROR SOMEWHERE !!! 82 | throw new Exception('Retrieve stripe customer failed!'); 83 | } 84 | 85 | return $customer; 86 | }, 87 | // MERGE STRIPE CUSTOMER WITH KIRBY USER ---------------------------------------------------------------------------------------- 88 | 'mergeStripeCustomer' => function () { 89 | 90 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 91 | $customer = null; 92 | 93 | try { 94 | 95 | // RETRIEVE STRIPE CUSTOMER 96 | $customer = $stripe->customers->retrieve( 97 | $this->stripe_customer(), 98 | ['expand' => ['subscriptions']] 99 | ); 100 | 101 | } catch(Exception $e) { 102 | 103 | // LOG ERROR SOMEWHERE !!! 104 | throw new Exception('Retrieve stripe customer failed!'); 105 | } 106 | 107 | $subscription = $customer->subscriptions['data'][0]; 108 | 109 | // DETERMINE TIER NAME BY STRIPE PRICE ID 110 | $price = $subscription->items['data'][0]->price->id; 111 | $priceIndex = array_search($price, array_column(option('kreativ-anders.memberkit.tiers'), 'price'), false); 112 | $tier = option('kreativ-anders.memberkit.tiers')[$priceIndex]['name']; 113 | 114 | try { 115 | 116 | // UPDATE KIRBY USER 117 | $this->update([ 118 | 'stripe_subscription' => $subscription->id, 119 | 'stripe_status' => $subscription->status, 120 | 'tier' => $tier 121 | ]); 122 | 123 | $this->changeEmail($customer->email); 124 | 125 | return true; 126 | 127 | } catch (Exception $e) { 128 | 129 | // LOG ERROR SOMEWHERE !!! 130 | throw new Exception('Update kirby user failed!'); 131 | } 132 | 133 | return false; 134 | }, 135 | // CHECK USER PRIVILEGES BASED ON TIER (INDEX) ----------------------------------------------------------------------------- 136 | 'isAllowed' => function ($tier) { 137 | 138 | $userTier = $this->tier()->toString(); 139 | 140 | // GET INDEX FROM USER AND TIER NAME 141 | $userIndex = array_search($userTier, array_column(option('kreativ-anders.memberkit.tiers'), 'name'), false); 142 | $tierIndex = array_search($tier, array_column(option('kreativ-anders.memberkit.tiers'), 'name'), false); 143 | 144 | // NO SUBSCRIPTION OR NON-ACTIVE SUBSCRIPTION 145 | if ($this->tier()->isEmpty() || $this->stripe_subscription()->isEmpty() || $this->stripe_status()->isEmpty() || $this->stripe_status()->toString() != 'active') { 146 | 147 | return false; 148 | } 149 | 150 | // REQUESTED TIER MATCHES USER TIER 151 | if ($userTier === $tier) { 152 | 153 | return true; 154 | } 155 | 156 | // USER TIER IS HIGHER (PRIO) THAN REQUESTED TIER 157 | if ($userIndex >= $tierIndex) { 158 | 159 | return true; 160 | } 161 | 162 | // DEFAULT 163 | return false; 164 | }, 165 | 166 | ]; -------------------------------------------------------------------------------- /routes.php: -------------------------------------------------------------------------------- 1 | STRIPE SLUG / ACTION NAME (SUBSCRIBE) / STRIPE TIER NAME (RAWURLENCODED) 15 | 'pattern' => Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/subscribe/(:all)', 16 | 'action' => function ($tier) { 17 | 18 | // DETERMINE PRICE BY TIER NAME 19 | $tier = rawurldecode($tier); 20 | $tierIndex = array_search($tier, array_map("Str::lower", array_column(option('kreativ-anders.memberkit.tiers'), 'name')), false); 21 | $price = option('kreativ-anders.memberkit.tiers')[$tierIndex]['price']; 22 | 23 | // BUILD MAN-IN-THE-MIDDLE/SUCCESS URL => SITE URL / STRIPE SLUG / ACTION NAME (SUCCESS) 24 | $successURL = kirby()->site()->url() . '/'; 25 | $successURL .= Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')); 26 | $successURL .= '/success'; 27 | 28 | $customer = kirby()->user()->stripe_customer(); 29 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 30 | 31 | try { 32 | 33 | // CREATE STRIPE CHECKOUT SESSION 34 | $checkout = $stripe->checkout->sessions->create([ 35 | 'success_url' => $successURL, 36 | 'cancel_url' => option('kreativ-anders.memberkit.cancelURL'), 37 | 'allow_promotion_codes' => true, 38 | 'line_items' => [ 39 | [ 40 | 'price' => $price, 41 | 'quantity' => 1, 42 | ], 43 | ], 44 | 'mode' => 'subscription', 45 | 'customer' => kirby()->user()->stripe_customer(), 46 | ]); 47 | 48 | } catch(Exception $e) { 49 | 50 | // LOG ERROR SOMEWHERE !!! 51 | throw new Exception('Could not create stripe checkout session!'); 52 | } 53 | 54 | return [ 55 | 'url' => $checkout->url 56 | ]; 57 | } 58 | ], 59 | // CREATE STRIPE CUSTOMER PORTAL SESSION ---------------------------------------------------------------------------------------- 60 | [ 61 | // PATTERN => STRIPE SLUG / STRIPE PORTAL 62 | 'pattern' => Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/portal', 63 | 'action' => function () { 64 | 65 | // BUILD MAN-IN-THE-MIDDLE/RETURN URL => SITE URL 66 | $returnURL = kirby()->site()->url() . '/'; 67 | 68 | $customer = kirby()->user()->stripe_customer(); 69 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 70 | 71 | try { 72 | 73 | // CREATE STRIPE PORTAL SESSION 74 | $session = $stripe->billingPortal->sessions->create([ 75 | 'customer' => $customer, 76 | 'return_url' => $returnURL, 77 | ]); 78 | 79 | $url = $session->url; 80 | 81 | } catch(Exception $e) { 82 | 83 | // LOG ERROR SOMEWHERE !!! 84 | throw new Exception('Could not create stripe portal session!'); 85 | } 86 | 87 | // GO TO STRIPE PORTAL 88 | return go($url); 89 | } 90 | ], 91 | // CANCEL STRIPE SUBSCRIPTION ------------------------------------------------------------------------------------------------- 92 | [ 93 | // PATTERN => STRIPE SLUG / ACTION NAME (CANCEL) / TYPE NAME (SUBSCRIPTION) 94 | 'pattern' => Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/cancel/subscription', 95 | 'action' => function () { 96 | 97 | $subscription = kirby()->user()->stripe_subscription(); 98 | $email = kirby()->user()->email(); 99 | 100 | $stripe = new \Stripe\StripeClient(option('kreativ-anders.memberkit.secretKey')); 101 | 102 | try { 103 | 104 | // CANCEL STRIPE SUBSCRIPTION 105 | $stripe->subscriptions->cancel( 106 | $subscription, 107 | [] 108 | ); 109 | } catch(Exception $e) { 110 | 111 | // LOG ERROR SOMEWHERE !!! 112 | throw new Exception('Could not cancel stripe subscription!'); 113 | } 114 | 115 | try { 116 | 117 | // RESET KIRBY USER SUBSCRIPTION - ROOT TIER (INDEX=0) 118 | kirby()->user($email)->update([ 119 | 'stripe_subscription' => null, 120 | 'stripe_status' => null, 121 | 'tier' => option('kreativ-anders.memberkit.tiers')[0]['name'] 122 | ]); 123 | 124 | } catch(Exception $e) { 125 | 126 | // LOG ERROR SOMEWHERE !!! 127 | throw new Exception('Could not reset kirby user subscriptions!'); 128 | } 129 | 130 | return go(); 131 | } 132 | ], 133 | // UPDATE/MERGE KIRBY USER AFTER (SUCCESSFUL) CHECKOUT -------------------------------------------------------------------------- 134 | [ 135 | // PATTERN => STRIPE SLUG / ACTION NAME (SUCCESS) 136 | 'pattern' => Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/success', 137 | 'action' => function () { 138 | 139 | try { 140 | 141 | // MERGE STRIPE USER WITH KIRBY USER 142 | kirby()->user()->mergeStripeCustomer(); 143 | 144 | } catch(Exception $e) { 145 | 146 | // LOG ERROR SOMEWHERE !!! 147 | throw new Exception('Could not merge stripe customer into kirby user!'); 148 | } 149 | 150 | // REDIRECT TO CUSTOM SUCCESS PAGE 151 | return go(option('kreativ-anders.memberkit.successURL')); 152 | } 153 | ], 154 | // LISTEN TO STRIPE NOTIFICATIONS AKA STRIPE WEBHOOK ---------------------------------------------------------------------------- 155 | // https://stripe.com/docs/webhooks/integration-builder 156 | // --> NOT SECURED!!! 157 | [ 158 | // PATTERN => STRIPE SLUG / ACTION NAME (WEBHOOK) 159 | 'pattern' => Str::lower(option('kreativ-anders.memberkit.stripeURLSlug')) . '/webhook', 160 | 'action' => function () { 161 | 162 | \Stripe\Stripe::setApiKey(option('kreativ-anders.memberkit.secretKey')); 163 | 164 | $endpoint_secret = option('kreativ-anders.memberkit.webhookSecret'); 165 | 166 | $payload = @file_get_contents('php://input'); 167 | $event = null; 168 | 169 | try { 170 | 171 | $event = \Stripe\Event::constructFrom( 172 | json_decode($payload, true) 173 | ); 174 | 175 | } catch(\UnexpectedValueException $e) { 176 | 177 | http_response_code(400); 178 | exit(); 179 | } 180 | 181 | // VERIFY ENPOINT INTEGRITY 182 | if ($endpoint_secret) { 183 | 184 | $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE']; 185 | 186 | try { 187 | 188 | $event = \Stripe\Webhook::constructEvent( 189 | $payload, $sig_header, $endpoint_secret 190 | ); 191 | 192 | } catch(\Stripe\Exception\SignatureVerificationException $e) { 193 | 194 | http_response_code(400); 195 | exit(); 196 | } 197 | } 198 | 199 | // HANDLE THE EVENT 200 | // https://stripe.com/docs/api/events/types 201 | switch ($event->type) { 202 | 203 | case 'customer.subscription.updated': 204 | $subscription = $event->data->object; 205 | 206 | // UPDATE STRIPE SUBSCRIPTION FOR USER X 207 | kirby()->site()->updateStripeSubscriptionWebhook($subscription); 208 | 209 | break; 210 | 211 | case 'customer.subscription.deleted': 212 | $subscription = $event->data->object; 213 | 214 | // RESET KIRBY USER SUBSCRIPTION INFO 215 | kirby()->site()->cancelStripeSubscriptionWebhook($subscription); 216 | 217 | break; 218 | 219 | case 'customer.updated': 220 | $customer = $event->data->object; 221 | 222 | // UPDATE KIRBY USER EMAIL 223 | kirby()->site()->updateStripeEmailWebhook($customer); 224 | 225 | break; 226 | 227 | case 'invoice.payment_failed': 228 | $subscription = $event->data->object; 229 | 230 | // DURING CHECKOUT PROCEDURE: 231 | // NOTHING TO DO SINCE NOTHING WILL BE CHANGED REGARDING THE CUSTOMER STATUS 232 | 233 | // AFTER SUCCESSFUL CHECHOUT PROCEDURE: 234 | // UPDATE STRIPE SUBSCRIPTION FOR USER X (STATUS BECOMES 'past_due') 235 | kirby()->site()->updateStripeSubscriptionWebhook($subscription); 236 | 237 | break; 238 | 239 | default: 240 | 241 | throw new Exception('Received unknown stripe event type!'); 242 | } 243 | 244 | http_response_code(200); 245 | return '✔️ Success!'; 246 | }, 247 | // ENSURE ONLY POST REQUESTS ARE CAPTURED 248 | 'method' => 'POST' 249 | ], 250 | ]; 251 | }; 252 | 253 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![kirby-memberkit](https://socialify.git.ci/kreativ-anders/kirby-memberkit/image?description=1&language=1&logo=https%3A%2F%2Fstatic-assets.kreativ-anders.de%2Flogo%2Fdark.svg&pattern=Brick%20Wall&theme=Light) 2 | 3 | # Kirby Memberkit Plug-In 4 | 5 | * [What do you get?](#what-do-you-get) 6 | * [Functional Overview?](#function-overview) 7 | * [Logic Index](#logic-index) 8 | * [Installation](#installation) 9 | * [Get Started](#get-started) 10 | * [Test](#test) 11 | * [Examples](#examples) 12 | * [Notes](#notes) 13 | * [Kirby CMS Licence](#kirby-cms-license) 14 | * [Support](#support) 15 | 16 | ## What do you get? 17 | A versatile Kirby User Membership Plug-In powered by [Stripe](https://stripe.com/) ([Checkout](https://stripe.com/payments/checkout) + [Customer Portal](https://stripe.com/blog/billing-customer-portal)) for [Kirby CMS](https://getkirby.com). 18 | 19 | | :exclamation: This repository contains a git submodule that makes it working as a submodule itself more complex than it should be when there are dependency updates | 20 | |----------------------------------------------------------------------------------------------------------------------------------------------------------------------| 21 | 22 | ## Function Overview: 23 | 24 | **Function** | **Trigger** | **Logic** | **Comment** 25 | :---- | :---- | :---- | :---- 26 | Create stripe product(s) | Manual | [Stripe Products Dashboard](https://dashboard.stripe.com/products) | Here, you also add the prices inclusively the payment intervals (subscriptions), e.g. 1€/Month or 10€/Year. 27 | Configure subscription tier(s) | Manual | [Kirby Options](https://getkirby.com/docs/guide/configuration#using-options) | Every price you create yield a distinct API-ID that is required in your kirby config.php. ([Learn more about subscription tiers](#set-subscription-tiers)) 28 | Create stripe user(s) | Automatic | [Kirby Hooks](https://getkirby.com/docs/reference/system/options/hooks) | Creates a stripe customer and store the stripe customer id (*stripe_customer*) and the root subscription tier name (*tier*) in the Kirby user information. 29 | Migrate existing user(s) | Manual | [Kirby Users methods](https://getkirby.com/docs/reference/plugins/extensions/users-methods) | Only an admin can execute the migration. 30 | Update stripe user(s) email | Automatic | [Kirby Hooks](https://getkirby.com/docs/reference/system/options/hooks) | 31 | Delete stripe user(s) | Automatic | [Kirby Hooks](https://getkirby.com/docs/reference/system/options/hooks) | The customer's billing information will be permanently removed from stripe and all current subscriptions will be immediately canceled. But processed payments and invoices associated with the customer will remain. 32 | Subscribe user(s) | Manual/Automatic | [Kirby User methods](https://getkirby.com/docs/reference/plugins/extensions/user-methods), [Kirby Routes](https://getkirby.com/docs/guide/routing), [Kirby Snippets](https://getkirby.com/docs/guide/templates/snippets) | You can generate a distinct URL for a specific tier (with the corresponding payment interval) and pass it to the snippet which creates the checkout button (The button also includes the required stripe JavaScript). On click, the route generates a dedicated session and redirects to stripe checkout page. After successful checkout, an in-between route handles the merge of the stripe user with the Kirby user. 33 | Manage user(s) subscription | Automatic | [Stripe Customer Portal](https://stripe.com/blog/billing-customer-portal), [Kirby User methods](https://getkirby.com/docs/reference/plugins/extensions/user-methods) | Actions that are allowed to be performed can be set in [Stripe Customer Portal Dashboard](https://dashboard.stripe.com/settings/billing/portal), e.g. payment interval or change email address. Most of the actions should be working, but do not activate the option to change the quantity at the moment. 34 | Check user(s) permission | Manual/Automatic | [Kirby User methods](https://getkirby.com/docs/reference/plugins/extensions/user-methods) | Compare the parameter ($tier) with the users' tier based on the index of the tiers config order. 35 | Cancel user(s) subscription | Automatic | [Stripe Customer Portal](https://stripe.com/blog/billing-customer-portal), [Kirby Routes](https://getkirby.com/docs/guide/routing), [Kirby User methods](https://getkirby.com/docs/reference/plugins/extensions/user-methods) | For debug purposes it is also possible to cancel a user subscription by redirecting to an URL (works only in debug mode). The best approach is to redirect the user to the stripe customer portal. 36 | Keep everything in sync | Automatic | [Stripe Customer Portal](https://stripe.com/blog/billing-customer-portal), [Kirby Routes](https://getkirby.com/docs/guide/routing), [Kirby Site methods](https://getkirby.com/docs/reference/plugins/extensions/site-methods) | Changes within the [Stripe Customer Portal](https://stripe.com/blog/billing-customer-portal) are communicated via webhook notifications from stripe to a defined route that performs the corresponding actions. 37 | 38 | ### Logic Index 39 | 40 | **Kirby Logic** | **Abstract** | **Comment** 41 | :---- | :---- | :---- 42 | Options | | Jump to [config.php in Get Started](#configphp) or check [options.php](https://github.com/kreativ-anders/kirby-memberkit/blob/main/options.php) 43 | Hooks | [user.create:after](https://getkirby.com/docs/reference/plugins/hooks/user-create-after), [user.delete:after](https://getkirby.com/docs/reference/plugins/hooks/user-delete-after), [user.changeEmail:after](https://getkirby.com/docs/reference/plugins/hooks/user-changeemail-after), [route:before](https://getkirby.com/docs/reference/plugins/hooks/route-before) 44 | User methods | getStripeCancelURL(), getStripeWebhookURL(), getStripeCheckoutURL(), getStripePortalURL(), retrieveStripeCustomer(), mergeStripeCustomer(), isAllowed($tier) | Check [userMethods.php](https://github.com/kreativ-anders/kirby-memberkit/blob/main/userMethods.php) 45 | Users methods | migrateStripeCustomers()| Check [usersMethods.php](https://github.com/kreativ-anders/kirby-memberkit/blob/main/usersMethods.php) 46 | Site methods | updateStripeSubscriptionWebhook($subscription), cancelStripeSubscriptionWebhook($subscription), updateStripeEmailWebhook($customer) | Check [siteMethods.php](https://github.com/kreativ-anders/kirby-memberkit/blob/main/siteMethods.php) 47 | Routes | | Check [routes.php](https://github.com/kreativ-anders/kirby-memberkit/blob/main/routes.php) 48 | 49 | #### Why no (Kirby) API? 50 | Kirby API is very restrictive, which is good on one hand. But, on the other hand, it requires the user to have **panel access** permission what is IMHO not in your favor. So using routes is so for the workaround for certain tasks. This also applies to stripe webhooks. API calls to Kirby need to be authenticated which does not comply with stripe webhooks calls. 51 | 52 | ## Installation: 53 | 54 | > The assets/code you can download on GitHub website does not include stripe´s PHP library on default (since it is a git submodule)! Either use the latest release download link below or install the Plug-In as a git submodule as well. 55 | 56 | ### Download 57 | 1. Download the [**latest release** .zip-file (_kirby-memberkit-vX.X.X.zip_)](https://github.com/kreativ-anders/kirby-memberkit/releases). 58 | 1. Unzip the files. 59 | 1. Paste inside _../site/plugins/_. 60 | 1. Head over to **[Get Started](#get-started)**. 61 | 62 | ### Git Submodule (Recommended) 63 | You can add the Kirby Memberkit Plug-In as a git submodule as well: 64 | ````bash 65 | $ cd YOUR/PROJECT/ROOT 66 | $ git submodule add https://github.com/kreativ-anders/kirby-memberkit.git site/plugins/kirby-memberkit 67 | $ git submodule update --init --recursive 68 | $ git commit -am "Add Kirby Memberkit" 69 | ```` 70 | Run these commands to update the Plug-In (and all other submodules): 71 | > **main** not "master"! 72 | ````bash 73 | $ cd YOUR/PROJECT/ROOT 74 | $ git submodule foreach git checkout main 75 | $ git submodule foreach git pull 76 | $ git commit -am "Update submodules" 77 | $ git submodule update --init --recursive 78 | ```` 79 | 80 | ## Get Started: 81 | 82 | > Before diving deep, become familiar with [Stripe Checkout](https://stripe.com/de/payments/checkout), check the respective Docs of [Stripe Checkout](https://stripe.com/docs/payments/checkout) and check out [Stripe Customer Portal](https://stripe.com/docs/billing/subscriptions/customer-portal). 83 | 84 | ### Stripe Dashboard 85 | 86 | * [Create Products on Stripe](https://dashboard.stripe.com/products) 87 | * Add **prices** to the product(s) 88 | * [Configure Stripe Customer Portal](https://dashboard.stripe.com/settings/billing/portal) 89 | * [Set up an endpoint for Stripe Webhooks](https://dashboard.stripe.com/webhooks) 90 | * The (default) URL looks like "https://YOUR-DOMAIN.TLD/stripe-checkout/webhook" (See [Overwrite stripe URL slug (optional)](#overwrite-stripe-url-slug-optional) to change "stripe-checkout".) 91 | 92 | ### config.php 93 | 94 | #### Set stripe API keys 95 | ````php 96 | 'kreativ-anders.memberkit.secretKey' => 'sk_test_xxxx', 97 | 'kreativ-anders.memberkit.publicKey' => 'pk_test_xxxx', 98 | ```` 99 | #### Overwrite stripe URL slug (optional) 100 | This setting is just an additional layer to create collision-free routes/URLs like "https://YOUR-DOMAIN.TLD/stripe-checkout/portal" 101 | ````php 102 | 'kreativ-anders.memberkit.stripeURLSlug' => 'stripe-checkout', 103 | ```` 104 | #### Set cancel/success URLs 105 | Those pages do not exist! You need to create them by yourself. This is a great opportunity to welcome users after they successfully subscribed to a tier or show them help when they canceled the stripe checkout process. 106 | 107 | > Use absolute paths. There is no route checking against those URLs. 108 | 109 | ````php 110 | 'kreativ-anders.memberkit.successURL' => 'https://YOUR-DOMAIN.TLD/success', 111 | 'kreativ-anders.memberkit.cancelURL' => 'https://YOUR-DOMAIN.TLD/cancel', 112 | ```` 113 | #### Set stripe webhook secret 114 | To keep everything (securely) in sync it is important to set a webhook secret. 115 | ````php 116 | 'kreativ-anders.memberkit.webhookSecret' => 'whsec_xxx', 117 | ```` 118 | 119 | #### Set subscription tiers 120 | The subscription tier is a 2D-array and needs to be in an ordered sequence. This means the lowest tier is first (Free) and the highest tier in the end (Premium). The first index is always the entry/default tier after registration/cancelation. 121 | 122 | > Due to consistency the tier on index 0 holds a price, but it is never checked, so keep it null. Again, all the following tiers need to be greater than the previous one, e.g., FREE --> BASIC --> PREMIUM --> SUPER DELUXE. 123 | 124 | You also have to maintain **all** price API-IDs (payment intervals) within one product that have been created within stripe dashboard. This enables you to create mutliple payment intervals that look like the following in the config.php: 125 | 126 | ##### Basic Example 127 | ````php 128 | 'kreativ-anders.memberkit.tiers' => [ 129 | [ 'name' => 'Free' 130 | ,'price' => null], 131 | [ 'name' => 'Basic' 132 | ,'price' => 'price_xxxx'], 133 | [ 'name' => 'Premium' 134 | ,'price' => 'price_yyyy'], 135 | ], 136 | ```` 137 | ##### Creative (Crazy) Example 138 | 139 | > I hope nobody will ever dare to do something like the following! 140 | 141 | ````php 142 | 'kreativ-anders.memberkit.tiers' => [ 143 | [ 'name' => 'Free' 144 | ,'price' => null], 145 | [ 'name' => 'Basic - Daily' 146 | ,'price' => 'price_xxxabc'], 147 | [ 'name' => 'Basic - Weekly' 148 | ,'price' => 'price_xxxdef'], 149 | [ 'name' => 'Premium - Monthly' 150 | ,'price' => 'price_yyyghi'], 151 | [ 'name' => 'Premium - Biannual' 152 | ,'price' => 'price_yyyjkl'], 153 | [ 'name' => 'Deluxe - Yearly' 154 | ,'price' => 'price_zzzmno'], 155 | [ 'name' => 'Deluxe - Custom' 156 | ,'price' => 'price_yyyopq'], 157 | ], 158 | ```` 159 | 160 | ## Test: 161 | 162 | ### Local 163 | 164 | For local tests use the [Stripe CLI](https://stripe.com/docs/stripe-cli). There is also a very handy [Extension for VS Code](https://stripe.com/docs/stripe-vscode) available. 165 | 166 | > **Note** 167 | > Authenticate your Stripe CLI with your account. (Make sure you are logged in with your stripe account into the desired app in your default browser.) 168 | 169 | ````shell 170 | C:/PATH/TO/stripe.exe login 171 | ```` 172 | 173 | > Use the following line of code to listen and forward stripe request to your local environment. 174 | 175 | ````shell 176 | C:/PATH/TO/stripe.exe listen --forward-to http://YOUR-DOMAIN.TLD/stripe-checkout/webhook --forward-connect-to http://YOUR-DOMAIN.TLD/stripe-checkout/webhook 177 | ```` 178 | Afterward, the (VS Code) terminal prompts a line like this "Ready! Your webhook signing secret is **whsec_xxxx** (^C to quit)". 179 | 180 | **Maintain the secret within your [config.php](#set-stripe-webhook-secret)!** 181 | 182 | ### Going Live 183 | 184 | Head over to [Stripe´s Webhook Dashboard](https://dashboard.stripe.com/webhooks) and add a new endpoint for your application. 185 | The URL should look like "https://YOUR-DOMAIN.TLD/stripe-checkout/webhook". 186 | Finally, add the following events that are handled by this Plug-In: 187 | 188 | - customer.subscription.updated 189 | - customer.subscription.deleted 190 | - customer.updated 191 | - invoice.payment_failed 192 | 193 | ## Examples: 194 | 195 | ### Migrate existing users 196 | 197 | Call the dedicated users method once somewhere, e.g. snippet: 198 | 199 | ````php 200 | var_dump($kirby->users()->migrateStripeCustomers()); // TO SEE THE NUMBER OF MIGRATIONS 201 | ```` 202 | 203 | > For every kirby user, a stripe user will be created, so (personal) information is shared with stripe, e.g. email address. 204 | 205 | ### Create a stripe user 206 | 207 | Stripe users are created automatically via hook after a successful user creation/registration. Check out the [Kirby Add-on Userkit](https://github.com/kreativ-anders/kirby-userkit) to allow self-registration and updates to kirby users. 208 | The logic applies to users' email changes and user deletion as well. 209 | 210 | ### Add a stripe checkout button 211 | 212 | 1. Create a URL for a dedicated tier: 213 | 214 | ````php 215 | $url = $kirby->user()->getStripeCheckoutURL(option('kreativ-anders.memberkit.tiers')[1]['name']); // Basic tier 216 | ```` 217 | You can also call the method with a hardcoded string: 218 | ````php 219 | $url = $kirby->user()->getStripeCheckoutURL('Premium'); // Premium tier 220 | ```` 221 | 222 | > The URL is handled via a route and returns a JSON with the required stripe session ID. 223 | 224 | 2. Add the stripe checkout button (via snippet): 225 | For styling or additional logic (with your JavaScript) you can pass an id, classes, and a text - whatever you want. 226 | 227 | ````php 228 | snippet('stripe-checkout-button', [ 'id' => 'basic-checkout-button' 229 | ,'classes' => '' 230 | ,'text' => 'Basic Checkout' 231 | ,'url' => $url]); 232 | ```` 233 | 234 | The snippet also includes the required JavaScript to initialize the checkout and redirect to Stripe itself. 235 | 236 | > Just be careful not to add the same id twice! 237 | 238 | ### Successful subscription checkout 239 | 240 | In case the stripe checkout was successful, stripe redirects to a hidden URL (captured via another route internally) that handles the user update procedure, e.g., payment status or set the subscribed tier name. Afterward, the redirect to **YOUR** individual succuss page ([set in config.php](#set-cancelsuccess-urls)) is triggered. In case the user return via cancel command on stripe checkout, the user will be immediately redirected to **YOUR** individual cancel page (set in config.php as well). 241 | 242 | ### Manage stripe subscription 243 | 244 | Changes regarding payment interval, subscription upgrades/downgrades, or even cancelation is **all covered** within the [Stripe Customer Portal](https://stripe.com/blog/billing-customer-portal). The only thing you have to do is showing the link to the portal somewhere somehow: 245 | 246 | ````php 247 | $portal_url = $kirby->user()->getStripePortalURL(); 248 | $portal = '(link: ' . $portal_url . ' text: Stripe Portal URL target: _blank)'; 249 | echo kirbytext($portal); 250 | ```` 251 | 252 | ### Change subscription 253 | 254 | > Yeah, well ... Let´s get back to the [previous section about managing subscriptions](#manage-stripe-subscription). 255 | 256 | ### Cancel subscription 257 | 258 | > Same! [Stripe Customer Portal](https://stripe.com/blog/billing-customer-portal) rules all of it! 259 | 260 | For debug purposes only it is possible to cancel a subscription by simply redirecting to an URL. 261 | 262 | ````php 263 | $cancel_url = $kirby->user()->getStripeCancelURL(); 264 | $cancel = '(link: ' . $cancel_url . ' text: Stripe Cancel URL)'; 265 | echo kirbytext($cancel); 266 | ```` 267 | 268 | ### Show/Hide functions or content based on subscribed tier 269 | 270 | Use the following condition somewhere in your templates or snippets: 271 | 272 | ````php 273 | user() && $kirby->user()->isAllowed(option('kreativ-anders.memberkit.tiers')[1]['name'])): ?> 274 |

275 | Basic visible 276 |

277 | 278 | ```` 279 | 280 | > The example code above would be the "safe" version of passing the correct tier name, but the following is more user-friendly... 281 | 282 | ````php 283 | user() && $kirby->user()->isAllowed('Premium')): ?> 284 |

285 | Premium visible 286 |

287 | 288 | ```` 289 | 290 | If you are using a construction with multiple pricing intervals for the same tier, make sure to use the first occurrence of your version for the comparison! 291 | 292 | > Remember: The 2D-array of your tiers needs to be in an ascending sequence! 293 | 294 | For illustration we assume an user with the tier **Premium - Monthly** (Look at the [creative (crazy) tier example above](#creative-crazy-example)): 295 | ````php 296 | user() && $kirby->user()->isAllowed('Premium - Monthly')): ?> 297 |

298 | The user will see the content since the tier is matching exactly! 299 |

300 | 301 | 302 | user() && $kirby->user()->isAllowed('Premium - Biannual')): ?> 303 |

304 | The user will NOT see the content, since "Premium - Biannual" is greater than "Premium - Monthly" 305 | from an order perspective. So make sure to always use the lower tier name for comparison to ensure all Premium users independent from their payment interval will able to see/use the content/functionality behind! 306 |

307 | 308 | ```` 309 | 310 | ### Overall snippet 311 | 312 | Place this snippet somewhere in your template, e.g. intro.php. 313 | 314 | ````php 315 | if ($kirby->user()) { 316 | 317 | $checkout_url = $kirby->user()->getStripeCheckoutURL(option('kreativ-anders.memberkit.tiers')[1]['name']); // Basic Tier 318 | $checkout = '(link: ' . $checkout_url . ' text: Stripe Checkout Callback-URL (Tier 1) = ' . $checkout_url . 'target: _blank)'; 319 | echo kirbytext($checkout); 320 | 321 | echo "
"; 322 | echo "Checkout Button (Tier 1) = "; 323 | 324 | snippet('stripe-checkout-button', [ 'id' => 'basic-checkout-button' 325 | ,'classes' => '' 326 | ,'text' => 'Basic Checkout' 327 | ,'url' => $checkout_url]); 328 | 329 | echo "

"; 330 | echo "Stripe Customer Subscription Debug = "; 331 | echo "
";
332 | 
333 |   $customer = $kirby->user()->retrieveStripeCustomer();
334 |   $subscription = $customer->subscriptions['data'];
335 |   var_dump($subscription);
336 | 
337 |   echo "
"; 338 | echo "
"; 339 | 340 | $portal_url = $kirby->user()->getStripePortalURL(); 341 | $portal = '(link: ' . $portal_url . ' text: Stripe Customer Portal URL = ' . $portal_url . ' target: _blank)'; 342 | echo kirbytext($portal); 343 | 344 | echo "
"; 345 | 346 | // $cancel_url = $kirby->user()->getStripeCancelURL(); 347 | // $cancel = '(link: ' . $cancel_url . ' text: Stripe Customer Subscription Cancel URL = ' . $cancel_url . ')'; 348 | // echo kirbytext($cancel); 349 | 350 | } 351 | ```` 352 | 353 | ## Notes: 354 | This Plug-In is built for Kirby CMS based on **Kirby´s Starterkit v3.8.2** with the Add-On **[kirby-userkit](https://github.com/kreativ-anders/kirby-userkit)** and **Stripe API Version 2022-11-15**. 355 | 356 | ### Kirby CMS license 357 | 358 | **Kirby CMS requires a dedicated license:** 359 | 360 | *Go to [https://getkirby.com/buy](https://getkirby.com/buy)* 361 | 362 | ## Warning: 363 | Do not subscribe multiple tiers to a user. Even though this should not be possible with the Plug-In by default (since the snippet "stripe-checkout-button" will check for subscriptions), be aware not to do it within the stripe dashboard anyway! 364 | Use with caution and test before of course. 365 | 366 | ## Disclaimer 367 | 368 | The source code is provided "as is" with no guarantee. Use it at your own risk and always test it yourself before using it in a production environment. If you find any issues, please create a new issue. 369 | 370 | ## Support 371 | 372 | In case this Plug-In saved you some time and energy consider supporting kreativ-anders by donating via [PayPal](https://paypal.me/kreativanders), or becoming a **GitHub Sponsor**. 373 | --------------------------------------------------------------------------------