{"id":19034461,"url":"https://github.com/shannah/xataface-module-stripe","last_synced_at":"2026-06-25T07:31:23.890Z","repository":{"id":149624892,"uuid":"325806743","full_name":"shannah/xataface-module-stripe","owner":"shannah","description":"Stripe payments module for Xataface","archived":false,"fork":false,"pushed_at":"2024-03-16T18:27:46.000Z","size":420,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-15T09:15:01.265Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shannah.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-12-31T13:34:02.000Z","updated_at":"2022-08-02T11:08:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"73a6274e-d2e5-4f02-a843-8587c1ee3ea7","html_url":"https://github.com/shannah/xataface-module-stripe","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shannah%2Fxataface-module-stripe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shannah%2Fxataface-module-stripe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shannah%2Fxataface-module-stripe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shannah%2Fxataface-module-stripe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shannah","download_url":"https://codeload.github.com/shannah/xataface-module-stripe/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240088848,"owners_count":19746145,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T21:45:26.578Z","updated_at":"2026-05-04T22:30:18.563Z","avatar_url":"https://github.com/shannah.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"= Xataface Stripe Module\n\nSupport for https://stripe.com[Stripe] payments in http://xataface.com[Xataface]\n\n== Synopsis\n\nThis module adds support for https://stripe.com[Stripe] payments in your http://xataface.com[Xataface] application.  While this module may be adapted to provide any type of workflow that requires payments, it is currently designed to support membership subscriptions, or something with a small number of products that can be purchased.  For example if you wanted to add different subscription levels to your application such as \"Free\", \"Basic\", and \"Premium\", this module would make it possible to use Stripe for the payments and maintenance of the different subscription levels.\n\n== Prerequisites\n\n. **Authentication**. The Stripe module requires that you have set up authentication in your application, so that you have a functioning \"users\" table.  This will allow it to map stripe customer IDs to your application's user accounts.\n. **Stripe PHP SDK**.  Your application should install the https://github.com/stripe/stripe-php[Stripe PHP SDK dependency] via composer. E.g.\n+\n[source,bash]\n----\ncomposer require stripe/stripe-php\n----\n\n== Installation\n\n. Download this module and copy it into your application's \"modules/stripe\" directory.\n+\n====\n.E.g. Using git\n[source,bash]\n----\ncd modules\ngit clone https://github.com/shannah/xataface-module-stripe stripe\n----\n====\n. Add the stripe module to the \"_modules\" section of your conf.ini file:\n+\n[source,ini]\n----\n[_modules]\n    modules_stripe=modules/stripe/stripe.php\n----\n. Add \"stripe\\__test\" and \"stripe_plans__test\" sections to your app's conf.ini file.\n+\n====\n\nNOTE: The \"stripe\\__test\" and \"stripe_plans__test\" sections are designed to be used with your Stripe test data, and not your live stripe data.  Once you have your site working in test mode, you will copy these sections to \"stripe\" and \"stripe_plans\" sections respectively.  But it is important to be aware that your live stripe keys and plan IDs will be different than your test keys and plan IDs.\n\n.Your app's conf.ini file.\n[source,ini]\n----\n[stripe__test]\n    stripe_secret_key = sk_test_XXXXXX \n    stripe_publishable_key = pk_test_XXXXXXXXXX \n    stripe_webhook_secret = whsec_XXXXXXX \n\tsuccess_action=YOUR_SUCCES_ACTION \n    \n[stripe_plans__test]\n    price_12345=\"$2.99 USD per month\" \n    price_45678=\"$5.00 CAD per month\"\n    price_XXXX=\"$39.99 CAD per year\"\n    ...\n----\n\n**Properties**\n\nstripe_secret_key::\nYour Stripe secret key.  You can find this in your https://dashboard.stripe.com[stripe dashboard] in the \"API Keys\" section.  See https://stripe.com/docs/keys[Stripe's documentation] for information about their API keys.\n\nstripe_publishable_key::\nYour Stripe publishable key. You can find this in your https://dashboard.stripe.com[stripe dashboard] in the \"API Keys\" section.  See https://stripe.com/docs/keys[Stripe's documentation] for information about their API keys.\n\nstripe_webhook_secret::\nYour stripe webhook secret. You'll need to set up a webhook for Stripe to notify your app when relevant events take place.  See \u003c\u003cwebhook\u003e\u003e for more information about this.\n\nsuccess_action::\nThe name of your \"success\" action.  This is an action that will be called when the user successfully purchases a plan.  See \u003c\u003csuccess-action\u003e\u003e for more information about this.\n\n**The \"stripe_plans__test\" Section**\n\nThe properties in the \"stripe_plans__test\" section are all key/value pairs matching a stripe price ID with a label for the price.  The key is the price ID, and the value the label that will be displayed on the button to purchase the \"price\".  See https://stripe.com/docs/billing/prices-guide[Stripe's Products and Prices] documentation for more information about prices.  \n\n====\n. Implement `stripe_isSandbox()` in your application delegate class to return `true`.\n+\n====\nThere are two modes in stripe: \n\n1. Test Mode\n2. Live Mode\n\nYou always need to be aware which mode your application is running in.  When your application is running in test mode, it will use the \"stripe\\__test\" and \"stripe_plans__test\" sections of your conf.ini file for configuration.  These configuration settings should map to \"test mode\" data in your Stripe account.  \n\nWhen in test mode, you cannot use real credit cards to make payments.  However, stripe provides a list of \"fake\" credit card numbers that you can use.  See https://stripe.com/docs/testing[Stripe's testing documentation] for a list of these credit cards, and more details about its test environment.\n\nBy default the Xataface stripe module will run in test mode when running on localhost, and live mode when running on any other domain.  You can override this behaviour by implementing the `stripe_isSandbox()` method in your application delegate class.\n\nHere are some examples:\n\n.Always in test mode\n[source,php]\n----\nfunction stripe_isSandbox() {\n    return true;\n}\n----\n\n.Always in live mode\n[source,php]\n----\nfunction stripe_isSandbox() {\n    return false;\n}\n----\n\n.In live mode if running on example.com.  Otherwise, in test mode.\n[source,php]\n----\nfunction stripe_isSandbox() {\n    return (@$_SERVER['HTTP_HOST'] == 'example.com');\n}\n----\n====\n. Implement \"success action\".  See \u003c\u003csuccess-action\u003e\u003e for more details about this step.\n. Implement the stripe webhook.  See \u003c\u003cwebhook\u003e\u003e for more details about this step.\n. Run the \"Refresh Stripe Products\" action to import all of the products you have defined in your https://dashboard.stripe.com[Stripe dashboard] into your local database.\n+\nimage::images/Image-311220-071924.198.png[]\n\n[#success-action]\n=== Setting up Success Action\n\nThe general workflow for a stripe transaction is for the user to visit the `stripe_plans` action, where it will present the available plans that the user can subscribe to.\n\nimage::images/Image-311220-070538.883.png[]\n\nThen the user will click on one of the buttons to subscribe, and they'll be taken to the stripe payment form to enter their credit card information.  Upon successful payment, the stripe will direct the user back to your application's \"success action\".  Generally you'll want this action to display some informative message, thanking the user for subscribing.  \n\nYou can name this action anything you like, but you should reference it by the `success_action` directive in the `stripe__test` section of your conf.ini file.  Here is a sample action, that I have named `nn_stripe_success`:\n\n.nn_stripe_success action defined in actions/nn_stripe_success.php\n[source,php]\n----\n\u003c?php\nimport('xf/core/XFException.php');\nclass actions_nn_stripe_success {\n\tfunction handle($params) {\n\t\t$user = getUser();\n\t\tif (!$user) {\n\t\t\txf\\core\\XFException::throwPermissionDenied();\n\t\t}\n\t\tdf_display([], 'narratednews/stripe_success_page.html');\n\t}\n}\n?\u003e\n----\n\nNotice that this displays the template at narratednews/stripe_success_page.html.  The contents of that template are as follows:\n\n.Success page template located at templates/narratednews/stripe_success_page.html\n[source,html]\n----\n{use_macro file=\"Dataface_Main_Template.html\"}\n\t{fill_slot name=\"main_section\"}\n\t\t\u003ch1\u003eYour payment was successful\u003c/h1\u003e\n\t\t\n\t\t{assign var=plan value=$ENV.user-\u003eval('plan')}\n\t\t{assign var=credits value=$ENV.user-\u003eval('credits')}\n\t\t\u003cp\u003eYou now have {$credits-\u003eval('credits')} credits, and your plan is {$plan-\u003eval('plan_name')|escape}.\u003c/p\u003e\n\t\t\n\t\t\u003cp\u003eYour balance will be topped up to {$plan-\u003eval('credits')} credits on the {$credits-\u003eval('plan_renewal_day')}{$credits-\u003eval('plan_renewal_day_suffix')} day of each month.\u003c/p\u003e\n\t\t\n\t\t\u003cp\u003eYou can update or cancel your plan at any time via the \u003ca href=\"{$ENV.DATAFACE_SITE_HREF}?-action=stripe_customer_portal\" target=\"_blank\"\u003esecure customer portal\u003c/a\u003e.\n\t\tIf you cancel your plan, your plan will remain active until the end of your billing period, at which point your plan will automatically revert to a basic account, and your balance will be adjusted to the basic level of 5 credits.\n\t\t\u003c/p\u003e\n\t\t\n\t\t\u003ch2\u003eWhere to go from here\u003c/h2\u003e\n\t\t\n\t\t\u003cul\u003e\n\t\t\t\u003cli\u003e\u003ca href=\"{$ENV.DATAFACE_SITE_HREF}?-action=xf_my_profile\"\u003eView my account\u003c/a\u003e\u003c/li\u003e\n\t\t\t\u003cli\u003e\u003ca href=\"{$ENV.DATAFACE_SITE_HREF}?-table=_tmp_newsfeed\"\u003eView my news feed\u003c/a\u003e\u003c/li\u003e\n\t\t\t\u003cli\u003e\u003ca href=\"{$ENV.DATAFACE_SITE_HREF}?-table=_tmp_nn_playlist\"\u003eView my playlist\u003c/a\u003e\u003c/li\u003e\n\t\t\t\u003cli\u003e\u003ca href=\"{$ENV.DATAFACE_SITE_HREF}?-table=_tmp_feed_catalog\"\u003eDiscover Content\u003c/a\u003e\u003c/li\u003e\n\t\t\u003c/ul\u003e\n\t\t\n\t{/fill_slot}\n{/use_macro}\n----\n\n\nThere are many aspects to this snippet that are proprietary to this application.  E.g. the \"users\" table of this application includes some calculated fields like \"plan\" and \"credits\" which keep track of which plan the user is currently subscribed to and how many credits they have.  These values are kept in sync using the webhook, which is called by Stripe after certain events occur.  See \u003c\u003cwebhook\u003e\u003e for details on setting up the web hook.\n\n[#webhook]\n=== Setting up the Stripe Webhook\n\nIn order for this module to function correctly, you'll need to create a webhook in your https://dashboard.stripe.com[Stripe dashboard] for the `stripe_webhook` action in your application.  For example, if your application is hosted at http://example.com/index.php, then the endpoint you'll need to set up will be at \"http://example.com/index.php?-action=stripe_webhook\".\n\nYou'll want this webhook to receive \"customer\" and \"checkout\" event types.  The following is a screenshot of the webhook details for a sample application:\n\nimage::images/Image-311220-072448.716.png[]\n\nFor more information about creating webhooks in Stripe see https://stripe.com/docs/webhooks[Receive event notifications with webooks] in Stripe's documentation section.\n\n[TIP]\n====\nDuring development it is common to run your application on localhost, or somewhere that isn't accessible on the internet.  In such cases, you'll need to use the https://stripe.com/docs/stripe-cli[Stripe CLI] to receive the webhook events.  See https://stripe.com/docs/stripe-cli/webhooks[Listen to webhook events] in the Stripe CLI docs for details on how to set this up.\n\nI set up this shell script to launch the CLI with my app's endpoint on my local machine, which is running my Xataface application on port 9090.\n\n[source,bash]\n----\n#!/bin/bash\nstripe listen --forward-to localhost:9090/admin.php?-action=stripe_webhook\n----\n\n====\n\n==== Implementing the Webhook Callback\n\nThe `stripe_webhook` action will validate the the incoming webhook request from stripe, then it will update its internal tables with the information that it receives.  Then it will pass the event to the application delegate class' `stripe_webhook()` method.  If you don't implement this method, it will just skip this step, however, you'll most likely want to implement it so that you can respond to events like when the user subscribes to a plan, or cancels their plan.  \n\nThe signature for this method is:\n\n[source,php]\n----\nfunction stripe_webhook(\\Stripe\\Event $event, Dataface_Record $record) {\n    // ...\n}\n----\n\n**Parameters**\n\n`$event : \\Stripe\\Event`::\nThe https://stripe.com/docs/api/events/object[Event object] received from the https://stripe.com/docs/api?lang=php[Stripe PHP API].  \n\n`$record : Dataface_Record`::\nA record from the `stripe_transactions` table corresponding to this event.  The stripe_transactions table logs all events that are received through the webhook.  It stores things like the customer ID and JSON representations of the event data.\n\n**What to do in the Webhook**\n\nThat most common thing you'll want to do inside your webhook callback is to update your user account details to reflect their current subscription levels.  For example, suppose users of your application receive a certain number of \"credits\" per month - a number that depends on their subscription level.\n\nA very simple implementation of this credit system might have a \"credits\" column in the \"users\" table to keep track of the user's available credits.  In this case your webhook will want to respond to events like `customer.subscription.created`, `customer.subscription.updated`, and `customer.subscription.deleted` so that you can update the credits of the customer accordingly.\n\nThe following is a snippet from a sample application that implements the webhook:\n\n.The stripe_webhook() method implemented in the application delegate class (conf/ApplicationDelegate.php).\n[source,php]\n----\nfunction stripe_webhook(\\Stripe\\Event $event, Dataface_Record $record) {\n\timport(XFAPPROOT.'inc/stripeWebhook.func.php');\n\tnn\\stripe\\webhook\\stripeWebhook($event, $record);\n}\n----\n\n.inc/stripeWebhook.func.php\n[source,php]\n----\n\u003c?php\nnamespace nn\\stripe\\webhook;\n\nuse \\Dataface_Record;\nuse \\Exception;\nuse function \\nn_info;\nuse function \\nn_error;\nuse function \\df_get_record;\nuse function \\import;\nuse function \\initUserCredits;\n\n\t\n/**\n * Implementation of the ApplicationDelegate::stripe_webhook callback.  Refactored into separate file for performance.\n * @param \\Stripe\\Event $event The webhook event.  \n * @param Dataface_Record $record The stripe_transactions record.\n */\nfunction stripeWebhook(\\Stripe\\Event $event, Dataface_Record $record) {\n\t$handler = new Handler($event, $record);\n\t$handler-\u003erun();\n\t\n}\n\n/**\n * Private implementation class for the stripe webhook that lets us break down the handling\n * of the webhook based on the type.\n */\nclass Handler {\n\tprivate $event;\n\tprivate $record;\n\t\n\t/**\n\t * @type Dataface_Record[users]\n\t */\n\tprivate $user;\n\t\n\t/**\n\t * @type int (userid column of users table)\n\t */\n\tprivate $userId;\n\t\n\t/**\n\t * @type Dataface_Record[stripe_customers]\n\t */\n\tprivate $customerRec;\n\tprivate $type;\n\tprivate $previous_attributes;\n\tprivate $object;\n\tprivate $canceled = true;\n\t\n\t/**\n\t * @type \\Dataface_Record from the subscription_plans table\n\t */\n\tprivate $subscription_plan;\n\n\t\n\tfunction __construct(\\Stripe\\Event $event, Dataface_Record $record) {\n\t\t$this-\u003eevent = $event;\n\t\t$this-\u003erecord = $record;\n\t\t\n\t\t$type = $event['type'];\n\t\tif (strpos($type, 'customer.subscription.') !== 0) {\n\t\t\t//nn_error(\"Not customer subscription.  Found \".$type);\n\t\t\treturn;\n\t\t}\n\t\t$this-\u003etype = $type;\n\t    $this-\u003eobject = $event['data']['object'];\n\t\t$this-\u003eprevious_attributes = [];\n\t\tif (@$event['data']['previous_attributes']) {\n\t\t\t$this-\u003eprevious_attributes = $event['data']['previous_attributes'];\n\t\t}\n\t\t\n\t\n\t    $this-\u003ecustomer = @$this-\u003eobject['customer'];\n\t\tif (!@$this-\u003ecustomer) {\n\t\t\tnn_error(\"No customer specified in webhook.  Data: \".json_encode($event), \"#stripe_webhook\");\n\t\t\treturn;\n\t\t}\n\t\tfor ($i=0; $i\u003c5; $i++) {\n\t\t\tif ($this-\u003ecustomerRec) break;\n\t\t\t$this-\u003ecustomerRec = df_get_record('stripe_customers', ['customer_id' =\u003e '=' . $this-\u003ecustomer]);\n\t\t\tif (!$this-\u003ecustomerRec) {\n\t\t\t\t$customerRefId = @$event['data']['customer_reference_id'];\n\t\t\t\tif ($customerRefId) {\n\t\t\t\t\t$this-\u003ecustomerRec = new Dataface_Record('stripe_customers', []);\n\t\t\t\t\t$this-\u003ecustomerRec-\u003esetValues([\n\t\t\t\t\t\t'username' =\u003e $customerRefId,\n\t\t\t\t\t\t'customer_id' =\u003e $this-\u003ecustomer,\n\t\t\t\t\t\t'currency' =\u003e $data['items']['data'][0]['currency']\n\t\t\t\t\t]);\n\t\t\t\t\ttry {\n\t\t\t\t\t\t$res = $this-\u003ecustomerRec-\u003esave();\n\t\t\t\t\t\tif (PEAR::isError($res)) {\n\t\t\t\t\t\t\tnn_error(\"Failed to insert stripe customer record. \".$res-\u003egetMessage().\". Record vals: \".json_encode($this-\u003ecustomerRec-\u003evals()).\"; Event data: \".json_encode($event), '#stripe_webhook');\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t} catch (\\Exception $ex) {\n\t\t\t\t\t\tnn_error(\"Failed to insert stripe customer record. \".$ex-\u003egetMessage().\". Record vals: \".json_encode($this-\u003ecustomerRec-\u003evals()).\"; Event data: \".json_encode($event), '#stripe_webhook', $ex);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\n\t\t\t}\n\t\t\tif (!$this-\u003ecustomerRec) {\n\t\t\t\t// Give the other webhooks a chance to possibly add the customer record\n\t\t\t\tsleep(1);\n\t\t\t}\n\t\t}\n\t\t\n\t\tif (!$this-\u003ecustomerRec) {\n\t\t\tnn_error(\"Failed to load customer record for customer \".$this-\u003ecustomer.\" in stripe_webhook.  Data: \".json_encode($event), \"#stripe_webhook\");\n\t\t\treturn;\n\t\t}\n\t\n\t\t$this-\u003euser = df_get_record('users', ['username' =\u003e '=' . $this-\u003ecustomerRec-\u003eval('username')]);\n\t\tif (!$this-\u003euser) {\n\t\t\tnn_error(\"Failedto load userr ecord for customer \".$this-\u003ecustomer.\" username=\".$this-\u003ecustomerRec-\u003eval('username').\".  Data\".json_encode($event), \"#stripe_webhook\");\n\t\t\treturn;\n\t\t}\n\t\t$this-\u003euserId = $this-\u003euser-\u003eval('userid');\n\t\t$this-\u003ecanceled = false;\n\t}\n\t\n\t\n\tfunction run() {\n\t\tif ($this-\u003ecanceled) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tswitch ($this-\u003etype) {\n\t\t\tcase 'customer.subscription.created' :  return $this-\u003esubscription_created();\n\t\t\tcase 'customer.subscription.updated' : return $this-\u003esubscription_updated();\n\t\t\tcase 'customer.subscription.deleted' : return $this-\u003esubscription_deleted();\n\t\t\tcase 'customer.subscription.trial_will_end' : return $this-\u003esubscription_trial_will_end();\n\t\t}\n\t}\n\t\n\tfunction subscription_created() {\n\t\t$transactionsDelegate = \\Dataface_Table::loadTable('user_credit_transactions')-\u003egetDelegate();\n\t\t\n\t\timport(XFAPPROOT.'inc/initUserCredits.func.php');\n\t\tinitUserCredits($this-\u003euserId);\n\n\t\t$credits = df_get_record('user_credits', ['userid' =\u003e '=' . $this-\u003euserId]);\n\t\tif (!$credits) {\n\t\t\tnn_error(\"Failed to load user_credits for user \".$this-\u003euserId, \"#stripe_webhook\");\n\t\t\treturn;\n\t\t}\n\t\tnn_info(\"Customer subscription created user \".$this-\u003euser-\u003eval('username').\". data=\".json_encode($this-\u003eevent), '#stripe_webhook #subscription.created');\n\t\n\t\t// TODO setup user plan\n\t\t\n\t\t$plan = $this-\u003esubscription_plan();\n\t\tif (!$plan) {\n\t\t\t$_plan = $this-\u003eplan(0);\n\t\t\tnn_error(\"No subscription plan found in subscription.created.  Plan data was \".json_encode($_plan).\" Item data: \".json_encode($this-\u003eitem(0)).\"; items: \".json_encode($this-\u003eitems()), '#stripe_webhook #subscription.created', true);\n\t\t\treturn;\n\t\t}\n\t\t\t\n\t\t$planCredits = intval($plan-\u003eval('credits'));\n\t\t$currentCredits = intval($credits-\u003eval('credits'));\n\t\t$creditsDue = 0;\n\t\tif ($currentCredits \u003c $planCredits) {\n\t\t\t$creditsDue = $planCredits - $currentCredits;\n\t\t}\n\t\t\n\t\t$updated = false;\n\t\tif ($credits-\u003eval('plan_id') != $plan-\u003eval('plan_id')) {\n\t\t\t$credits-\u003esetValue('plan_id',$plan-\u003eval('plan_id'));\n\t\t\t$updated = true;\n\t\t\t\n\t\t}\n\t\t$renewalDay = min(28, intval(date('d')));\n\t\tif (intval($credits-\u003eval('plan_renewal_day')) !== $renewalDay) {\n\t\t\t$credits-\u003esetValue('plan_renewal_day', $renewalDay);\n\t\t\t$updated = true;\n\t\t}\n\t\t\n\t\t$credits-\u003esetValue('last_renewal_date', date('Y-m-d H:i:s'));\n\t\t$updated = true;\n\t\t\n\t\tif (@$object['current_period_end']) {\n\t\t\t$credits-\u003esetValue('plan_expiry_date', date('Y-m-d', intval($object['current_period_end'])));\n\t\t\t$updated = true;\n\t\t}\n\t\t\n\t\tif ($updated) {\n\t\t\t$res = $credits-\u003esave();\n\t\t\tif (\\PEAR::isError($res)) {\n\t\t\t\tnn_error(\"Failed to save user credits update upon subscription created.  \". $res-\u003egetMessage().\" data: \".json_encode($this-\u003eevent), '#stripe_webhook', true);\n\t\t\t}\n\t\t}\n\t\t\n\t\tif ($creditsDue \u003e 0) {\n\t\t\t$transaction = $transactionsDelegate-\u003eaddTransaction(intval($creditsDue), 'Credits top-up for plan', $this-\u003euserId);\n\t\t}\n\t\t\n\t}\n\t\n\tfunction subscription_updated() {\n\t\tif (@$this-\u003eprevious_attributes['status'] === 'active' and $this-\u003eobject['status'] === 'past_due') {\n\t\t\t$this-\u003esubscription_past_due();\n\t\t\t\n\t\t} else if (@$this-\u003eobject['status'] == 'active' and @$this-\u003eprevious_attributes['status'] and @$this-\u003eprevious_attributes['status'] != 'active') {\n\t\t\t$this-\u003esubscription_activated();\n\t\t} else if ($this-\u003eplanChanged()) {\n\t\t\t$this-\u003esubscription_plan_changed();\n\t\t}\n\t\t\n\t\t\n\t\t\n\t}\n\t\n\tfunction subscription_plan_changed() {\n\t\tnn_info(\"Subscription plan changed for user \".$this-\u003euser-\u003eval('username').\" data: \".json_encode($this-\u003eevent), '#stripe_webhook #planchange');\n\t\t$newPlan = $this-\u003esubscription_plan();\n\t\t$currentPlan = $this-\u003euser-\u003eval('plan');\n\t\t\n\t\t\n\t\t$transactionsDelegate = \\Dataface_Table::loadTable('user_credit_transactions')-\u003egetDelegate();\n\t\t\n\n\t\t$credits = df_get_record('user_credits', ['userid' =\u003e '=' . $this-\u003euserId]);\n\t\tif (!$credits) {\n\t\t\tnn_error(\"Failed to load user_credits for user \".$this-\u003euserId, \"#stripe_webhook\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t// TODO setup user plan\n\t\t\n\t\t\t\n\t\t$planCredits = intval($newPlan-\u003eval('credits'));\n\t\t$currentCredits = intval($credits-\u003eval('credits'));\n\t\t$creditsDue = 0;\n\t\t$toDeduct = 0;\n\t\tif ($currentCredits \u003c $planCredits) {\n\t\t\t$creditsDue = $planCredits - $currentCredits;\n\t\t} else if ($currentCredits \u003e $planCredits) {\n\t\t\t$toDeduct = $planCredits - $currentCredits;\n\t\t}\n\t\t\n\t\t$updated = false;\n\t\tif ($credits-\u003eval('plan_id') != $newPlan-\u003eval('plan_id')) {\n\t\t\t$credits-\u003esetValue('plan_id',$newPlan-\u003eval('plan_id'));\n\t\t\t$updated = true;\n\t\t\t\n\t\t}\n\t\t\n\t\t\n\t\tif (@$object['current_period_end']) {\n\t\t\t$credits-\u003esetValue('plan_expiry_date', date('Y-m-d', intval($object['current_period_end'])));\n\t\t\t$updated = true;\n\t\t}\n\t\t\n\t\tif ($updated) {\n\t\t\t$res = $credits-\u003esave();\n\t\t\tif (\\PEAR::isError($res)) {\n\t\t\t\tnn_error(\"Failed to save user credits update upon subscription created.  \". $res-\u003egetMessage().\" data: \".json_encode($this-\u003eevent), '#stripe_webhook', true);\n\t\t\t}\n\t\t}\n\t\t\n\t\tif ($creditsDue \u003e 0) {\n\t\t\t$transaction = $transactionsDelegate-\u003eaddTransaction(intval($creditsDue), 'Credits top-up for plan', $this-\u003euserId);\n\t\t} else if ($toDeduct \u003c 0) {\n\t\t\t$transaction = $transactionsDelegate-\u003eaddTransaction(intval($toDeduct), 'Downgraded plan', $this-\u003euserId);\n\t\t}\n\t\t\n\t\t\n\t\t\n\t}\n\t\n\tfunction subscription_past_due() {\n\t\t// Account is past due.\n\t\t// We should send an email to the customer\n\t\tnn_info(\"Invoice past due for user \" . $this-\u003euser-\u003eval('username').\" data: \".json_encode($this-\u003eevent), '#stripe_webhook #pastdue');\n\t\n\t\t// TODO send email to user announcing past due.\n\t\n\t\treturn;\n\t}\n\t\n\tfunction subscription_activated() {\n\t\tnn_info(\"Subscription has become 'active' for user \".$this-\u003euser-\u003eval('username').' data: '.json_encode($this-\u003eevent), '#stripe_webhook #subscription.active');\n\t\n\t\t// TODO Update the user plan to the active subscription\n\t\n\t\treturn;\n\t}\n\t\n\tfunction subscription_deleted() {\n\t\t\n\t\tnn_info(\"Customer subscription deleted for user \".$this-\u003euser-\u003eval('username').' data: '.json_encode($this-\u003eevent), '#stripe_webhook #subscription.deleted');\n\t\n\t\t// TODO change plan back to basic\n\t\t$credits = df_get_record('user_credits', ['userid' =\u003e '=' . $this-\u003euserId]);\n\t\tif (!$credits) {\n\t\t\tnn_error(\"Failed to load user_credits for user \".$this-\u003euserId, \"#stripe_webhook\");\n\t\t\treturn;\n\t\t}\n\t\t\n\t\t$settings = nn_global_settings();\n   \t\n\t    $plan = $settings-\u003eval('default_plan');\n\t    if (!$plan) {\n\t        nn_error(\"Failed to load default plan while canceling subscription for user \".$this-\u003euser-\u003eval('username'), '#stripe_webhook #subscription.deleted');\n\t\t\treturn;\n\t    }\n\t\t\n\t\t$credits-\u003esetValue('plan_id', $plan-\u003eval('plan_id'));\n\t\t$credits-\u003esave();\n\t\t$planCredits = intval($plan-\u003eval('credits'));\n\t\t\n\t\t$userCredits = intval($credits-\u003eval('credits'));\n\t\t$toDeduct = $planCredits - $userCredits;\n\t\tif ($toDeduct \u003c 0) {\n\t\t\t$transactionsDelegate = \\Dataface_Table::loadTable('user_credit_transactions')-\u003egetDelegate();\n\t\t\t$transaction = $transactionsDelegate-\u003eaddTransaction(intval($toDeduct), 'Canceled subscription', $this-\u003euserId);\n\t\t}\n\t\n\t\treturn;\n\t\t\n\t}\n\t\n\tfunction subscription_trial_will_end() {\n\t\t//https://stripe.com/docs/api/events/types#event_types-customer.subscription.trial_will_end\n\t\t// Sent 3 days before trial period ends\n\t\t\n\t\t// TODO:  What do we do here?  Send the user an email to encourage them to stay?\n\t}\n\t\n\t/**\n\t * @return The list of line items in subscription.\n\t */\n\tfunction items() {\n\t\tif (@$this-\u003eobject['items'] and @$this-\u003eobject['items']['data']) {\n\t\t\treturn $this-\u003eobject['items']['data'];\n\t\t}\n\t\treturn null;\n\t}\n\t\n\t/**\n\t * The line item in the subscription at the given index.\n\t */\n\tfunction item($index) {\n\t\t$items = $this-\u003eitems();\n\t\tif ($items and count($items) \u003e $index) {\n\t\t\treturn $items[$index];\n\t\t}\n\t\treturn null;\n\t}\n\t\n\t\n\t/**\n\t * @return The Stripe Plan object array\n\t */\n\tfunction plan($index) {\n\t\t$item = $this-\u003eitem($index);\n\t\tif ($item and @$item['plan']) {\n\t\t\treturn $item['plan'];\n\t\t}\n\t\treturn null;\n\t\t\t\n\t}\n\t\n\t\n\tfunction planChanged() {\n\t\t$newPlan = $this-\u003esubscription_plan();\n\t\t$currentPlan = $this-\u003euser-\u003eval('plan');\n\t\tif ($newPlan and !$currentPlan or $currentPlan and !$newPlan) {\n\t\t\treturn true;\n\t\t}\n\t\tif ($newPlan and $newPlan-\u003eval('plan_id') != $currentPlan-\u003eval('plan_id')) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\t\n\t/**\n\t * @return Dataface_Record The subscription_plan record\n\t */\n\tfunction subscription_plan() {\n\t\tif (!isset($this-\u003esubscription_plan)) {\n\t\t\t$plan = $this-\u003eplan(0);\n\t\t\tif (!$plan) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t\n\t\t\t$joinRecord = df_get_record('nn_stripe_plans', ['stripe_plan_id' =\u003e '=' . $plan['product']]);\n\t\t\tif (!$joinRecord) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t$this-\u003esubscription_plan = df_get_record('subscription_plans', ['plan_id' =\u003e '=' . $joinRecord-\u003eval('nn_plan_id')]);\n\t\t\t\n\t\t}\n\t\treturn $this-\u003esubscription_plan;\n\t}\n\t\n}\n\n?\u003e\n----\n\nThe above example is quite verbose and it refers to some methods that are specific to the application, but it should give you a good idea of how to extract information from the webhook events in order to synchronize your application's state.\n\n== Typical User Workflow\n\n=== New Users\n\nOnce you have finished setting up your Stripe products and module, the typical user workflow is as follows:\n\n1. Add links to the `stripe_plans` action to display the list of subscription plans for your application. This page will look something like:\n+\nimage::images/Image-311220-070538.883.png[]\n+\nEach plan is displayed in its own box, with the different pricing options for the plan listed as buttons inside the plan's box.\n2. When the user clicks on a \"pricing\" button for one of the plans, they'll be taken to the Stripe payment page.\n+\nimage::images/Image-311220-082051.423.png[]\n+\n[TIP]\n====\nWhen in test mode you can use one of the test credit card numbers that Stripe provides https://stripe.com/docs/testing[here].\n====\n3. When payment is successful, the user is redirected back to your application.  The application will display the action specified in the \"success_action\" directive of the \"stripe\" (or \"stripe__test\") section of your conf.ini file.\n+\n[WARNING]\n====\nIt may be tempting to use this \"success_action\" to perform housekeeping duties in response to user subscriptions but this is not a good idea.  You should process customer subscriptions inside the webhook as this will ensure that you are informed of actions the user takes outside of your app.  E.g. If they change their subscription directly with Stripe, you'll want your application to handle this.\n====\n\n=== Existing Users\n\nIf the user already has a subscription to one of your plans, then the `stripe_plans` action will reflect this.\n\n.The stripe_plans action displayed for a user who is already subscribed to the \"Jabberwocky Basic Plan\" on a yearly subscription.  Notice the \"Subscribed\" ribbon on the \"Basic Plan\" box indicating that the use is subscribed.  Also notice that \"(Current Plan)\" is displayed on the button for the yearly plan to reflect the fact that the user is already on this plan.\nimage::images/Image-311220-082837.373.png[]\n\nIf the user presses on any of these options, they will be asked to confirm that they want to change their plan, and if they say \"yes\", then their plan will be automatically changed.\n\nimage::images/Image-311220-083205.268.png[]\n\nExisting users will also find a menu option called \"Manage Billing\" in their personal tools menu.\n\nimage::images/Image-311220-083230.734.png[]\n\nIf they click on this option, they will be sent to their Stripe billing account page where they can cancel their subscription, or change their plan.\n\nimage::images/Image-311220-083325.711.png[]\n\n\n\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshannah%2Fxataface-module-stripe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshannah%2Fxataface-module-stripe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshannah%2Fxataface-module-stripe/lists"}