Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion content/docs/dashboard/dashboard-settings/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"overview-settings-revenue-tracking",
"overview-settings-refund-protection",
"overview-settings-audit-log",
"overview-settings-access-controls",
"overview-settings-team",
"overview-settings-projects",
"overview-settings-all-teams",
Expand All @@ -16,4 +17,4 @@
"!overview-settings-apple-search-ads",
"..."
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: "Access Controls"
description: "Manage organization roles, project access, and scoped API keys."
---

Use **Access Controls** to decide who can work inside your organization, which projects they can access, and what organization API keys are allowed to do.

Access controls apply at the organization level. You can give a member or API key access to every project, or restrict it to specific projects.

<Frame>![Team settings showing members, roles, and project access](/images/rbac_teams.jpg)</Frame>

### Opening Access Controls

Open an app and go to **Settings > Team** to manage member roles and project access.

Use these settings pages for access management:

| Page | Use it to |
| --- | --- |
| Team | Invite teammates, update organization roles, and restrict members to specific projects. |
| API Keys | Create, update, or revoke organization API keys with selected scopes and project access. |

Only **Owners** and **Admins** can manage access. Owners can manage any role, including other Owners. Admins can manage most members and API keys, but they cannot assign or manage the Owner role.

<Warning>
If an Admin is restricted to specific projects, they can only manage access for projects they can already access. Restricted Admins cannot grant unrestricted organization access.
</Warning>

### Organization roles

Organization roles control the maximum set of actions a member can take. Project access can narrow where those actions apply, but it cannot grant permissions beyond the member's organization role.

| Role | What it can do |
| --- | --- |
| Owner | Full organization control. Owners can manage billing, settings, access controls, API keys, and other Owners. Owners always have access to all projects. |
| Admin | Full working access and access-management permissions, except for managing Owners. Admins can be restricted to specific projects. |
| User (Legacy) | Legacy Admin-level role kept for backward compatibility. Treat this as full access and reassign it when possible. |
| Editor | Can create and edit paywalls, campaigns, notifications, and assets. Editors can view related resources, but cannot manage access or sensitive organization settings. |
| Reader | Read-only visibility into dashboard resources. Readers cannot create, update, or delete resources. |
| Analyst | Read-only, analytics-focused visibility for stakeholders who need reporting access without edit permissions. |

### Project access

Each member has one project access mode:

| Mode | What it means |
| --- | --- |
| All Projects | The member can access every current and future project allowed by their organization role. |
| Restricted | The member can only access the projects you assign to them. |

When a member is **Restricted**, assign one role for each project they can access:

| Project role | Use it for |
| --- | --- |
| Admin | Project-level management access. |
| Editor | Editing resources inside the project. |
| Viewer | Read-only access to the project. |

<Note>
Project roles are capped by the organization role. For example, a Reader with a Project Admin grant is still read-only because the organization role does not allow writes.
</Note>

Use the **Project access** dropdown when inviting or editing a member to choose **Restricted**. When selected, Superwall shows the project assignments and project role controls for that member.

<Frame>![Invite member dialog showing organization role and project access controls](/images/rbac_invite.jpg)</Frame>

### Invite a member

1. Open **Settings > Team**.
2. Click **Invite member**.
3. Enter the member's name and email.
4. Choose an organization role.
5. Choose **All Projects** or **Restricted**.
6. If restricted, select the projects they can access and choose a project role for each one.
7. Click **Invite**.

The invite appears as pending until the user accepts it.

### Update a member

From **Settings > Team**, click **Edit** next to a member. You can change their organization role, project access mode, and project assignments.

Owners cannot remove or demote the last Owner in an organization. Admins cannot assign the Owner role or edit existing Owners.

### API key access

Organization API keys use the same access model:

| Setting | What it controls |
| --- | --- |
| Scopes | Which resources the key can read or write, such as paywalls, campaigns, products, webhooks, charts, users, assets, or access controls. |
| Project Access | Whether the key can operate across all projects or only selected projects. |

Both checks must pass. For example, an API key with `paywalls:write` and **Restricted** access to one project can only update paywalls in that project.

