# Subscription Walls

The Subscription Wall recovers revenue by controlling app access when a subscription is paused or a payment has failed. For a paused subscription the user is asked to resume to regain access; for a failed payment the user is asked to update their payment method. In both cases the user is guided through the right recovery flow so they can keep the subscription active and get access back.

## Pause Wall

The Pause Wall applies when a customer has paused their subscription. Depending on configuration, it can be a dismissible wall or a full block that stays until the subscription is resumed.

![Pause Wall UI](https://churnsolution.com/wp-content/themes/astra-child/assets/images/docs/pause-wall.png "Pause Wall: resume in one click, or cancel instead.")

## Failed Payment Wall

The Failed Payment Wall applies when a subscription is still active but a payment attempt has failed. It asks the user to update their payment method to restore access.

![Failed Payment Wall UI](https://churnsolution.com/wp-content/themes/astra-child/assets/images/docs/failed-payment-wall.png "Failed Payment Wall: update the card inline.")

## Installation

### Step 1: Place the embed code and launch snippet

Paste the embed code before the closing `</body>` tag on every page where the walls should appear. The script loads Churn Solution and checks the subscription each time the page opens, showing the Pause Wall or the Failed Payment Wall when one applies. The `options` object controls each wall's behaviour; every setting and callback is listed under Configuration options. Your App ID is on the [Integration page](https://app.churnsolution.com/settings/network) after you connect your payment provider.

```html
<script>
  !function(){
    if (!window.churnSolution || !window.churnSolution.ready) {
      window.churnSolution = { ready: true };
      const s = document.createElement('script');
      s.src = 'https://app.churnsolution.com/sdk/index.min.js';
      s.async = true;
      const e = document.getElementsByTagName('script')[0];
      e.parentNode.insertBefore(s, e);
    }
  }();
</script>
<script>
  function runWallScript() {
    window.churnSolution?.checkWall({
      subscriptionId, // your subscription ID from your payment provider
      appId: 'APP_ID', // your App ID
      options: {
        softWall: false,        // false = hard (blocking); true = dismissible
        gracePeriodDays: 7,     // failed-payment only: soft until invoice.created + days, then hard
        shouldShowWall: (session) => {
          // session: { type, invoice, subscription, customer, resumes_at }
          // type: FAILED_PAYMENT | PAUSE
          return true; // true | false | 'soft' | 'hard'
        },
      },
    });
  }

  function onChurnSolutionLoad(callback, timeout = 10000, interval = 100) {
    const startTime = Date.now();
    const check = () => {
      if (typeof window.churnSolution?.checkWall === 'function') callback();
      else if (Date.now() - startTime < timeout) setTimeout(check, interval);
      else console.warn('Churn Solution script did not load in time.');
    };
    check();
  }

  onChurnSolutionLoad(() => runWallScript());
</script>
```

### Step 2: Generate security token (Optional)

Generate a hash on your server so every request to Churn Solution is secure, then send the hash to the front end and pass it as `authKey` in the launch call. Examples in several backend languages are on your [gateway page](/docs/integrations/stripe/). Your secret key is on the **Account** settings page — keep it on your server and never send it to the browser.

**Node.js**

```js
const crypto = require('crypto');

// POST /api/churn-solution  — requires a logged-in user
app.post('/api/churn-solution', requireAuth, (req, res) => {
  const subscriptionId = req.user.subscriptionId; // from YOUR session, never the request body
  const authKey = crypto
    .createHmac('sha256', process.env.CHURNSOLUTION_SECRET_KEY)
    .update(subscriptionId)
    .digest('hex');
  res.json({ authKey, subscriptionId });
});
```
**Next.js**

```ts
// app/api/churn-solution/route.ts
import crypto from 'crypto';
import { getSession } from '@/lib/auth';

export async function POST() {
  const session = await getSession();
  if (!session) return new Response('Unauthorized', { status: 401 });
  const subscriptionId = session.user.subscriptionId;
  const authKey = crypto
    .createHmac('sha256', process.env.CHURNSOLUTION_SECRET_KEY!)
    .update(subscriptionId)
    .digest('hex');
  return Response.json({ authKey, subscriptionId });
}
```
**Python**

```py
import hmac, hashlib, os
from flask import Flask, jsonify
from auth import current_user, login_required

app = Flask(__name__)

@app.post("/api/churn-solution")
@login_required
def churn_solution():
    subscription_id = current_user().subscription_id
    auth_key = hmac.new(
        os.environ["CHURNSOLUTION_SECRET_KEY"].encode(),
        subscription_id.encode(),
        hashlib.sha256,
    ).hexdigest()
    return jsonify(authKey=auth_key, subscriptionId=subscription_id)
```
**PHP**

```php
<?php
// Laravel route: POST /api/churn-solution (auth middleware)
Route::post('/api/churn-solution', function () {
    $subscriptionId = auth()->user()->subscription_id;
    $authKey = hash_hmac('sha256', $subscriptionId, env('CHURNSOLUTION_SECRET_KEY'));
    return response()->json(['authKey' => $authKey, 'subscriptionId' => $subscriptionId]);
})->middleware('auth');
```
**Ruby**

```rb
# Rails controller
class ChurnSolutionController < ApplicationController
  before_action :authenticate_user!

  def create
    subscription_id = current_user.subscription_id
    auth_key = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('CHURNSOLUTION_SECRET_KEY'), subscription_id)
    render json: { authKey: auth_key, subscriptionId: subscription_id }
  end
end
```
**Go**

```go
func churnSolutionHandler(w http.ResponseWriter, r *http.Request) {
    user := mustUser(r) // your auth
    mac := hmac.New(sha256.New, []byte(os.Getenv("CHURNSOLUTION_SECRET_KEY")))
    mac.Write([]byte(user.SubscriptionID))
    authKey := hex.EncodeToString(mac.Sum(nil))
    json.NewEncoder(w).Encode(map[string]string{
        "authKey": authKey, "subscriptionId": user.SubscriptionID,
    })
}
```
**Java**

```java
// Spring Boot
@PostMapping("/api/churn-solution")
public Map<String, String> churnSolution(@AuthenticationPrincipal AppUser user) throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(System.getenv("CHURNSOLUTION_SECRET_KEY").getBytes(UTF_8), "HmacSHA256"));
    String authKey = HexFormat.of().formatHex(mac.doFinal(user.getSubscriptionId().getBytes(UTF_8)));
    return Map.of("authKey", authKey, "subscriptionId", user.getSubscriptionId());
}
```
**C#**

```cs
// ASP.NET Core minimal API
app.MapPost("/api/churn-solution", (ClaimsPrincipal user) =>
{
    var subscriptionId = user.FindFirst("subscription_id")!.Value;
    var key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("CHURNSOLUTION_SECRET_KEY")!);
    using var hmac = new HMACSHA256(key);
    var authKey = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(subscriptionId))).ToLowerInvariant();
    return Results.Ok(new { authKey, subscriptionId });
}).RequireAuthorization();
```

#### Update: launch Churn Solution

After your endpoint returns an auth key, add it to the launch call from Step 1 as `authKey`.

**Before**

```js
window.churnSolution?.checkWall({
  subscriptionId,
  appId: 'APP_ID',
  options: { /* ... */ },
});
```
**After**

```js
window.churnSolution?.checkWall({
  authKey, // hash generated by your server
  subscriptionId,
  appId: 'APP_ID',
  options: { /* ... */ },
});
```

## Configuration options

### Access modes

#### Soft Wall

A soft wall lets users close it and keep using the application.

```js
softWall: true
```

#### Hard Wall

A hard wall blocks access completely until the user resumes the subscription or updates their payment.

```js
softWall: false
```

### Grace period (failed payments only)

A grace period allows continued access for a set number of days before the wall is enforced.

```js
gracePeriodDays: 7
```

## Dynamic control with shouldShowWall

The `softWall` setting applies one mode to every user. The `shouldShowWall` callback lets you decide per user, at runtime, whether to show a wall and whether it should be soft or hard. It runs each time `checkWall` is called and receives a single `session` argument describing the current subscription state.

| Session property | Description |
|---|---|
| `type` | The wall type: `FAILED_PAYMENT` or `PAUSE`. |
| `invoice` | The related invoice for failed payments, including amount, currency and status. |
| `subscription` | The subscription, including its plan, status and metadata. |
| `customer` | The customer, including email and any custom metadata. |
| `resumes_at` | For paused subscriptions, the time the subscription is scheduled to resume. |

| Return value | Outcome |
|---|---|
| `true` | Shows the wall using the mode set by `softWall` or `gracePeriodDays`. |
| `false` | Doesn't show the wall; the user keeps full access. |
| `'soft'` | Forces a dismissible soft wall, overriding other settings. |
| `'hard'` | Forces a blocking hard wall, overriding other settings. |

Use it to match the recovery experience to your business rules: hard-block large unpaid invoices while giving small balances a dismissible wall, keep trusted or high-tier customers on a soft wall, or skip the wall for a segment identified through subscription or customer metadata.

```js
shouldShowWall: (session) => {
  const { type, invoice, subscription, customer, resumes_at } = session;

  // Never wall your internal or VIP accounts
  if (customer?.metadata?.plan_tier === 'vip') return false;

  if (type === 'FAILED_PAYMENT') {
    // Hard-block large unpaid invoices, keep small ones dismissible
    return invoice?.amount_due >= 10000 ? 'hard' : 'soft';
  }

  if (type === 'PAUSE') {
    // Only wall paused subscriptions that are still active plans
    return subscription?.metadata?.enforce_pause === 'true';
  }

  return true; // fall back to the softWall / gracePeriodDays settings
},
```

> **Note:**
> `shouldShowWall` runs on the client, so treat it as a presentation rule. Churn Solution always re-validates access enforcement on its side.

## System events

System events apply globally to both the Pause Wall and the Failed Payment Wall.

### Lifecycle events

| Callback | Fires when |
|---|---|
| `onWallActivated({ type })` | Either wall becomes visible to the user. |
| `onWallClose({ type })` | The user closes or exits the wall. |
| `onError(error, type)` | An error occurs within the wall system. `type` is one of `WALL_INITIALIZATION_ERROR`, `FAILED_PAYMENT_WALL_UPDATE_CARD_ERROR`, `PAUSE_WALL_RESUME_ERROR`, `PAUSE_WALL_CANCEL_ERROR`. |

## Subscription actions

Subscription actions fire based on the recovery flow the user is in.

### Failed payment flow

`onUpdatePaymentInformation(customer)` — fires after the user successfully updates their payment details.

### Pause flow

`onResumeSubscription(customer)` — fires when the user resumes their subscription.
`onCancelSubscription(customer)` — fires when the user cancels a paused subscription.

## Full configuration example

This example combines every option in a single `checkWall` call: the gating settings, the `shouldShowWall` decision callback, and all lifecycle and subscription-action callbacks for both wall types.

```js
window.churnSolution?.checkWall({
  authKey,          // hash generated by your server
  subscriptionId,   // your subscription ID from your payment provider
  appId: 'APP_ID',  // your App ID
  options: {
    // Gating (applies to both wall types)
    softWall: false,        // false = hard (blocking, non-dismissable)
    gracePeriodDays: 0,     // failed-payment only: soft until (invoice.created + days), then hard
    shouldShowWall: (session) => true, // true | false | 'soft' | 'hard'

    // Generic callbacks
    onWallActivated: ({ type }) => console.log('wall activated, type:', type),
    onWallClose: ({ type }) => console.log('wall closed (soft), type:', type),
    onError: (error, type) => console.log('wall error', type, error),

    // Failed-payment wall
    onUpdatePaymentInformation: (customer) => console.log('payment updated, customer:', customer),

    // Pause wall
    onResumeSubscription: (customer) => console.log('subscription resumed, customer:', customer),
    onCancelSubscription: (customer) => console.log('subscription cancelled, customer:', customer),
  },
});
```

## Paid Memberships Pro

If you use Paid Memberships Pro, you don't need to change any code. Contact the Churn Solution team and we enable and configure the Pause Wall and Failed Payment Wall for your site. The integration is fully managed and requires no additional implementation.

[Contact the team](https://app.churnsolution.com/contact-us)
