npm package: @sftech/ng-revenue
Description: Angular library for RevenueCat subscription and entitlement management. Protects premium routes via a backend-verified guard, exposes subscription status, and wraps the RevenueCat SDK for purchase flows on both web and native (Capacitor) platforms.
Latest version: 0.2.1

# @sftech/ng-revenue

Angular library for RevenueCat subscription and entitlement management. Protects premium routes via a backend-verified guard, exposes subscription status, and wraps the RevenueCat SDK for purchase flows on both web and native (Capacitor) platforms.

## Installation

```bash
npm install @sftech/ng-revenue
```

### Optional: Purchase flows (P2)

Install the RevenueCat SDK that matches your deployment target. Neither package is required for route protection or status checks.

```bash
# Web (browser)
npm install @revenuecat/purchases-js

# Native (iOS / Android via Capacitor)
npm install @revenuecat/purchases-capacitor @capacitor/core
```

## Configuration

### 1. Add config keys to your environment config

```typescript
// environment.ts or config loaded via APP_INITIALIZER
export const appConfig = {
    apiUrl: 'https://api.example.com/api',
    REVENUECAT_PUBLIC_KEY: 'appl_xxxxxxxxxxxxxxxx',
    // Optional overrides (defaults shown):
    SUBSCRIPTION_API_URL: 'https://api.example.com/api/subscription', // defaults to apiUrl + '/subscription'
    REVENUECAT_ENTITLEMENT_ID: 'premium',                              // defaults to 'premium'
    REVENUECAT_UPGRADE_REDIRECT: '/upgrade',                           // defaults to '/upgrade'
};
```

`REVENUECAT_PUBLIC_KEY` is the **only required key**. The mapper throws a descriptive error if it is missing.

### 2. Register the module in app.config.ts

```typescript
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { RevenueModule, RevenueConfigMapper } from '@sftech/ng-revenue';

export function appConfig(config: Record<string, unknown>): ApplicationConfig {
    return {
        providers: [
            // ...other providers
            importProvidersFrom(
                RevenueModule.forRoot(RevenueConfigMapper.map(config)),
            ),
        ],
    };
}
```

`RevenueConfigMapper.map()` validates the config and applies defaults. Pass its return value directly to `RevenueModule.forRoot()`.

## Features

### Route Protection with `entitlementGuard`

Protects routes from users without an active subscription. The guard queries the backend on every navigation attempt and is fail-closed: any HTTP error redirects to the upgrade path instead of granting access.

```typescript
import { Routes } from '@angular/router';
import { entitlementGuard } from '@sftech/ng-revenue';

export const routes: Routes = [
    {
        path: 'premium-feature',
        loadComponent: () => import('./premium/premium.component').then((m) => m.PremiumComponent),
        canActivate: [entitlementGuard],
    },
];
```

The redirect target is `IRevenueConfig.upgradeRedirectPath` (mapped from `REVENUECAT_UPGRADE_REDIRECT`, default `'/upgrade'`).

### Subscription Status Checking with `SubscriptionApiService`

Query the backend subscription status directly in any component or service. Useful for conditional UI rendering without gating an entire route.

```typescript
import { Component, inject } from '@angular/core';
import { SubscriptionApiService, SubscriberStatus } from '@sftech/ng-revenue';

@Component({
    selector: 'app-profile',
    standalone: true,
    template: `<p>Premium: {{ status?.isPremium }}</p>`,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProfileComponent {
    private subscriptionApi = inject(SubscriptionApiService);

    status: SubscriberStatus | null = null;

    ngOnInit(): void {
        this.subscriptionApi.getEntitlementStatus().subscribe({
            next: (status) => {
                this.status = status;
                console.log('Premium:', status.isPremium);
                console.log('Expires:', status.expiresAt);
                console.log('Active subscriptions:', status.activeSubscriptions);
            },
        });
    }
}
```

`getEntitlementStatus()` never throws. When the backend is unreachable it returns `SubscriberStatus.free()` (all fields set to their zero/null values).

### Purchase Flows with `RevenueService` (optional SDK)

`RevenueService` wraps the RevenueCat SDK with automatic platform detection. It uses the Capacitor plugin on native platforms and the web SDK in a browser. Both paths share the same method signatures.