In the create key dialog, choose the scopes first, then use **Project access** to decide whether the key can access all projects or only selected projects.

<Frame>![Create API key dialog showing scopes and project access](/images/rbac_api.jpg)</Frame>

When you create a key, Superwall shows the token once. Copy it before closing the dialog. After that, the dashboard only shows a masked token.

### Revoke or update an API key

Use **Settings > API Keys** to review each key's scopes, project access, creation date, and last-used timestamp. Edit the key to change its scopes or project restrictions, or revoke it when it is no longer needed.

<Tip>
Prefer restricted API keys for automation. Give each service only the scopes and projects it needs.
</Tip>

### Troubleshooting

If a member cannot see a project, confirm that their project access mode is **All Projects** or that the project is selected in their restricted assignments.

If an API request is denied, check both the key's scopes and its project access. The key needs the correct resource scope and access to the target project.

If you cannot assign an Owner, make sure you are signed in as an Owner. Admins cannot grant or manage Owner access.

### Related

- [Team settings](/dashboard/dashboard-settings/overview-settings-team)
- [Projects](/dashboard/dashboard-settings/overview-settings-projects)
- [Keys](/dashboard/dashboard-settings/overview-settings-keys)
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ You'll use your API key to initialize the Superwall client in your apps. Each on

<Frame>![](/images/overview-settings-keys-copy.png)</Frame>

<Note>
These are public SDK keys used to configure Superwall in your app. For scoped organization API keys used by servers, CI jobs, or other backend automation, use [Access Controls](/dashboard/dashboard-settings/overview-settings-access-controls).
</Note>

### Session starts by SDK version

Use the session starts by SDK chart to see how many sessions are being started, split out by the SDK version of Superwall.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: "Team"

In the **Team** section within **Settings**, you can view and edit your Superwall team:

Team members have access to all of your apps within Superwall, making collaboration seamless.
Team members can collaborate across your Superwall organization. By default, members can access all projects allowed by their role. To restrict members to specific projects, use [Access Controls](/dashboard/dashboard-settings/overview-settings-access-controls).

### Invite users

Expand All @@ -16,7 +16,7 @@ Once the user accepts the invite, they'll show up in your Team section. You can

### Team roles

Only **Owners** or **Admins** can change team member roles.
Only **Owners** or **Admins** can change team member roles. For project-level restrictions and API key permissions, use [Access Controls](/dashboard/dashboard-settings/overview-settings-access-controls).

#### Owner — Full control

Expand All @@ -31,18 +31,13 @@ Only **Owners** or **Admins** can change team member roles.

- Can perform most administrative actions
- Can invite/remove team members
- Cannot assign or change Owner roles (can only assign Admin, Editor, or Reader)
- Cannot assign or change Owner roles
- Access to sensitive features like webhook destinations
- Full create/update/delete permissions on paywalls, campaigns, products, etc.

#### Editor — Can create and modify content

- Can read, create, and update:
- Paywalls and paywall triggers
- Campaigns and A/B tests
- Products
- Notifications
- Projects
- Can create and update paywalls, campaigns, notifications, and assets
- Can view applications and organizations
- Cannot delete applications
- Cannot access team management (invite/remove members)
Expand All @@ -54,15 +49,21 @@ Only **Owners** or **Admins** can change team member roles.
- Cannot create, update, or delete anything
- Useful for stakeholders who need visibility but shouldn't make changes

#### Analyst — Analytics-focused visibility

- Can view analytics and reporting surfaces
- Cannot create, update, or delete resources
- Useful for finance, data, or growth stakeholders who need visibility without edit access

#### User (Legacy)

<Warning>
The User role is a legacy role kept for backward compatibility. It has the same permissions as Admin. New team members should be assigned one of the roles listed above instead.
The User role is a legacy role kept for backward compatibility. It has the same permissions as Admin. Use Admin, Editor, Reader, or Analyst for new assignments when possible.
</Warning>

