{"id":346528,"date":"2026-08-20T05:30:18","date_gmt":"2026-08-20T05:30:18","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/gsheet-membership\/"},"modified":"2026-10-08T19:24:21","modified_gmt":"2026-10-08T19:24:21","slug":"membership-google-sheets","status":"publish","type":"plugin","link":"https:\/\/sr.wordpress.org\/plugins\/membership-google-sheets\/","author":23530366,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"4.10.16","stable_tag":"4.10.16","tested":"7.1.3","requires":"5.9","requires_php":"7.4","requires_plugins":null,"header_name":"GSheet Membership","header_author":"David Payton","header_description":"Membership access control and sales for WordPress, backed by a private Google Sheet. Restricts page access and login by membership status (Google Service Account \u2014 no public sharing required) and sells memberships via Stripe Checkout with a three-dimensional picker (Delivery \u00d7 Duration \u00d7 Postage). The GSheet Membership Pro add-on adds a membership level editor, PayPal checkout, gift purchases, admin refund\/retry tools, Group Email with an optional Mailchimp delivery, the Email-in mailbox, authenticated mailbox sending, PDF watermarks, and large-file links.","assets_banners_color":"f5f9f4","last_updated":"2026-10-08 19:24:21","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/wordpress.org\/plugins\/membership-google-sheets\/","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":707,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"4.10.16":{"tag":"4.10.16","author":"gsheetplugins","date":"2026-10-08 19:24:21","revision":3735402},"4.10.8":{"tag":"4.10.8","author":"gsheetplugins","date":"2026-10-04 05:33:56","revision":3726980},"4.5.8":{"tag":"4.5.8","author":"gsheetplugins","date":"2026-08-20 05:30:02","revision":3655836},"4.5.9":{"tag":"4.5.9","author":"gsheetplugins","date":"2026-08-21 08:53:32","revision":3658608},"4.6.1":{"tag":"4.6.1","author":"gsheetplugins","date":"2026-08-24 08:31:46","revision":3662921},"4.6.10":{"tag":"4.6.10","author":"gsheetplugins","date":"2026-09-06 17:49:22","revision":3683772},"4.6.11":{"tag":"4.6.11","author":"gsheetplugins","date":"2026-09-20 07:16:53","revision":3703955},"4.6.12":{"tag":"4.6.12","author":"gsheetplugins","date":"2026-09-21 22:32:20","revision":3706389},"4.6.6":{"tag":"4.6.6","author":"gsheetplugins","date":"2026-08-25 02:31:42","revision":3664475},"4.6.7":{"tag":"4.6.7","author":"gsheetplugins","date":"2026-08-27 20:12:01","revision":3669468},"4.6.8":{"tag":"4.6.8","author":"gsheetplugins","date":"2026-08-27 21:41:00","revision":3669544},"4.6.9":{"tag":"4.6.9","author":"gsheetplugins","date":"2026-08-29 06:33:19","revision":3671163},"4.8.1":{"tag":"4.8.1","author":"gsheetplugins","date":"2026-09-23 02:36:42","revision":3708346},"4.9.0":{"tag":"4.9.0","author":"gsheetplugins","date":"2026-09-25 22:59:42","revision":3713671}},"upgrade_notice":{"4.10.14":"<p>Checks fresh roster data before refusing a login for a missing email. Pro 1.15.10 and Chapters 1.18.8 remain unchanged.<\/p>","4.10.13":"<p>Adds a 60-second login-code resend delay with reassurance messages. Suppressed requests do not count toward the send limit. Chapters 1.18.8 and Pro 1.15.10 remain compatible.<\/p>","4.10.12":"<p>Fixes an incorrect &quot;selected membership record has changed&quot; refusal for unchanged chapter members. Keep Chapters 1.18.8 and Pro 1.15.10 installed. Previously failed paid purchases and existing duplicate rows still require reconciliation.<\/p>","4.10.11":"<p>Improves roster-read timeout recovery. Install Chapters 1.18.8 for same-payment chapter recovery and Pro 1.15.10 for the unsafe-retry safeguard. Existing duplicate rows require manual reconciliation.<\/p>","4.10.10":"<p>Adds safe existing-member destination support for Chapters 1.18.7; purchases complete independently of optional new-email approval.<\/p>","4.10.9":"<p>Refreshes feature and compatibility documentation; Free runtime behavior is unchanged.<\/p>","3.1.0":"<p>The plugin folder and text domain changed to membership-google-sheets. Deactivate and delete the old copy after installing this one; settings and data are preserved automatically.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3658640,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3658640,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3658609,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3658609,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["4.10.16","4.10.8","4.5.8","4.5.9","4.6.1","4.6.10","4.6.11","4.6.12","4.6.6","4.6.7","4.6.8","4.6.9","4.8.1","4.9.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3658609,"resolution":"1","location":"assets","locale":"","width":1099,"height":850},"screenshot-10.png":{"filename":"screenshot-10.png","revision":3658609,"resolution":"10","location":"assets","locale":"","width":1099,"height":850},"screenshot-11.png":{"filename":"screenshot-11.png","revision":3658609,"resolution":"11","location":"assets","locale":"","width":2122,"height":728},"screenshot-12.png":{"filename":"screenshot-12.png","revision":3658609,"resolution":"12","location":"assets","locale":"","width":1988,"height":1358},"screenshot-13.png":{"filename":"screenshot-13.png","revision":3658609,"resolution":"13","location":"assets","locale":"","width":1986,"height":1060},"screenshot-14.png":{"filename":"screenshot-14.png","revision":3658609,"resolution":"14","location":"assets","locale":"","width":1762,"height":1322},"screenshot-15.png":{"filename":"screenshot-15.png","revision":3658609,"resolution":"15","location":"assets","locale":"","width":1099,"height":850},"screenshot-16.jpg":{"filename":"screenshot-16.jpg","revision":3658609,"resolution":"16","location":"assets","locale":"","width":1063,"height":1220},"screenshot-17.png":{"filename":"screenshot-17.png","revision":3658609,"resolution":"17","location":"assets","locale":"","width":1302,"height":656},"screenshot-18.png":{"filename":"screenshot-18.png","revision":3658609,"resolution":"18","location":"assets","locale":"","width":1099,"height":850},"screenshot-19.png":{"filename":"screenshot-19.png","revision":3658609,"resolution":"19","location":"assets","locale":"","width":1099,"height":850},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3658609,"resolution":"2","location":"assets","locale":"","width":1099,"height":850},"screenshot-20.png":{"filename":"screenshot-20.png","revision":3658609,"resolution":"20","location":"assets","locale":"","width":1099,"height":380},"screenshot-21.png":{"filename":"screenshot-21.png","revision":3658609,"resolution":"21","location":"assets","locale":"","width":1099,"height":850},"screenshot-22.png":{"filename":"screenshot-22.png","revision":3658609,"resolution":"22","location":"assets","locale":"","width":1099,"height":850},"screenshot-23.png":{"filename":"screenshot-23.png","revision":3658609,"resolution":"23","location":"assets","locale":"","width":1099,"height":643},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3658609,"resolution":"3","location":"assets","locale":"","width":2542,"height":658},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3713670,"resolution":"4","location":"assets","locale":"","width":1614,"height":1516},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3658609,"resolution":"5","location":"assets","locale":"","width":1099,"height":850},"screenshot-6.png":{"filename":"screenshot-6.png","revision":3658609,"resolution":"6","location":"assets","locale":"","width":1099,"height":850},"screenshot-7.png":{"filename":"screenshot-7.png","revision":3658609,"resolution":"7","location":"assets","locale":"","width":1626,"height":978},"screenshot-8.png":{"filename":"screenshot-8.png","revision":3658609,"resolution":"8","location":"assets","locale":"","width":1099,"height":850},"screenshot-9.png":{"filename":"screenshot-9.png","revision":3658609,"resolution":"9","location":"assets","locale":"","width":1099,"height":385}},"screenshots":{"1":"Site Mode and Google Service Account \u2014 connect a private Google Sheet as the membership roster.","2":"Main Roster \u2014 the organisation's spreadsheet ID and column mapping (email, membership code, name, cache duration).","3":"A private Google Sheet roster \u2014 email and membership-code columns support access checks; additional columns can supply expiration dates, member details, and purchase information.","4":"Membership Expiration \u2014 Main Roster: choose Daily, Monthly, or Annual expiration updates, then set the warning window, mode-specific grace period, and expired-access message. Chapter sites can set separate local-roster rules.","5":"Extra User-Info Fields \u2014 map additional sheet columns (address, phone, region) for members to view and verify on the front end.","6":"The Membership Access panel in the page editor sidebar \u2014 set the membership codes required to view this page.","7":"The login gate turning away a visitor whose email is not on the roster.","8":"Login Flow \u2014 pick the page that hosts your custom sign-in form.","9":"Front-end Appearance \u2014 force the login form and status panel to keep their original colors and resize their text.","10":"Access Control Behaviour \u2014 exact vs. letters-mode code matching, and the message shown when access is denied.","11":"Signing in \u2014 a member enters their email to get started.","12":"First-time sign-in \u2014 a 6-digit verification code is emailed and entered here.","13":"After the code, a first-time member is prompted to create a password.","14":"Membership status confirmed after sign-in \u2014 shown here right after a purchase, but the same confirmation appears for any member signing in.","15":"Sales settings \u2014 the Pricing Matrix with Delivery, Duration and Postage pricing; <code>0<\/code> offers a free membership and a blank cell is unavailable.","16":"The membership purchase picker a buyer sees, with live price updates.","17":"The confirmation email a buyer receives after purchase.","18":"The Membership Code Matrix \u2014 the stored code for every level and delivery-method combination.","19":"Payments with Pro active \u2014 Stripe Connect account details and the optional PayPal gateway; free-only sites use Stripe Connect without PayPal.","20":"Recent Purchases \u2014 purchase outcomes and available actions; sheet-write retry needs Pro, while refundable Stripe Connect sales can be refunded on free sites.","21":"General Settings \u2014 currency, thank-you page, expiration-column override and the new-member bonus cutoff used in Annual mode.","22":"Membership Level Names \u2014 map raw membership codes to the human-readable names members see.","23":"Email Verification (OTP) \u2014 code expiry and sender details for the verification email."}},"plugin_section":[],"plugin_tags":[8874,215942,25296,1932,5349],"plugin_category":[45,58],"plugin_contributors":[276608],"plugin_business_model":[],"class_list":["post-346528","plugin","type-plugin","status-publish","hentry","plugin_tags-association","plugin_tags-branches","plugin_tags-google-sheets","plugin_tags-membership","plugin_tags-stripe","plugin_category-ecommerce","plugin_category-user-management","plugin_contributors-gsheetplugins","plugin_committers-gsheetplugins"],"banners":{"banner":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/banner-772x250.png?rev=3658609","banner_2x":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/banner-1544x500.png?rev=3658609","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/icon-128x128.png?rev=3658640","icon_2x":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/icon-256x256.png?rev=3658640","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-1.png?rev=3658609","caption":"Site Mode and Google Service Account \u2014 connect a private Google Sheet as the membership roster."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-2.png?rev=3658609","caption":"Main Roster \u2014 the organisation's spreadsheet ID and column mapping (email, membership code, name, cache duration)."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-3.png?rev=3658609","caption":"A private Google Sheet roster \u2014 email and membership-code columns support access checks; additional columns can supply expiration dates, member details, and purchase information."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-4.png?rev=3713670","caption":"Membership Expiration \u2014 Main Roster: choose Daily, Monthly, or Annual expiration updates, then set the warning window, mode-specific grace period, and expired-access message. Chapter sites can set separate local-roster rules."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-5.png?rev=3658609","caption":"Extra User-Info Fields \u2014 map additional sheet columns (address, phone, region) for members to view and verify on the front end."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-6.png?rev=3658609","caption":"The Membership Access panel in the page editor sidebar \u2014 set the membership codes required to view this page."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-7.png?rev=3658609","caption":"The login gate turning away a visitor whose email is not on the roster."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-8.png?rev=3658609","caption":"Login Flow \u2014 pick the page that hosts your custom sign-in form."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-9.png?rev=3658609","caption":"Front-end Appearance \u2014 force the login form and status panel to keep their original colors and resize their text."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-10.png?rev=3658609","caption":"Access Control Behaviour \u2014 exact vs. letters-mode code matching, and the message shown when access is denied."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-11.png?rev=3658609","caption":"Signing in \u2014 a member enters their email to get started."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-12.png?rev=3658609","caption":"First-time sign-in \u2014 a 6-digit verification code is emailed and entered here."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-13.png?rev=3658609","caption":"After the code, a first-time member is prompted to create a password."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-14.png?rev=3658609","caption":"Membership status confirmed after sign-in \u2014 shown here right after a purchase, but the same confirmation appears for any member signing in."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-15.png?rev=3658609","caption":"Sales settings \u2014 the Pricing Matrix with Delivery, Duration and Postage pricing; <code>0<\/code> offers a free membership and a blank cell is unavailable."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-16.jpg?rev=3658609","caption":"The membership purchase picker a buyer sees, with live price updates."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-17.png?rev=3658609","caption":"The confirmation email a buyer receives after purchase."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-18.png?rev=3658609","caption":"The Membership Code Matrix \u2014 the stored code for every level and delivery-method combination."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-19.png?rev=3658609","caption":"Payments with Pro active \u2014 Stripe Connect account details and the optional PayPal gateway; free-only sites use Stripe Connect without PayPal."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-20.png?rev=3658609","caption":"Recent Purchases \u2014 purchase outcomes and available actions; sheet-write retry needs Pro, while refundable Stripe Connect sales can be refunded on free sites."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-21.png?rev=3658609","caption":"General Settings \u2014 currency, thank-you page, expiration-column override and the new-member bonus cutoff used in Annual mode."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-22.png?rev=3658609","caption":"Membership Level Names \u2014 map raw membership codes to the human-readable names members see."},{"src":"https:\/\/ps.w.org\/membership-google-sheets\/assets\/screenshot-23.png?rev=3658609","caption":"Email Verification (OTP) \u2014 code expiry and sender details for the verification email."}],"raw_content":"<!--section=description-->\n<h4>Access \u2014 the login and page gate<\/h4>\n\n<p><strong>GSheet Membership<\/strong> lets you use a <strong>private<\/strong> Google Sheet as your membership database.<\/p>\n\n<ul>\n<li><strong>Truly private<\/strong> \u2014 the sheet is never shared publicly. Access is granted exclusively to a Google Service Account that you control.<\/li>\n<li><strong>Login gate<\/strong> \u2014 roster members sign in through the member login page. Administrator dashboard authentication stays on WordPress's native login path and does not require the administrator's email on a membership roster; member logins return to front-end content rather than the dashboard.<\/li>\n<li><strong>Membership codes<\/strong> \u2014 each row contains an email address and a membership code (e.g. GOLD, SILVER, PLATINUM).<\/li>\n<li><strong>Per-page restrictions<\/strong> \u2014 set required membership code(s) on any page or post via a sidebar panel.<\/li>\n<li><strong>Flexible access-denied handling<\/strong> \u2014 redirect to a custom URL or show a configurable inline message.<\/li>\n<li><strong>Encrypted roster cache<\/strong> \u2014 the entire fetched roster is encrypted as one authenticated payload before it is cached in a non-autoloaded WordPress option. On a cache hit, the whole payload is decrypted for the existing lookups; it is not encrypted or decrypted one member at a time. The OAuth2 access token is cached separately.<\/li>\n<li><strong>Site Mode<\/strong> \u2014 every site runs as Main (a single roster) or Chapter (its own local roster, alongside or instead of the main one) \u2014 Chapter mode is enabled by the separate Chapters add-on.<\/li>\n<li><strong>Expiration handling<\/strong> \u2014 warn members as their membership approaches its end, grant a configurable grace period after it lapses, and show a custom message once access is revoked.<\/li>\n<li><strong>Extra user-info fields<\/strong> \u2014 map any additional sheet columns (address, phone, region, or anything else you track) so members can view and verify them on the front end via <code>[gsma_user_info]<\/code>.<\/li>\n<li><strong>Custom login page<\/strong> \u2014 host the sign-in form on any WordPress page you choose with the <code>[gsma_login_form]<\/code> shortcode, instead of WordPress's default login screen.<\/li>\n<li><strong>Front-end appearance controls<\/strong> \u2014 force the login form and status panel to keep their original colors if your theme recolors them, and scale their text size independently of your theme.<\/li>\n<li><strong>Two code-matching modes<\/strong> \u2014 require an exact membership code, or \"letters mode,\" where a member's code must simply contain every letter in the required set (e.g. a required code of \"AB\" matches any member code containing both A and B).<\/li>\n<\/ul>\n\n<h4>How it works<\/h4>\n\n<p>The plugin authenticates using a <strong>Google Service Account<\/strong> \u2014 a special non-human Google account your server uses to read and update the sheet. You share the spreadsheet with it; no one else can access the sheet. With OpenSSL available, the saved service-account JSON is encrypted at rest; the roster cache is only written when it can be encrypted. The two use separate keys derived from this WordPress installation's security salts. They are not exposed in the browser or source code. The entire cached roster is decrypted in server memory when it is read; this does not encrypt old backups made before the upgrade.<\/p>\n\n<h4>Signing in<\/h4>\n\n<p>There's no separate account-creation step for members. A member already on the roster enters their email on your sign-in page: the first time, a 6-digit verification code is emailed to them and they're prompted to create a password after entering it; after that, they sign in with the password directly. See the FAQ entry below for the full flow.<\/p>\n\n<h4>Optional Pro add-on<\/h4>\n\n<p><strong>GSheet Membership Pro<\/strong> adds authenticated mailbox sending, Group Email (including Mailchimp campaign delivery), Email-in, File Delivery links and optional PDF watermarks, PayPal checkout, gift purchases, editable membership levels, and admin tools to retry failed sheet writes and refund supported purchases. Pro can also sync roster contacts to Mailchimp and EmailOctopus without sending a campaign; EmailOctopus is a sync option, not a campaign-delivery gateway. The Access and Sales admin screens show read-only previews of relevant Pro features and a direct upgrade button. After purchasing, install and activate the Pro add-on, then complete its license activation; the free plugin's existing settings stay in place.<\/p>\n\n<p>Compose's <strong>Sync Roster<\/strong> action syncs its filtered recipients to the connected providers and can supply a selected attachment through a personalized <code>GDLINK1<\/code> field. The separate Mailchimp and EmailOctopus settings-card buttons save the card and sync the full roster only to that provider.<\/p>\n\n<p>Pro and Chapters check the required Free features before loading their modules, so an add-on update arriving first through Freemius can keep working with compatible older Free versions. Unsupported combinations pause with an admin notice, preserve saved settings, and resume automatically after a compatible Free update. New optional functionality can require a newer Free version without pausing an otherwise compatible add-on.<\/p>\n\n<h3>Built for organizations with branches or chapters<\/h3>\n\n<p>Most membership plugins assume one site and one member list. Many real organizations don't work that way: a national society with local chapters, an alumni association with regional groups, a union with locals. GSheet Membership was designed for exactly that structure:<\/p>\n\n<ul>\n<li><strong>One national roster, many chapter rosters<\/strong> \u2014 the national organization keeps the master membership sheet; each chapter keeps its own local roster sheet. Both live in ordinary Google Sheets the organization owns.<\/li>\n<li><strong>Each chapter runs its own WordPress site<\/strong> \u2014 with its own pages, branding and admins, while membership checks stay consistent: a chapter site can honour the national roster, its local roster, or both.<\/li>\n<li><strong>Chapter-local sales and email<\/strong> \u2014 in chapter mode, membership sales write to the chapter's own roster, and group email (Pro add-on) goes to the local chapter list.<\/li>\n<li><strong>Conditional chapter pricing<\/strong> \u2014 when the Chapters add-on is configured, the buyer checks their main-organization membership before checkout. Current main members see chapter-only pricing; expired main members see a combined total based on the main site's published price plus local chapter dues.<\/li>\n<li><strong>Separate identity and dues checks<\/strong> \u2014 Chapters confirms main and chapter records independently and revalidates selected row tokens at submission. When Pro gifts are enabled, the recipient's main membership determines dues; the giver's signed-in identity is not reused as recipient proof. Combined purchase confirmations identify both memberships and the exact total while preserving the configured email template.<\/li>\n<li><strong>Your organization owns the data<\/strong> \u2014 roster sheets stay in your own Google account rather than a hosted membership database. Chapter mode requires the separately licensed Chapters add-on on participating chapter sites.<\/li>\n<\/ul>\n\n<p>Chapter mode is provided by the <strong>GSheet Membership \u2014 Chapters<\/strong> add-on; the free plugin covers everything a single-roster organization needs.<\/p>\n\n<h4>Public pricing data for chapter sites<\/h4>\n\n<p>The free plugin provides the unauthenticated, read-only <code>GET \/wp-json\/gsma-sales\/v1\/pricing<\/code> endpoint so a configured Chapters site can display the main organization's current combined-price options. The JSON response contains the main site's configured membership levels, pricing matrix, membership-code matrix, delivery and postage options, currency, and <code>access_match_mode<\/code> (<code>exact<\/code> or <code>letters<\/code>). It does not contain roster or buyer data. Responses advertise a five-minute public cache (<code>Cache-Control: public, max-age=300<\/code>); the chapter site validates and caches this pricing separately.<\/p>\n\n<p>On a chapter site, the Chapters add-on checks the buyer's email against the main roster. If no email match is found, the buyer can check a box to self-report main-organization membership; that statement is not verified and must not be treated as proof of membership. A last-name lookup can help locate a possible roster record, but a buyer's selection there is also not authentication. Chapter administrators should review these cases using their normal membership procedures.<\/p>\n\n<p>Combined sales write the chapter membership to the chapter site's local roster and create a manual remittance worklist entry for the main-organization dues. No payment is transferred or remitted automatically. Chapter administrators must handle the main-organization payment separately and mark the worklist entry resolved after reconciliation. An optional per-sale email can notify a configured main-organization administrator; it is a notification only, not remittance or verification.<\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin connects to the following third-party services to provide its core functionality:<\/p>\n\n<p><strong>Google Sheets API and Google OAuth2<\/strong> (developers.google.com)\nUsed to read and write membership data to your private Google Sheet, and to authenticate as your Google Service Account. When a page's access is checked, when a purchase is recorded, or when you save settings and test the connection, the plugin sends: your Service Account's signed authentication request to https:\/\/oauth2.googleapis.com\/token, and membership-related row data (emails, membership codes, and \u2014 depending on which features you use \u2014 names, addresses, and purchase details) to https:\/\/sheets.googleapis.com. This only occurs because you have configured a Google Service Account and Spreadsheet ID; no data is sent to Google until you do so.\nGoogle APIs Terms of Service: https:\/\/developers.google.com\/terms\nGoogle API Services User Data Policy: https:\/\/developers.google.com\/terms\/api-services-user-data-policy\nGoogle Privacy Policy: https:\/\/policies.google.com\/privacy<\/p>\n\n<p><strong>Stripe<\/strong> (stripe.com)\nUsed to process membership purchases via Stripe Checkout. Card details are entered on Stripe's hosted checkout page, not this site. With the free plugin's Stripe Connect flow, the hosted GSheet Connect service creates the Checkout Session and relays Stripe's confirmation to the site; Pro can instead use your own Stripe API keys and a direct Stripe webhook. Stripe is contacted only when the gateway is enabled and connected or configured.\nStripe Terms of Service: https:\/\/stripe.com\/legal\/ssa\nStripe Privacy Policy: https:\/\/stripe.com\/privacy<\/p>\n\n<p><strong>GSheet Connect<\/strong> (access-manager-pro.replit.app) \u2014 Stripe Connect mode\nThe plugin's hosted payment service is used for Stripe Connect sales, including on a Pro site until its own Stripe keys are saved for the active mode. It handles \"Connect with Stripe\" onboarding, creates the Stripe Checkout Session for each purchase (applying the disclosed platform commission), processes refunds you issue from the dashboard, and relays Stripe's payment confirmations back to your site. The plugin sends: your connected Stripe account ID, the purchase amount, currency and description, the buyer's email address, and your site's webhook address to https:\/\/access-manager-pro.replit.app. The service does not keep a membership roster; payment processing itself is performed by Stripe. A site using its own Stripe API keys instead of Connect does not use this service for checkout.\nService overview: https:\/\/access-manager-pro.replit.app<\/p>\n\n<p><strong>PayPal<\/strong> (paypal.com) \u2014 requires the GSheet Membership Pro add-on\nUsed to process membership purchases via PayPal Checkout as an alternative to Stripe. Only active if you install the Pro add-on and configure your PayPal credentials; the free plugin does not send any data to PayPal on its own.\nPayPal User Agreement: https:\/\/www.paypal.com\/us\/legalhub\/paypal\/useragreement-full\nPayPal Privacy Statement: https:\/\/www.paypal.com\/us\/legalhub\/paypal\/privacy-full<\/p>\n\n<p><strong>Mailchimp<\/strong> (mailchimp.com) \u2014 requires the GSheet Membership Pro add-on\nOptional campaign delivery and roster-sync method in Pro. With an API key and selected audience, classic sends sync the current membership filter; segments-mode sends sync the full roster and target a segment. The optional Sync Roster action updates the audience without sending a campaign. Pro sends member email addresses, names, and optional personalized link fields from the Google Sheet to https:\/\/.api.mailchimp.com to add, update, or archive audience contacts as appropriate; it does not override contacts who unsubscribed or were cleaned. Only active when Pro is installed and Mailchimp is configured; the free plugin sends no data to Mailchimp on its own.\nMailchimp Standard Terms of Use: https:\/\/mailchimp.com\/legal\/terms\/\nIntuit Mailchimp Privacy Statement: https:\/\/www.intuit.com\/privacy\/statement\/<\/p>\n\n<p><strong>EmailOctopus<\/strong> (emailoctopus.com) \u2014 requires the GSheet Membership Pro add-on\nAn optional roster-sync destination, not a Group Email campaign-sending gateway. If configured, Pro sends member email addresses, names, and optionally a personalized attachment-link field to https:\/\/api.emailoctopus.com to update the selected list without sending a campaign. The provider-card Sync Roster action syncs the full local recipient roster; Compose's Sync Roster action uses the selected membership filter. The free plugin sends no data to EmailOctopus on its own.\nEmailOctopus Terms of Service: https:\/\/emailoctopus.com\/legal\/terms\nEmailOctopus Privacy Policy: https:\/\/emailoctopus.com\/legal\/privacy<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>membership-google-sheets<\/code> folder to <code>\/wp-content\/plugins\/<\/code> (or upload the ZIP under <strong>Plugins \u2192 Add New \u2192 Upload Plugin<\/strong>).<\/li>\n<li>Activate the plugin through the Plugins screen.<\/li>\n<li>Follow the Google setup steps below.<\/li>\n<li>Go to <strong>Membership Access<\/strong> (top-level menu in the WordPress admin sidebar) and configure the plugin.<\/li>\n<li>Click <strong>Save &amp; Test Main Roster<\/strong> to verify the service account can read your sheet.<\/li>\n<\/ol>\n\n<h4>Google setup (~5 minutes)<\/h4>\n\n<ol>\n<li>Go to https:\/\/console.cloud.google.com\/ and create or select a project.<\/li>\n<li>Enable the <strong>Google Sheets API<\/strong> (APIs &amp; Services \u2192 Library \u2192 search \"Sheets\").<\/li>\n<li>Create a <strong>Service Account<\/strong> (IAM &amp; Admin \u2192 Service Accounts \u2192 Create).<\/li>\n<li>Generate a <strong>JSON key<\/strong> for the service account (Keys tab \u2192 Add Key \u2192 JSON). Download the file.<\/li>\n<li>Open your Google Sheet \u2192 <strong>Share<\/strong> \u2192 add the service account email (looks like <code>name@project.iam.gserviceaccount.com<\/code>) as an <strong>Editor<\/strong> (required for the Sales feature and the Pro add-on's Group Email; Viewer is enough if you only use page protection). The sheet stays private to everyone else.<\/li>\n<li>In <strong>Membership Access<\/strong> (top-level menu in the WordPress admin sidebar), paste the full contents of the JSON key file and enter your Spreadsheet ID.<\/li>\n<\/ol>\n\n<p>When you click <strong>Save &amp; Test Main Roster<\/strong>, the result identifies the exact service-account email in use and checks authentication, spreadsheet access, available tabs, the selected tab, row reading, and required column mappings in order. If a check fails, the result names the Main roster and gives a specific correction without displaying the private key, access token, raw JSON, or Google response body.<\/p>\n\n<p>For the Sheet (Tab) Name, leave the field blank to use the spreadsheet's first tab automatically. If an explicit tab name is missing, the connection result lists the available tab names and tells you to choose one or clear the field.<\/p>\n\n<h4>Restricting pages<\/h4>\n\n<ol>\n<li>Edit any page or post in WordPress.<\/li>\n<li>Find the <strong>Membership Access<\/strong> panel in the editor sidebar.<\/li>\n<li>Enter the required membership code(s), comma-separated (e.g. <code>GOLD, PLATINUM<\/code>).<\/li>\n<li>Save the page.<\/li>\n<\/ol>\n\n<p>Codes are compared according to the Access Control Match Mode setting (exact match by default, or \"letters mode\" \u2014 see Configuration reference below). Leave the field blank for unrestricted access.<\/p>\n\n<h4>Configuration reference<\/h4>\n\n\n\n\n  Setting\n  Description\n\n\n\n\n  Service Account JSON\n  Full contents of the JSON key file downloaded from Google Cloud Console\n\n\n  Spreadsheet ID\n  The ID from the spreadsheet URL (between <code>\/d\/<\/code> and <code>\/edit<\/code>)\n\n\n  Sheet (Tab) Name\n  Optional. Leave blank to use the spreadsheet's first tab automatically.\n\n\n  Email Column\n  Column letter containing email addresses (default: A)\n\n\n  Membership Code Column\n  Column letter containing membership codes (default: B)\n\n\n  Header Row Number\n  Row number of the header; data starts on the next row (default: 1)\n\n\n  Cache Duration (seconds)\n  How long to keep the encrypted whole-roster cache in WordPress options (default: 300). Set 0 to disable roster caching.\n\n\n  Expiration Mode\n  Daily (individual dates), Monthly, or Annual (covered-to year).\n\n\n  Warning Window (days)\n  Days before expiration to start showing members a renewal warning (default: 14)\n\n\n  Grace Period\n  Continued access after expiration: days in Daily mode; calendar months in Monthly and Annual modes.\n\n\n  Expired-Access Message\n  Message shown once a membership has expired and the grace period has passed\n\n\n  Login Redirect URL\n  Where to send unauthenticated visitors trying to access a restricted page\n\n\n  Access-Denied Redirect URL\n  Where to redirect logged-in users without the required membership\n\n\n  Access-Denied Message\n  Message to display instead of redirecting (if no redirect URL is set)\n\n\n  Access Control Match Mode\n  Exact match, or \"letters mode,\" where the member's code must contain every letter in the required set\n\n\n  Custom Login Page\n  The WordPress page containing <code>[gsma_login_form]<\/code>; leave on Auto-detect to let the plugin find it, or fall back to WordPress's default login page\n\n<!--section=faq-->\n<dl>\n<dt id=\"how%20do%20i%20close%20a%20failed%20sandbox%20test%20purchase%3F\"><h3>How do I close a failed sandbox test purchase?<\/h3><\/dt>\n<dd><p>In Recent Purchases, choose Reconcile and then \"Abandon a sandbox test purchase\". Verify the original transaction in the payment provider's test or sandbox environment, confirm that no real funds were charged, and confirm that any test roster changes have been removed or reversed without deleting a legitimate membership. Record a short note and mark it reconciled. This permanently prevents another fulfillment attempt for that payment without claiming a refund or a completed membership, and does not approve an email addition. Known live transactions cannot use this shortcut: a real-money purchase made for testing still needs a completed full refund or the correct fulfilled membership. Original errors and email-status notes remain available in expandable details instead of an active failure warning.<\/p><\/dd>\n<dt id=\"does%20the%20google%20sheet%20need%20to%20be%20shared%20publicly%3F\"><h3>Does the Google Sheet need to be shared publicly?<\/h3><\/dt>\n<dd><p>No \u2014 never. You only share it with the service account email address. It remains completely private to all other users.<\/p><\/dd>\n<dt id=\"what%20php%20extensions%20are%20required%3F\"><h3>What PHP extensions are required?<\/h3><\/dt>\n<dd><p>The <strong>OpenSSL<\/strong> extension is needed to sign the JWT for authentication. It is enabled by default on virtually all shared and managed WordPress hosts (including WP Engine, Kinsta, SiteGround, Flywheel, and others).<\/p><\/dd>\n<dt id=\"are%20administrators%20ever%20blocked%3F\"><h3>Are administrators ever blocked?<\/h3><\/dt>\n<dd><p>No. Users with the <code>manage_options<\/code> capability always bypass all membership checks.<\/p><\/dd>\n<dt id=\"where%20is%20the%20service%20account%20json%20stored%3F\"><h3>Where is the Service Account JSON stored?<\/h3><\/dt>\n<dd><p>It is stored in your WordPress database (the <code>wp_options<\/code> table). With OpenSSL available, the plugin encrypts it using this installation's security salts; if encryption is unavailable, the credential-saving routine can retain plaintext rather than replace a working credential with unusable ciphertext. Check the encryption status on the Access settings screen. It is never sent to the browser or written to a file by the plugin. Keep the salts and database backups secure.<\/p><\/dd>\n<dt id=\"how%20quickly%20do%20membership%20changes%20take%20effect%3F\"><h3>How quickly do membership changes take effect?<\/h3><\/dt>\n<dd><p>After the encrypted whole-roster cache expires (default 5 minutes \/ 300 seconds), the next read fetches fresh data and writes a newly encrypted payload. Reduce the cache duration in settings or click <strong>Clear Cache<\/strong> to force the next read to refresh.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20the%20sheets%20api%20is%20unreachable%3F\"><h3>What happens if the Sheets API is unreachable?<\/h3><\/dt>\n<dd><p>The plugin logs an error and denies access as a safe default. Use <strong>Save &amp; Test Main Roster<\/strong> in settings to verify connectivity.<\/p><\/dd>\n<dt id=\"signing%20in%3A%20verification%20code%2C%20then%20password\"><h3>Signing in: verification code, then password<\/h3><\/dt>\n<dd><p>This works for <strong>any member already on the roster<\/strong>, whether they got there through the plugin's own purchase pathway or not \u2014 a name typed in manually, imported from a spreadsheet, added after a mailed-in check, however your roster is maintained. The sign-in flow doesn't care how someone became a member, only that their email is on the sheet. It's part of the free plugin.<\/p>\n\n<p>There's no separate account-creation step. A member enters their email, and what happens next depends on whether they've signed in before:<\/p>\n\n<ul>\n<li><strong>First time signing in?<\/strong> A 6-digit verification code is emailed to them. They enter the code and are prompted to create a password. From then on, that password signs them in directly.<\/li>\n<li><strong>Already have a password?<\/strong> The code step is skipped entirely \u2014 they go straight to a normal password sign-in.<\/li>\n<\/ul>\n\n<p>(Pro's <strong>Authenticated mailbox sending<\/strong>, below, improves deliverability of these verification codes \u2014 it doesn't add the sign-in flow itself.)<\/p><\/dd>\n<dt id=\"authenticated%20mailbox%20sending%20%28pro%20add-on%29\"><h3>Authenticated mailbox sending (Pro add-on)<\/h3><\/dt>\n<dd><p>With the <strong>GSheet Membership Pro<\/strong> add-on, you can connect a real mailbox (Google sign-in or an App Password) on the main settings page and route login verification codes, purchase confirmations and gift notices through it. A rejected verification code falls back to the site's normal mailer. Group Email can also use the authenticated mailbox for batched sending, or use Mailchimp for campaign delivery. Correctly configured mailbox authentication can improve deliverability; it does not guarantee that messages avoid spam filters.<\/p><\/dd>\n<dt id=\"group%20email%3A%20message%20your%20roster%20%28pro%20add-on%29\"><h3>Group Email: message your roster (Pro add-on)<\/h3><\/dt>\n<dd><p>With the <strong>GSheet Membership Pro<\/strong> add-on, the <strong>Group Email<\/strong> page in <strong>Membership Access<\/strong> lets you compose a message and send it to your roster \u2014 everyone, or a subset filtered by membership code. Delivery can use the site's batched mailer (including an authenticated mailbox) or a connected <strong>Mailchimp<\/strong> audience. Mailchimp can sync the filtered audience before a classic-mode send or target a segment of the synced full roster. Chapter sites send Group Email to their local chapter roster only, even if their access checks also include the main roster. Direct delivery and Mailchimp campaigns have different unsubscribe and reporting behavior; Pro includes send history and delivery reports.<\/p>\n\n<p>There are two ways to trigger a send: filling out the compose form yourself, or <strong>Email-in<\/strong> \u2014 forwarding a single email to a dedicated mailbox, covered next.<\/p><\/dd>\n<dt id=\"email-in%3A%20trigger%20a%20group%20email%20by%20forwarding%20a%20message%20%28pro%20add-on%29\"><h3>Email-in: trigger a group email by forwarding a message (Pro add-on)<\/h3><\/dt>\n<dd><p>Email-in is the second way to send a Group Email: instead of the compose form, you <strong>forward a message to a dedicated mailbox<\/strong> (e.g. <code>blast@example.org<\/code>). The plugin checks that mailbox on a schedule (IMAP); a message from an authorized sender that meets the subject-keyword rule is queued under the same audience rules as the compose form. The configured delivery method determines whether it goes through site mail or Mailchimp.<\/p><\/dd>\n<dt id=\"setting%20it%20up\"><h3>Setting it up<\/h3><\/dt>\n<dd><ol>\n<li>Create a dedicated mailbox for intake (a Gmail \/ Google Workspace account works well). Don't reuse a personal inbox \u2014 every unread message is examined.<\/li>\n<li>For Gmail \/ Google Workspace: enable IMAP (Gmail Settings \u2192 Forwarding and POP\/IMAP), turn on 2-Step Verification for the account, and create an <strong>App Password<\/strong> (Google Account \u2192 Security \u2192 App passwords). The regular account password will NOT work over IMAP.<\/li>\n<li>In <strong>Membership Access \u2192 Group Email<\/strong>, fill in the Email-in card: host <code>imap.gmail.com<\/code>, port <code>993<\/code>, the mailbox address as username, and the App Password.<\/li>\n<li>Add the email addresses allowed to trigger a send (one per line) and \u2014 strongly recommended \u2014 a <strong>required subject keyword<\/strong>.<\/li>\n<li>Enable polling, click <strong>Test connection<\/strong>, then send yourself a trial: forward a message from an allowlisted address (with the keyword in the subject) and click <strong>Check mailbox now<\/strong>.<\/li>\n<\/ol><\/dd>\n<dt id=\"why%20the%20subject%20keyword%20matters%20%28spoofing%29\"><h3>Why the subject keyword matters (spoofing)<\/h3><\/dt>\n<dd><p>The From: address of an email can be forged (spoofing) \u2014 an allowlist alone is not sufficient protection for something that can email your whole membership. The required subject keyword adds a second check; keep it private and choose a non-obvious phrase. The keyword is stripped from the subject before delivery. This does not replace mailbox security or prevent an attacker who learns the keyword from sending a forged message.<\/p><\/dd>\n<dt id=\"audience%20%26%20attachments\"><h3>Audience &amp; attachments<\/h3><\/dt>\n<dd><ul>\n<li>Embed a code filter in the subject to target membership levels: <code>Meeting notes [codes: GOLD,SILVER]<\/code>. The tag is removed before delivery. Without a tag, the membership code filter saved on the Group Email page is applied; if that filter is blank, all members receive the message. Works on both main and chapter sites.<\/li>\n<li><strong>Chapter sites<\/strong> send to the local chapter roster only \u2014 the code filter narrows within that roster, regardless of whether access checks also use the main roster.<\/li>\n<li>Email-in attachments are uploaded to the media library. With Pro's <strong>File Delivery<\/strong> link mode, every attachment becomes an expiring, recipient-bound link that opens without a login; PDFs may also be watermarked for the recipient. With link mode off, the first file is attached to site mail (or converted to a link if over 5 MB), while additional files use shared login-gated links. Mailchimp link mode supports up to five personalized file links per campaign; additional files are omitted with a notice.<\/li>\n<li>Accepted and permanently skipped authorized messages are marked read and recorded to avoid duplicate sends. Unauthorized or unrelated messages are ignored and left unread, with their IDs recorded so they are not repeatedly processed.<\/li>\n<\/ul><\/dd>\n<dt id=\"sales%20%E2%80%94%20sell%20memberships\"><h3>Sales \u2014 sell memberships<\/h3><\/dt>\n<dd><p>The Sales half of <strong>GSheet Membership<\/strong> puts a configurable buy-membership picker on any page and, on a successful payment, writes (or updates) the buyer's row in your private Google Sheet so they can immediately log in on your site.<\/p>\n\n<ul>\n<li><strong>Three-dimensional picker<\/strong> \u2014 buyers choose Delivery (Electronic \/ Print \/ Both) \u00d7 Duration (1-Year and Lifetime included by default; the Pro add-on adds custom levels and multi-year durations) \u00d7 Postage (Regular, First Class, Canada\/Mexico, Other International), and the price updates live as they change selections.<\/li>\n<li><strong>Discrete pricing matrix<\/strong> \u2014 you set the exact price for every saleable combination in the admin. Enter <code>0<\/code> for a free membership that skips the gateway; leave a cell blank to make that combination unavailable.<\/li>\n<li><strong>Stripe Checkout<\/strong> \u2014 the free plugin uses Connect with Stripe and hosted onboarding, with no API keys to paste; Pro can use your own Stripe keys instead. Card details stay on Stripe's hosted checkout page. Pro's own-key settings provide a Test\/Live switch; free Connect has no separate mode switch in WordPress.<\/li>\n<li><strong>PayPal Standard Checkout<\/strong> (requires the Pro add-on) \u2014 buyers can optionally pay via PayPal instead of (or in addition to) Stripe. Both gateways share the same sheet-upsert pipeline, purchase log, and retry UI.<\/li>\n<li><strong>Sheet upsert<\/strong> \u2014 on a verified webhook the plugin first tries to update the row that matches the buyer's email; if there is no row, it appends one. Renewals extend an existing future expiration date instead of overwriting it.<\/li>\n<li><strong>Reliable webhook pipeline<\/strong> \u2014 per-event and per-email atomic locks prevent duplicate writes from concurrent webhook redeliveries; a sheet-write failure returns HTTP 500 so Stripe retries, and the admin sees a notice. The Pro add-on adds an admin retry control for failed sheet writes.<\/li>\n<li><strong>Buyer confirmation email<\/strong> \u2014 configurable subject and body with placeholders (first name, selection, amount, code, expiration, login URL, etc.). Integrations can enrich purchase variables with <code>gsms_purchase_email_vars<\/code> and adjust the rendered confirmation body with <code>gsms_confirmation_body<\/code>.<\/li>\n<li><strong>Roster-aware checkout and contact collection<\/strong> \u2014 checkout distinguishes the signed-in purchaser's own verified email from another entered email or a gift recipient. Add-ons can collect contact fields independently of local sheet-write columns using the same field list for rendering and paid-checkout validation. With Chapters, this supports international State \/ Province \/ Region, Country, and Zone for main-organization reporting without local mappings. Electronic-delivery buyers can select the international-address checkbox when international fields are collected, even without a local ZIP column; administrator direct-apply still uses only local mappings.<\/li>\n<li><strong>Admin direct-apply<\/strong> \u2014 a separate <code>[gsms_admin_picker]<\/code> shortcode lets a site administrator apply a membership directly to the sheet <strong>without taking a payment<\/strong>, perfect for comp memberships, cheque \/ cash-in-hand orders, or back-filling an old purchase.<\/li>\n<li><strong>Membership Level Names<\/strong> \u2014 map the raw membership codes stored in your sheet to human-readable names, so a member who's denied access sees a name like \"Gold Member\" instead of the raw code. Pro adds a level editor; copying Pro levels to free-only storage requires an explicit administrator action.<\/li>\n<\/ul><\/dd>\n<dt id=\"requirements\"><h3>Requirements<\/h3><\/dt>\n<dd><ul>\n<li>PHP 7.4+ with the standard <code>openssl<\/code>, <code>hash<\/code>, and <code>mbstring<\/code> extensions (already enabled on every major WordPress host).<\/li>\n<li>WordPress 5.9+.<\/li>\n<li>The access features (above) configured with a working Service Account \u2014 the sales pipeline reuses the same sheet-writer.<\/li>\n<li>A Stripe account for live or test payments (a PayPal developer account too, if you use the Pro add-on's PayPal checkout).<\/li>\n<\/ul><\/dd>\n<dt id=\"selling%20setup\"><h3>Selling setup<\/h3><\/dt>\n<dd><ol>\n<li>Configure the Access settings first (see Installation above) and confirm the <strong>Save &amp; Test Main Roster<\/strong> button succeeds.<\/li>\n<li>Go to <strong>Membership Sales<\/strong> in the WordPress admin sidebar. Every sub-page shows a \"Setup\" notice at the top reminding you which shortcode to put on which page.<\/li>\n<li>Fill in the <strong>Pricing Matrix<\/strong>, <strong>Code Matrix<\/strong>, and <strong>Payments<\/strong> sub-pages (see Configuration below).<\/li>\n<li>Create a page containing only the shortcode <code>[gsma_membership_purchase]<\/code> \u2014 that is the buy page.<\/li>\n<li>Create (or pick) a thank-you page and put <code>[gsma_user_info]<\/code> on it (that shortcode shows the buyer's status with an inline login form when they're not yet signed in). Select that page on the <strong>Settings<\/strong> sub-page under <strong>Thank-you page<\/strong>.<\/li>\n<\/ol><\/dd>\n<dt id=\"shortcodes\"><h3>Shortcodes<\/h3><\/dt>\n<dd><ul>\n<li><code>[gsma_login_form]<\/code> \u2014 renders the sign-in form (email, then a first-time verification code or a returning password) on the page you choose as the Custom Login Page. Independent of the Sales feature \u2014 every site can use it.<\/li>\n<li><code>[gsma_membership_purchase]<\/code> \u2014 renders the full picker (Delivery \u00d7 Duration \u00d7 Postage) with live total and the <strong>Buy membership<\/strong> button. Place this on the page where you want to take payments.<\/li>\n<li><code>[gsms_admin_picker]<\/code> \u2014 admin-only variant of the picker that applies a membership directly to the sheet without any payment. Only site administrators (<code>manage_options<\/code>) see it; everyone else sees a polite notice.<\/li>\n<li><code>[gsma_user_info]<\/code> \u2014 place it on the thank-you page so buyers see their membership status (and an inline login form when they aren't logged in).<\/li>\n<\/ul><\/dd>\n<dt id=\"admin%20sub-pages\"><h3>Admin sub-pages<\/h3><\/dt>\n<dd><ul>\n<li><strong>Pricing Matrix<\/strong> \u2014 one row per Delivery, one column per Postage, repeated for each Duration. Type a price into a cell to make it saleable, including <code>0<\/code> for a free membership; leave it blank to make that combination unavailable. The <strong>Membership Levels<\/strong> block above the grid lets you choose which levels are currently offered for purchase.<\/li>\n<li><strong>Code Matrix<\/strong> \u2014 the membership code written into the buyer's sheet row for each combination. Every membership level gets its own row of codes. Levels beyond the built-in 1-Year and Lifetime, including multi-year durations, can be added with the Pro add-on; each new level gets its own Code Matrix row automatically. A warning is shown if any saleable cell is missing a code.<\/li>\n<li><strong>Payments<\/strong> \u2014 enable Stripe and use <strong>Connect with Stripe<\/strong> in the free plugin (no API keys or Stripe webhook secret to paste). With Pro, use your own Stripe keys instead and optionally enable PayPal, which has its own credentials and webhook setup.<\/li>\n<li><strong>General Settings<\/strong> (sidebar label <strong>Settings<\/strong>) \u2014 currency, thank-you page picker, optional override of the access settings' expiration column, annual new-member bonus cutoff, buyer confirmation email templates, and option labels.<\/li>\n<li><strong>Recent Purchases<\/strong> \u2014 the last 200 purchase events with gateway and sheet status. <strong>Reconcile<\/strong> guides administrators through the original-payment, intended-membership and correction checks, with a shorter path for a completed full refund and a missing-record review when an entry is no longer retained. It records the verified outcome without charging, refunding or editing the roster, prevents another renewal from that payment, and clears the warning only when no unresolved failures remain. Pro adds an inline <strong>Retry<\/strong> form for failed sheet writes and refunds for supported purchases; refundable Stripe Connect sales can also be refunded in the free plugin.<\/li>\n<\/ul><\/dd>\n<dt id=\"configuration%3A%20webhooks\"><h3>Configuration: webhooks<\/h3><\/dt>\n<dd><p><strong>Stripe Connect (free plugin):<\/strong> On the Payments page, click <strong>Connect with Stripe<\/strong> and complete Stripe's hosted onboarding. No Stripe API keys or direct webhook signing secret are entered in the free plugin; payment confirmations are relayed through the hosted Connect service.<\/p>\n\n<p><strong>Direct Stripe API keys (Pro add-on only):<\/strong> If you choose Pro's own-key mode instead of Connect, configure separate Test\/Live keys on the Payments page and register its direct webhook:<\/p>\n\n<ol>\n<li><strong>Developers \u2192 Webhooks \u2192 Add endpoint<\/strong>.<\/li>\n<li>Paste the <strong>Stripe Webhook URL<\/strong> shown on the Pro Payments page (ends in <code>\/wp-json\/gsma-sales\/v1\/stripe-webhook<\/code>).<\/li>\n<li>Subscribe to a single event: <code>checkout.session.completed<\/code>.<\/li>\n<li>Copy the resulting <strong>Signing secret<\/strong> (<code>whsec_...<\/code>) into Pro's matching webhook field for that mode.<\/li>\n<\/ol>\n\n<p><strong>PayPal<\/strong> (requires the Pro add-on):<\/p>\n\n<ol>\n<li>In the PayPal developer dashboard \u2192 <strong>My Apps &amp; Credentials \u2192 your app \u2192 Add webhook<\/strong>.<\/li>\n<li>Paste the <strong>PayPal Webhook URL<\/strong> shown on the plugin's Payments page (ends in <code>\/wp-json\/gsma-sales\/v1\/paypal-webhook<\/code>).<\/li>\n<li>Subscribe to <code>CHECKOUT.ORDER.APPROVED<\/code> and <code>PAYMENT.CAPTURE.COMPLETED<\/code>.<\/li>\n<li>Copy the resulting <strong>Webhook ID<\/strong> into the matching field on the plugin's Payments page.<\/li>\n<\/ol><\/dd>\n<dt id=\"how%20to%20test%20without%20real%20money\"><h3>How to test without real money<\/h3><\/dt>\n<dd><p>The picker can be exercised without a payment gateway. A price of <code>0<\/code> also completes a free membership without sending the buyer to Stripe or PayPal.<\/p><\/dd>\n<dt id=\"1.%20picker%20ui%20%2F%20pricing%20layout%20%28no%20gateway%20at%20all%29\"><h3>1. Picker UI \/ pricing layout (no gateway at all)<\/h3><\/dt>\n<dd><ul>\n<li>Fill in just the <strong>Pricing Matrix<\/strong> (real prices) and the <strong>Code Matrix<\/strong> (any short string like <code>TEST<\/code>).<\/li>\n<li>Visit your buy page. The picker renders, the cheapest combination is preselected, and clicking different options updates the live total.<\/li>\n<li>Leave a cell blank in the Pricing Matrix and reload \u2014 that postage \/ duration is greyed-out and unselectable, and the <strong>Buy membership<\/strong> button is disabled if the current selection has no price.<\/li>\n<\/ul><\/dd>\n<dt id=\"2.%20end-to-end%20with%20stripe%20in%20test%20mode%20%28pro%20own-key%20setup%29\"><h3>2. End-to-end with Stripe in TEST mode (Pro own-key setup)<\/h3><\/dt>\n<dd><ol>\n<li>Sign up for a free Stripe account. No bank details are needed for test mode.<\/li>\n<li>Activate Pro, enable Stripe on <strong>Payments<\/strong>, select <strong>Test<\/strong> under <strong>Your own Stripe keys (Pro)<\/strong>, save your <code>pk_test_...<\/code> and <code>sk_test_...<\/code> keys, and register the test-mode direct webhook as described above. The free Connect setup does not offer a Test\/Live switch in the WordPress settings; do not use a test card against a live connected account.<\/li>\n<li>Visit the buy page, fill in a test email\/name, click <strong>Buy membership<\/strong>, and on the Stripe checkout page use the official test card:<\/li>\n<\/ol>\n\n<ul>\n<li><strong>Card number:<\/strong> 4242 4242 4242 4242<\/li>\n<li><strong>Expiry:<\/strong> any future date \u00a0 <strong>CVC:<\/strong> any 3 digits \u00a0 <strong>ZIP:<\/strong> any 5 digits<\/li>\n<\/ul>\n\n<ol>\n<li>After the redirect, open <strong>Membership Sales \u2192 Recent Purchases<\/strong> in the admin. Once the payment confirmation is delivered, you should see a new entry with <code>paid<\/code> from Stripe and <code>inserted<\/code> (or <code>updated<\/code>) for the sheet.<\/li>\n<\/ol><\/dd>\n<dt id=\"what%20happens%20if%20stripe%20or%20paypal%20sends%20the%20same%20event%20twice%3F\"><h3>What happens if Stripe or PayPal sends the same event twice?<\/h3><\/dt>\n<dd><p>The first delivery is processed normally. Any duplicate of the same event is identified and returns HTTP 200 with <code>status: duplicate<\/code> without writing to the sheet. Two concurrent deliveries for the same event are serialized via an atomic per-event lock; the loser receives HTTP 409 so the gateway retries it. From 1.7.13 each processed event is stored as its own <code>wp_options<\/code> row with a 72-hour TTL \u2014 there is no fixed-size eviction window that could cause old-but-recent events to fall off and be reprocessed.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20the%20google%20sheet%20write%20fails%3F\"><h3>What happens if the Google Sheet write fails?<\/h3><\/dt>\n<dd><p>The plugin returns HTTP 500 to the gateway (so it retries delivery automatically), records a \"failed\" entry on the Recent Purchases page, and shows a persistent admin notice while failures remain unresolved. The buyer confirmation email is <strong>not<\/strong> sent for a failed write \u2014 only a successful upsert triggers it. Pro offers a manual retry for safe failures. If the result of an earlier write is uncertain, use <strong>Reconcile<\/strong> to compare the original payment and intended membership before confirming an already-corrected outcome; do not renew again blindly. Reconciliation itself does not send a receipt, charge, refund, approve an email change or write the roster.<\/p><\/dd>\n<dt id=\"is%20the%20buyer%27s%20payment%20ever%20lost%20if%20the%20sheet%20write%20fails%3F\"><h3>Is the buyer's payment ever lost if the sheet write fails?<\/h3><\/dt>\n<dd><p>No. The gateway has captured the payment regardless of what happens on your site. Recoverable sheet failures can catch up through retries, but an uncertain prior write is deliberately blocked to avoid renewing twice and needs administrator review through <strong>Reconcile<\/strong>.<\/p><\/dd>\n<dt id=\"can%20lifetime%20memberships%20and%20renewals%20coexist%3F\"><h3>Can lifetime memberships and renewals coexist?<\/h3><\/dt>\n<dd><p>Yes. Renewals extend the existing expiration date forward, so an early renewal never shortens the buyer's coverage. A lifetime purchase clears the expiration cell entirely.<\/p><\/dd>\n<dt id=\"what%20if%20a%20buyer%20clicks%20cancel%20at%20paypal%20and%20then%20tries%20to%20pay%20again%3F\"><h3>What if a buyer clicks Cancel at PayPal and then tries to pay again?<\/h3><\/dt>\n<dd><p>The cancelled checkout row is kept visible in Recent Purchases for auditing, but it never blocks a retry. If the gateway somehow delivers a capture event for an order the cancel handler already marked cancelled, the finalisation pipeline now detects and recovers that row automatically rather than treating it as already-done.<\/p><\/dd>\n<dt id=\"does%20the%20free%20plugin%20charge%20a%20fee%20on%20sales%3F\"><h3>Does the free plugin charge a fee on sales?<\/h3><\/dt>\n<dd><p>It depends on how you take card payments:<\/p>\n\n<ul>\n<li><strong>Stripe Connect (free, the default since 3.5.0):<\/strong> click <strong>Connect with Stripe<\/strong> on the Payments screen \u2014 no API keys to paste, Stripe hosts the onboarding. A <strong>2% platform commission<\/strong> is deducted from each card sale (in addition to Stripe's own standard processing fees). The current rate is always shown on the Payments screen before you connect.<\/li>\n<li><strong>Your own API keys (Pro add-on):<\/strong> when Pro's own Stripe keys are saved for the active mode, direct-key checkout has a <strong>0% platform fee<\/strong>. Activating Pro alone does not switch a connected site away from Connect or remove its commission; deactivating Pro returns a connected site to Connect without losing its saved settings.<\/li>\n<li><strong>Legacy free installs<\/strong> that saved their own Stripe keys before 3.5.0 keep working with no commission; the keys remain a supported deprecation path until you choose to connect.<\/li>\n<li><strong>PayPal<\/strong> (a Pro feature) carries no platform commission either.<\/li>\n<\/ul>\n\n<p>The commission is applied server-side by the plugin's hosted Stripe Connect service when the Checkout Session is created; the plugin itself never computes or transmits the fee amount.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>4.10.16<\/h4>\n\n<ul>\n<li>Fix reconciliation of chapter purchases whose email-status warning is added only for display, so the submitted form matches the stored purchase.<\/li>\n<li>Show reconciled purchases as resolved and retain their original errors and email-status notes in expandable details instead of active failure warnings.<\/li>\n<li>Add a sandbox-test abandonment outcome that requires confirmation of no real funds charged, preserves evidence and blocks fulfillment or renewal retries without claiming a refund.<\/li>\n<li>Refuse the sandbox shortcut for known live payments and retain the original gateway mode when provided.<\/li>\n<\/ul>\n\n<h4>4.10.15<\/h4>\n\n<ul>\n<li>Add guided purchase reconciliation with original-payment, intended-membership and correction checks, plus a shorter path for completed full refunds.<\/li>\n<li>Retain administrator evidence and acknowledge later deliveries of reconciled payments without another charge, roster write or renewal.<\/li>\n<li>Keep warnings active while another purchase remains unresolved, and allow guided review of missing purchase records without deleting recovery evidence.<\/li>\n<\/ul>\n\n<h4>4.10.14<\/h4>\n\n<ul>\n<li>Refresh the active roster caches once before refusing a member login when the email is missing from the cached roster.<\/li>\n<li>Keep cached positive matches fast, preserve administrator access, and leave sales lookups unchanged.<\/li>\n<\/ul>\n\n<h4>4.10.13<\/h4>\n\n<ul>\n<li>Wait 60 seconds before sending a replacement login code; repeated presses cycle reassurance messages without sending email, invalidating the current code or consuming the send quota.<\/li>\n<li>Suppress simultaneous code requests on the server and retain the existing code when replacement delivery fails.<\/li>\n<\/ul>\n\n<h4>4.10.12<\/h4>\n\n<ul>\n<li>Correct the selected-member row identity comparison so an unchanged existing chapter member is not incorrectly rejected as changed during paid fulfillment or approved email addition.<\/li>\n<li>Keep genuine identity changes blocked and provide explicit no-write evidence when the selected-record check refuses a write.<\/li>\n<\/ul>\n\n<h4>4.10.11<\/h4>\n\n<ul>\n<li>Retry transient roster-read failures once with a longer timeout, without automatically repeating membership writes.<\/li>\n<li>Distinguish a failed pre-write roster read from an uncertain write response so add-ons can safely recover the original paid purchase.<\/li>\n<li>Keep the member write lock active throughout the longer bounded read attempts.<\/li>\n<\/ul>\n\n<h4>4.10.10<\/h4>\n\n<ul>\n<li>Support an add-on-verified existing-member purchase destination separately from the payer and receipt email, without adding an unapproved address or creating a duplicate member.<\/li>\n<li>Recheck the selected row during the fresh sheet write and calculate its renewal under the member lock, preserving existing names, aliases and contacts.<\/li>\n<li>Keep free-membership eligibility and atomic claims tied to the selected existing member; fail safely if a required destination add-on becomes unavailable.<\/li>\n<\/ul>\n\n<h4>4.10.9<\/h4>\n\n<ul>\n<li>Refresh documentation for separate administrator\/member login routes, provider-specific and filtered roster sync, roster-aware checkout identity, international contact collection, and add-on update-order safeguards.<\/li>\n<li>Documentation and release metadata only; Free runtime behavior is unchanged.<\/li>\n<\/ul>\n\n<h4>4.10.8<\/h4>\n\n<ul>\n<li>Keep the international-address checkbox beside its label instead of stacking it above the text.<\/li>\n<li>Allow add-ons to collect contact fields independently of local sheet write columns, using the same field list for picker rendering and paid-checkout validation.<\/li>\n<li>Offer the electronic-delivery international checkbox when international contact fields are collected, even without a local ZIP column.<\/li>\n<\/ul>\n\n<h4>4.10.7<\/h4>\n\n<ul>\n<li>Add row-preserving roster reads for Chapters identity resolution without changing existing email-keyed readers.<\/li>\n<li>Gift purchases require an active Pro add-on.<\/li>\n<\/ul>\n\n<h4>4.10.6<\/h4>\n\n<ul>\n<li>Administrator dashboard login stays on WordPress's native login page and does not require the administrator's email on a membership roster.<\/li>\n<li>Member logins return to requested front-end content or the home page instead of a carried-over dashboard destination.<\/li>\n<li>Native login submissions, reauthentication, password-reset actions, and relocated administrator login URLs remain supported.<\/li>\n<\/ul>\n\n<h4>4.10.5<\/h4>\n\n<ul>\n<li>Purchase forms identify the signed-in member's email without extending that identity trust to a different entered email or a gift recipient.<\/li>\n<li>Names prefilled from the member's saved record are distinguished from manually entered names so add-ons can reset identity proof without erasing entered recipient details.<\/li>\n<\/ul>\n\n<h4>4.10.4<\/h4>\n\n<ul>\n<li>Lifetime-upgrade revalidation now reads a unique row from the active sales roster and refuses ambiguous email aliases or unavailable membership columns instead of guessing.<\/li>\n<li>The per-email fulfillment lock is released on errors and thrown extension callbacks without being released before the roster write.<\/li>\n<li>Checkout rejects nonnumeric, negative, nonfinite, or out-of-range extension prices while preserving legitimate free ($0) memberships.<\/li>\n<\/ul>\n\n<h4>4.10.3<\/h4>\n\n<ul>\n<li>Added the generic <code>gsms_purchase_email_vars<\/code> and <code>gsms_confirmation_body<\/code> purchase-confirmation filters so integrations can provide purchase-specific itemization while preserving the configured template.<\/li>\n<\/ul>\n\n<h4>4.10.2<\/h4>\n\n<ul>\n<li>Fixed update and read lookups for multi-email roster cells, preserving aliases on renewal and safely refusing duplicate matches across data rows.<\/li>\n<\/ul>\n\n<h4>4.10.1<\/h4>\n\n<ul>\n<li>Added the main site's <code>access_match_mode<\/code> to the public, read-only pricing contract consumed by Chapters sites.<\/li>\n<\/ul>\n\n<h4>4.10.0<\/h4>\n\n<ul>\n<li>Added the public read-only pricing endpoint used by Chapters sites to show conditional chapter-only or combined pricing based on a buyer's main-roster status.<\/li>\n<li>Documented the manual combined-sale remittance workflow and the limits of buyer self-reporting; no funds are transferred automatically.<\/li>\n<\/ul>\n\n<h4>4.9.0<\/h4>\n\n<ul>\n<li>Encrypt each cached main or chapter roster as a single payload in the WordPress database using a separate key derived from this site's security salts.<\/li>\n<li>Encrypt existing plaintext cache entries on first read without extending their expiry; discard cache entries if encryption fails.<\/li>\n<\/ul>\n\n<h4>4.8.1<\/h4>\n\n<ul>\n<li>Existing Annual-mode sites now migrate their day-based grace setting to a calendar-month value that never shortens previously granted access.<\/li>\n<li>The settings page warns before leaving with unsaved edits and shows a fixed Save Settings bar after a field changes.<\/li>\n<\/ul>\n\n<h4>4.8.0<\/h4>\n\n<ul>\n<li>Annual expiration mode now uses the same month-based Grace Period field as Monthly mode. Daily mode continues to use days.<\/li>\n<li>The single Grace Period control switches immediately between days and months when the expiration mode changes.<\/li>\n<\/ul>\n\n<h4>4.7.0<\/h4>\n\n<ul>\n<li>Replaced the separate annual-mode checkbox and Sales month-alignment controls with one roster-aware Daily, Monthly, or Annual expiration mode.<\/li>\n<li>Monthly mode keeps active and recently lapsed renewals anchored to their prior expiration, limits deep-lapse backdating to half the configured grace window, and moves calculated dates forward to the first day of a month.<\/li>\n<li>Existing sites using annual calendar-year memberships migrate automatically to Annual mode.<\/li>\n<\/ul>\n\n<h4>4.6.13<\/h4>\n\n<ul>\n<li>Added an optional Sales expiration rule that extends new date-based memberships to the start of the next even-numbered or odd-numbered month.<\/li>\n<li>The same adjustment can optionally apply to expired renewals after a configurable number of completed months past their prior expiration. Active renewals, lifetime memberships, annual calendar-year memberships, and rows without an expiration date are unchanged.<\/li>\n<\/ul>\n\n<h4>4.6.12<\/h4>\n\n<ul>\n<li>Service-account JSON credentials are now encrypted at rest with authenticated encryption derived from this WordPress installation's security salts. Existing plaintext credentials migrate automatically, and the Access settings screen reports their encryption status.<\/li>\n<li>The credential-bearing settings option no longer autoloads on every WordPress request.<\/li>\n<\/ul>\n\n<h4>4.6.11<\/h4>\n\n<ul>\n<li>Added an explicit security warning to the Google Service Account setup instructions: the downloaded JSON private-key file must never be shared or made public; only the service account email should be shared with the spreadsheet owner.<\/li>\n<\/ul>\n\n<h4>4.6.10<\/h4>\n\n<ul>\n<li>Fixed chapter roster comparisons so main-roster rows without an email address can still be considered by the optional last-name review match. Email-less rows remain excluded from login, access, and ordinary member-profile lookups.<\/li>\n<\/ul>\n\n<h4>4.6.9<\/h4>\n\n<ul>\n<li>Clarified that one Google Service Account covers every roster this site connects to. \"Save &amp; Test Main Roster\" now notes when the connected sheet has Viewer-only access, so Sales and Group Email limitations are visible immediately. Blank-tab tests now show which tab was actually read.<\/li>\n<\/ul>\n\n<h4>4.6.8<\/h4>\n\n<ul>\n<li>Replaced generic Google Sheets connection failures with staged, roster-specific diagnostics. Tests now identify malformed or incomplete service-account JSON, unusable private keys, Google authentication failures, spreadsheet access or ID problems, missing explicit tabs with the available tab list, automatic first-tab selection, and required column-mapping errors. The report shows the non-secret service-account email that must be granted access while never showing private keys, tokens, raw JSON, roster rows, or Google response bodies.<\/li>\n<li>Chapter connection tests and local-vs-main comparisons now identify the exact roster that needs correction instead of returning a generic comparison failure.<\/li>\n<\/ul>\n\n<h4>4.6.7<\/h4>\n\n<ul>\n<li>Fixed blank\/unknown Sheet tab names in Chapter roster comparisons and connection tests. Fresh main and Chapter configurations now truly leave the tab name blank so the first tab is discovered automatically. A renamed or recreated first tab is refreshed once instead of leaving comparison stuck on a stale cached title. Existing sites that still show the old \"Sheet1\" default can clear that field once to opt into automatic first-tab discovery; explicit names still fail visibly if they are wrong.<\/li>\n<\/ul>\n\n<h4>4.6.6<\/h4>\n\n<ul>\n<li>Fixed the Site Mode guidance so the Main and Chapter instructions update immediately when the mode selector changes. Chapter sites are now explicitly advised to use Viewer (read-only) access to the main roster so they cannot broadcast Group Email to the entire organisation.<\/li>\n<\/ul>\n\n<h4>4.6.5<\/h4>\n\n<ul>\n<li>Tested with the latest stable WordPress release, 7.1.<\/li>\n<\/ul>\n\n<h4>4.6.4<\/h4>\n\n<ul>\n<li>Fixed the Chapters promotion card for existing customers. Installed-but-inactive sites are directed to activate Chapters, and active sites with an expired, revoked, or missing license are directed to the Freemius Customer Portal instead of being asked to buy Chapters again.<\/li>\n<\/ul>\n\n<h4>4.6.3<\/h4>\n\n<ul>\n<li>Clarified the Site Mode guidance: Chapter mode writes membership changes only to the local chapter roster and reads the main roster only for optional checks or comparisons; Main mode requires Editor access to the main roster for membership changes and Group Email, which can target only that authorized main roster.<\/li>\n<\/ul>\n\n<h4>4.6.2<\/h4>\n\n<ul>\n<li>Added a secure Chapters add-on purchase link to the free plugin's Access promotion. When Chapters is installed, the link uses its existing Freemius upgrade URL; free-only sites use the verified public checkout for product 37810, plan 62888.<\/li>\n<\/ul>\n\n<h4>4.6.1<\/h4>\n\n<ul>\n<li>Fixed an admin-screen fatal for sites where Pro is active but its license is not currently valid. The GSheet Membership Pro card now correctly links existing customers to the Freemius Customer Portal.<\/li>\n<\/ul>\n\n<h4>4.6.0<\/h4>\n\n<ul>\n<li>Added a clear, state-aware GSheet Membership Pro upgrade path to the Access and Sales admin screens. Free sites see the verified Pro checkout link, installed-but-inactive sites are directed to activate Pro, and licensed Pro sites see the active state instead of purchase prompts.<\/li>\n<li>Added read-only Pro previews for authenticated mailbox sending, PDF watermarks, editable membership levels, and PayPal. Preview controls never save Pro-only settings.<\/li>\n<\/ul>\n\n<h4>4.5.9<\/h4>\n\n<ul>\n<li>WordPress.org listing refresh: broadened the discovery tags and branch\/chapter wording, gave Group Email its own FAQ entry with Email-in as the forwarding trigger, expanded the screenshot gallery, and clarified Pro versus the separate Chapters add-on in both banner sizes. No functional changes.<\/li>\n<\/ul>\n\n<h4>4.5.8<\/h4>\n\n<ul>\n<li>Multi-address roster cells now split on any separator: a second email address added on a new line inside the cell, or separated only by a space, is now recognized. Previously only commas and semicolons split, so an address on its own line silently fused with its neighbor and disappeared from logins, rosters, and email lists.<\/li>\n<\/ul>\n\n<h4>4.5.7<\/h4>\n\n<ul>\n<li>Escaped result-box output at the point it's printed, rather than relying on callers to have already done so, resolving a Plugin Check finding. No visible change.<\/li>\n<\/ul>\n\n<h4>4.5.6<\/h4>\n\n<ul>\n<li>Action-result boxes can now be placed next to the exact form or buttons that produced them: a result may carry a placement tag, and the page renders tagged results at the matching spot (used by the Pro Group Email send\/test buttons). Untagged results keep rendering under the page title.<\/li>\n<\/ul>\n\n<h4>4.5.5<\/h4>\n\n<ul>\n<li>Action results and save confirmations now appear inline on the plugin's own admin pages \u2014 directly under the page title, next to the forms that triggered them \u2014 instead of at the very top of the admin screen.<\/li>\n<\/ul>\n\n<h4>4.5.4<\/h4>\n\n<ul>\n<li>Admin feedback (save confirmations, action results, upload results, the Stripe connection status and the webhook-failure warning) is now shown in the plugin's own message boxes instead of standard WordPress notice banners. Admin-cleanup and white-label plugins commonly hide or relocate every standard notice banner, which silently swallowed this feedback on affected sites.<\/li>\n<\/ul>\n\n<h4>4.5.3<\/h4>\n\n<ul>\n<li>Admin notices (save confirmations, error reports, action results shown after a redirect) now survive hosts whose object cache drops transients: each notice is also stored as a short-lived option and read from there first, so redirect round-trip feedback no longer disappears silently.<\/li>\n<\/ul>\n\n<h4>4.5.2<\/h4>\n\n<ul>\n<li>Reworded comments and documentation describing what the Pro add-on supplies, consistently phrased as capability it adds on top of the free plugin, never as something the free plugin restricts or withholds. No functional changes.<\/li>\n<\/ul>\n\n<h4>4.5.1<\/h4>\n\n<ul>\n<li>Free-checkout polish: when a free ($0) signup can't be recorded, the buyer now always sees the generic translated failure message \u2014 the specific reason stays in the admin purchase log, matching every other checkout failure. Internal documentation now lists the 'free' gateway.<\/li>\n<\/ul>\n\n<h4>4.5.0<\/h4>\n\n<ul>\n<li>Free ($0) memberships are now supported: enter 0 in a Pricing Matrix cell to offer that combination at no charge. Buyers who pick it skip the payment step entirely \u2014 the membership is recorded and the confirmation email sent immediately, and the picker shows \"Free\" instead of a zero amount. A blank cell still means not-for-sale.<\/li>\n<\/ul>\n\n<h4>4.4.2<\/h4>\n\n<ul>\n<li>The Membership Levels block on the Pricing Matrix screen is now its own form with a \"Save Levels\" button for choosing which levels are offered for purchase. The price grid below keeps its own Save button for prices only.<\/li>\n<\/ul>\n\n<h4>4.4.1<\/h4>\n\n<ul>\n<li>The plugin no longer stores any Stripe API keys of its own \u2014 card payments on the free plugin always run through the hosted \"Connect with Stripe\" onboarding. Sites using their own Stripe keys via the Pro add-on are unaffected: the add-on now stores those keys itself. The PayPal enable toggle likewise moved to the Pro add-on's settings defaults \u2014 the free plugin no longer defines any PayPal setting.<\/li>\n<\/ul>\n\n<h4>4.3.6<\/h4>\n\n<ul>\n<li>A fresh install's two default membership levels are now 1-Year and Lifetime (previously 1-Year and 3-Year). Existing sites are unaffected \u2014 the defaults only apply when no levels have been saved yet. Pairs with 4.3.5: a Lifetime member on a stock free install can change their delivery\/postage at the correct price differential, no Pro add-on required.<\/li>\n<\/ul>\n\n<h4>4.3.5<\/h4>\n\n<ul>\n<li>Existing members of a non-expiring level can now change their delivery\/postage option without the Pro add-on: the plugin computes the price differential itself and charges exactly that. Same-option and lower-priced requests are declined with clear messages. The Pro add-on's role narrows to gift-upgrade validation.<\/li>\n<\/ul>\n\n<h4>4.3.4<\/h4>\n\n<ul>\n<li>Readme: added a Mailchimp entry to the External services disclosure section (Pro's optional Group Email delivery method). No functional changes.<\/li>\n<\/ul>\n\n<h4>4.3.3<\/h4>\n\n<ul>\n<li>Wording: the plugin header description now reflects the current Pro feature set (level editor, Mailchimp delivery option, authenticated mailbox sending, PDF watermarks, large-file links). No functional changes.<\/li>\n<\/ul>\n\n<h4>4.3.2<\/h4>\n\n<ul>\n<li>Wording: the Pro add-on summaries (settings-page teaser card and readme) now mention the optional Mailchimp delivery for Group Email. No functional changes.<\/li>\n<\/ul>\n\n<h4>4.3.1<\/h4>\n\n<ul>\n<li>Wording: the Pro teaser card on the Access settings page now describes the Pro add-on's level editor instead of the outdated \"multi-year and Lifetime durations\" line (3-Year is included in the free plugin as of 4.3.0).<\/li>\n<\/ul>\n\n<h4>4.3.0<\/h4>\n\n<ul>\n<li>A fresh install now ships two ready-to-sell membership levels, 1-Year and 3-Year. Existing sites keep their current levels unchanged.<\/li>\n<li>Wording corrections across the Pricing Matrix screen and readme: the Pro add-on's boundary is a level editor (create, rename, remove, redefine levels, including Lifetime) \u2014 not a level count.<\/li>\n<\/ul>\n\n<h4>4.2.17<\/h4>\n\n<ul>\n<li>Plugin Check compliance: annotated the intentional use of WordPress's DONOTCACHEPAGE caching convention on the login page so it is no longer flagged as an unprefixed plugin constant. No functional changes.<\/li>\n<\/ul>\n\n<h4>4.2.16<\/h4>\n\n<ul>\n<li>Wording refinements on the Pricing Matrix screen.<\/li>\n<\/ul>\n\n<p>Earlier entries are in changelog.txt inside the plugin.<\/p>","raw_excerpt":"Membership access control and sales from a private Google Sheet \u2014 page and login gating, Stripe checkout, and print &amp; digital subscriptions.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/346528","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=346528"}],"author":[{"embeddable":true,"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/gsheetplugins"}],"wp:attachment":[{"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=346528"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=346528"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=346528"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=346528"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=346528"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/sr.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=346528"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}