**Requires** one of the optional SDK packages listed in the [Installation](#installation) section.

```typescript
import { Component, inject } from '@angular/core';
import { RevenueService, Offering, Package } from '@sftech/ng-revenue';

@Component({
    selector: 'app-upgrade',
    standalone: true,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UpgradeComponent {
    private revenueService = inject(RevenueService);

    offerings: Offering[] = [];

    async ngOnInit(): Promise<void> {
        // Call initialize() once per session, passing the current user's ID
        await this.revenueService.initialize('user-123');
        this.offerings = await this.revenueService.getOfferings();
    }

    async purchase(pkg: Package): Promise<void> {
        const success = await this.revenueService.purchasePackage(pkg);
        if (success) {
            // Navigate to premium content
        }
    }

    async restore(): Promise<void> {
        await this.revenueService.restorePurchases();
    }

    async signOut(): Promise<void> {
        await this.revenueService.logout();
    }
}
```

`isInitialized` is a signal you can read to disable purchase buttons before `initialize()` completes:

```typescript
readonly isInitialized = this.revenueService.isInitialized;
```

`purchasePackage()` returns `false` (not throws) when the user cancels the purchase dialog.

## API Reference

### Services

| Symbol | Description |
|--------|-------------|
| `SubscriptionApiService` | Calls `GET {apiUrl}/status` and returns `Observable<SubscriberStatus>`. Returns `SubscriberStatus.free()` on error. |
| `RevenueService` | Wraps the RevenueCat SDK (web + native). Methods: `initialize(userId)`, `getOfferings()`, `purchasePackage(pkg)`, `restorePurchases()`, `logout()`. Signal: `isInitialized`. |

### Guards

| Symbol | Description |
|--------|-------------|
| `entitlementGuard` | `CanActivateFn` — allows navigation when `isPremium === true`, redirects to `upgradeRedirectPath` otherwise. Fail-closed. |

### Models

| Symbol | Description |
|--------|-------------|
| `SubscriberStatus` | User subscription state. Fields: `isPremium`, `entitlement`, `expiresAt` (`Date \| null`), `activeSubscriptions`. Factories: `fromDto()`, `free()`. |
| `Offering` | RevenueCat offering. Fields: `identifier`, `serverDescription`, `availablePackages`. Factory: `fromSdkOffering()`. |
| `Package` | RevenueCat package within an offering. Fields: `identifier`, `packageType`, `product`, `rcBillingProduct`. Factory: `fromSdkPackage()`. |
| `Product` | RevenueCat product. Fields: `identifier`, `title`, `description`, `priceString`, `price`, `currencyCode`. Factory: `fromSdkProduct()`. |

### Enums

| Symbol | Values | Description |
|--------|--------|-------------|
| `ERevenuePlatform` | `IOS`, `ANDROID`, `STRIPE`, `AMAZON`, `MACOS`, `PLAY_STORE` | Platform identifiers used by RevenueCat. |
| `ESubscriptionApiEndpoint` | `STATUS = '/status'` | Backend endpoint paths appended to `apiUrl`. |

### Configuration

| Symbol | Description |
|--------|-------------|
| `IRevenueConfig` | Config interface: `publicKey`, `apiUrl`, `entitlementId`, `upgradeRedirectPath`. |
| `RevenueConfigMapper` | Maps flat app-config keys to `IRevenueConfig`. Required key: `REVENUECAT_PUBLIC_KEY`. |
| `REVENUE_CONFIGURATION` | `InjectionToken<IRevenueConfig>` — inject to access config in custom services. |

### DTOs

| Symbol | Description |
|--------|-------------|
| `IApiResponse<TDto>` | Generic backend response wrapper: `status`, `messages?`, `data?`. |
| `ISubscriberStatusResponseDto` | Raw backend DTO: `isPremium`, `entitlement`, `expiresAt`, `activeSubscriptions`. |

### Module

| Symbol | Description |
|--------|-------------|
| `RevenueModule` | `NgModule` with `forRoot(config: IRevenueConfig): ModuleWithProviders<RevenueModule>`. Registers all services. |

## Peer Dependencies

| Package | Version | Required |
|---------|---------|----------|
| `@angular/common` | `>=20.0.0` | Yes |
| `@angular/core` | `>=20.0.0` | Yes |
| `@angular/router` | `>=20.0.0` | Yes |
| `@revenuecat/purchases-js` | `>=1.0.0` | No (web purchase flows only) |
| `@revenuecat/purchases-capacitor` | `>=12.0.0` | No (native purchase flows only) |
| `@capacitor/core` | `>=6.0.0` | No (required alongside purchases-capacitor) |

The three RevenueCat/Capacitor packages are optional. Route protection and status checks work without them.

## Backend Requirements

This library connects to a backend that implements the `@sftech/nestjs-revenue` API.

**Required Endpoint:**

- `GET /subscription/status` — Returns the current user's subscription state.

**Response Format:**

```typescript
{
    status: number;
    messages?: string[];
    data?: {
        isPremium: boolean;
        entitlement: string | null;
        expiresAt: string | null;       // ISO 8601 date string
        activeSubscriptions: string[];
    };
}
```

The endpoint is expected to be secured (authentication required). Token handling is delegated to `@sftech/ng-auth` — its `authenticationInterceptor` attaches the Bearer token to outgoing requests automatically.

## Changelog

See [CHANGELOG.md](./CHANGELOG.md) for a full history of changes.