- Has the same permissions as Admin
- Exists only for backward compatibility with accounts created before the current role system
- If you see team members with the User role, consider reassigning them to the appropriate role (Owner, Admin, Editor, or Reader)
- If you see team members with the User role, consider reassigning them to the appropriate role

### Renaming your team

Expand Down
8 changes: 3 additions & 5 deletions content/docs/dashboard/products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Adding Products"
---

Add your existing products from their respective storefront, such as the App Store or the Google Play Store, to an app so they can be used in one or more paywalls. For adding Stripe products, please view [this doc](/web-checkout/web-checkout-adding-a-stripe-product).
Add your existing products from their respective storefront, such as the App Store or the Google Play Store, to an app so they can be used in one or more paywalls. For adding Stripe products, please view [this doc](/web-checkout/web-checkout-adding-a-stripe-product). For iOS products purchased through your own billing system, see [Custom Store Products](/ios/guides/custom-store-products).

<Warning>
Before you attempt to test a paywall on iOS via TestFlight, make sure they are in the "Ready to Submit" phase if it's their initial launch. For local testing, you can use a [StoreKit configuration file](/sdk/guides/testing-purchases) at any point.
Expand All @@ -26,17 +26,15 @@ From there, you have five fields to fill out:

| Field | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------- |
| Identifier | The StoreKit or Google Play product identifier for your product. |
| Identifier | The StoreKit, Google Play, or custom billing product identifier for your product. |
| Trial | The trial duration attached to the product, if any. |
| Price | The price attached to the product. Either type one in, or just the dropdown to select common price points. |
| Period | The length of the subscription. |
| Entitlements | The entitlements this product belongs to. |

When you're done, click **Save**.

Note that the pricing information you enter here is **only** used in the Paywall Editor. On
device, that information is pulled directly from the App Store or Google Play Store and will be
localized.
Note that the pricing information you enter here is **only** used in the Paywall Editor for App Store and Google Play products. On device, that information is pulled directly from the App Store or Google Play Store and will be localized. For iOS Custom Store Products, the SDK uses the product metadata from Superwall and routes purchase attempts to your `PurchaseController`.

<Warning>
Take care to make sure your product identifier is correct and matches its storefront. This is the
Expand Down
Binary file added content/docs/images/rbac_api.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added content/docs/images/rbac_invite.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added content/docs/images/rbac_teams.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
157 changes: 157 additions & 0 deletions content/docs/ios/guides/custom-store-products.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
title: "Custom Store Products"
description: "Sell products from non-App Store billing systems on iOS paywalls using a PurchaseController."
---

Custom Store Products let an iOS paywall sell products that are not backed by StoreKit. Use them when the checkout is owned by your app, your server, Stripe, a web flow, or another billing system, but you still want the product to appear on a Superwall paywall with product variables, trial eligibility, and purchase tracking.

<Warning>
Custom Store Products require iOS SDK `4.15.0` or later and a [`PurchaseController`](/ios/sdk-reference/PurchaseController). If your app does not configure a purchase controller, custom product purchases will fail because there is no App Store product for Superwall to purchase.
</Warning>

## How It Works

When a paywall contains a Custom Store Product, Superwall:

1. Loads the product metadata from Superwall instead of StoreKit.
2. Makes the product available to paywall variables such as `products.primary.price`, `products.selected.period`, and trial variables.
3. Checks trial eligibility using the product's entitlements and the user's entitlement history.
4. Calls your `PurchaseController` when the user starts the purchase.
5. Tracks the purchase result that your controller returns.

Your app is responsible for the actual checkout and entitlement state. After a successful external purchase, update `Superwall.shared.subscriptionStatus` so Superwall knows whether the user should keep seeing paywalls.

<Note>
If you want Superwall's hosted Stripe web checkout and redemption flow, use [Web Checkout](/ios/guides/web-checkout). Custom Store Products are for purchase flows that your app handles from `PurchaseController`.
</Note>

## Add a PurchaseController

Pass a `PurchaseController` when configuring Superwall:

```swift Swift
let purchaseController = CustomStorePurchaseController()

Superwall.configure(
apiKey: "MY_API_KEY",
purchaseController: purchaseController
)
```

Inside `purchase(product:)`, StoreKit-backed products contain either `sk1Product` or `sk2Product`. Custom Store Products do not, so route them to your external billing system using `product.productIdentifier`.

```swift Swift
import SuperwallKit

final class CustomStorePurchaseController: PurchaseController {
func purchase(product: StoreProduct) async -> PurchaseResult {
if hasStoreKitProduct(product) {
return await Superwall.shared.purchase(product)
}

do {
let result = try await BillingClient.shared.purchase(
productIdentifier: product.productIdentifier
)

switch result {
case .purchased:
await syncSubscriptionStatus()
return .purchased
case .pending:
return .pending
case .cancelled:
return .cancelled
}
} catch {
return .failed(error)
}
}

func restorePurchases() async -> RestorationResult {
do {
try await BillingClient.shared.restorePurchases()
await syncSubscriptionStatus()
return .restored
} catch {
return .failed(error)
}
}

private func hasStoreKitProduct(_ product: StoreProduct) -> Bool {
if product.sk1Product != nil {
return true
}

if #available(iOS 15.0, *), product.sk2Product != nil {
return true
}

return false
}

private func syncSubscriptionStatus() async {
let activeProductIds = await BillingClient.shared.activeProductIdentifiers()
let entitlements = Superwall.shared.entitlements.byProductIds(activeProductIds)

await MainActor.run {
Superwall.shared.subscriptionStatus = entitlements.isEmpty
? .inactive
: .active(entitlements)
}
}
}
```

Replace `BillingClient` with your own billing implementation. It should start checkout for `product.productIdentifier`, report cancellation and pending states distinctly when possible, and expose the active product identifiers that should unlock Superwall entitlements.

## Keep Entitlements In Sync

Superwall decides whether a user is active from `subscriptionStatus`, not from the external payment provider directly. When your billing system says the user has access, map the active product identifiers back to Superwall entitlements and set the status:

```swift Swift
let activeProductIds: Set<String> = ["pro_monthly_external"]
let entitlements = Superwall.shared.entitlements.byProductIds(activeProductIds)

Superwall.shared.subscriptionStatus = entitlements.isEmpty
? .inactive
: .active(entitlements)
```

Call this after purchase, after restore, on app launch, and whenever your billing provider reports a subscription or entitlement change.

<Warning>
Make sure the product identifier returned by your billing system matches the product identifier configured in Superwall. If the identifier does not match, `entitlements.byProductIds(_:)` will not find the entitlement and the user can remain inactive after purchase.
</Warning>

## Trial Eligibility

Custom Store Products can use the same trial variables as App Store products. Superwall checks the custom product's trial metadata and associated entitlements, then looks at the user's entitlement history to avoid showing a trial to someone who has already had access.

For best results:

- Attach at least one entitlement to each custom subscription product.
- Keep `subscriptionStatus` current before presenting paywalls.
- Return `.pending` when the external checkout requires more user action.
- Return `.cancelled` when the user intentionally exits checkout.

If customer information has not loaded yet, Superwall avoids treating the user as eligible for a custom-product trial.

## What Not To Do

- Do not call `Superwall.shared.purchase(product)` for a custom product. That helper is for StoreKit-backed products.
- Do not fetch custom products with `Superwall.shared.products(for:)`; that method fetches App Store products.
- Do not rely on `sk1Product` or `sk2Product` for a custom product. Use `product.productIdentifier`.
- Do not wait until the next app launch to update `subscriptionStatus` after purchase.

## Testing

Test the full flow on a paywall that contains your Custom Store Product:

1. Confirm the product's price and trial copy render on the paywall.
2. Tap the product and verify your `PurchaseController` receives the product identifier.
3. Complete, cancel, fail, and mark a purchase pending in your external billing test environment.
4. Confirm your app updates `subscriptionStatus` after purchase and restore.
5. Confirm users who have already held the entitlement do not see a custom-product trial as available.

For general purchase-controller setup, see [Advanced Purchasing](/ios/guides/advanced-configuration).
Loading
Loading