# Ionic Skills

> Ionic Capacitor mobile app development with Angular, React, or Vue. RevenueCat payments, AdMob ads, i18n localization, onboarding flow, paywall, and Ionic Tabs navigation. Use when this capability is needed.

- Skill: `tomevault-io/ionic-skills` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/ionic-skills`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/ionic-skills/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/ionic-skills

---


# Ionic Capacitor Application Development Guide

> **IMPORTANT**: This is a SKILL file, NOT a project. NEVER run npm install in this folder. NEVER create code files here. When creating a new project, ALWAYS ask the user for the project path first or create it in a separate directory (e.g., `~/Projects/app-name`).

This guide covers building production-ready mobile apps with **Ionic Capacitor** using **Angular**, **React**, or **Vue**.

---

## MANDATORY REQUIREMENTS

When creating a new Ionic project, you MUST include ALL of the following:

### Required Pages (ALWAYS CREATE)

- [ ] Onboarding page - Swipe-based onboarding with fullscreen background video and gradient overlay
- [ ] Paywall page - RevenueCat paywall page (shown after onboarding)
- [ ] Settings page - Settings page with language, theme, notifications, and reset onboarding options

### Required Navigation (ALWAYS USE)

- [ ] Use `ion-tabs` with `ion-tab-bar` for tab navigation - NEVER use custom tab implementations or third-party tab libraries

### Required Libraries (ALWAYS INSTALL)

#### Angular

```bash
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob @ngx-translate/core @ngx-translate/http-loader swiper
```

#### React

```bash
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob react-i18next i18next i18next-http-backend swiper
```

#### Vue

```bash
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob vue-i18n swiper
```

### Shared Libraries (All Frameworks)

- `@revenuecat/purchases-capacitor` (RevenueCat)
- `@capacitor-community/admob` (AdMob)
- `@capacitor/push-notifications` (Push Notifications)
- `@capacitor/preferences` (Key-value storage)
- `@capacitor/splash-screen` (Splash screen control)
- `@capacitor/status-bar` (Status bar styling)
- `swiper` (Onboarding slides)

### Framework-Specific i18n Libraries

| Framework | Library | Usage |
|-----------|---------|-------|
| Angular | `@ngx-translate/core` + `@ngx-translate/http-loader` | `translate` pipe |
| React | `react-i18next` + `i18next` | `useTranslation()` hook |
| Vue | `vue-i18n` | `useI18n()` composable / `$t()` |

---

## FORBIDDEN (NEVER USE)

### All Frameworks

- ❌ `localStorage` directly - Use `@capacitor/preferences` instead
- ❌ `@ionic/storage` - Use `@capacitor/preferences` instead
- ❌ Custom tab bars - Use `ion-tabs` + `ion-tab-bar` instead
- ❌ `cordova-plugin-*` plugins - Use Capacitor plugins instead
- ❌ `any` type - Always use proper TypeScript types
- ❌ `ngx-admob-free` or other deprecated ad libraries - ONLY use `@capacitor-community/admob`
- ❌ Synchronous Capacitor calls - Always `await` Capacitor plugin methods

### Angular-Specific

- ❌ NgModules for new pages/components - Use standalone components
- ❌ `IonicModule` in standalone components - Import individual components (`IonButton`, `IonContent`, etc.)
- ❌ Inline `template` or `styles` in `@Component` - Use separate `.html`, `.ts`, `.scss` files with `templateUrl` and `styleUrls`
- ❌ `@angular/http` (deprecated) - Use `@angular/common/http`

### React-Specific

- ❌ Class components - Use functional components with hooks
- ❌ Direct DOM manipulation - Use React refs and state
- ❌ `@ionic/angular` imports - Use `@ionic/react`

### Vue-Specific

- ❌ Options API for new code - Use Composition API with `<script setup>`
- ❌ Direct DOM manipulation - Use Vue refs and reactivity
- ❌ `@ionic/angular` imports - Use `@ionic/vue`

---

## Technology Stack

| Concern | Angular | React | Vue |
|---------|---------|-------|-----|
| Framework | Ionic 8 + Angular 19 | Ionic 8 + React 19 | Ionic 8 + Vue 3.5 |
| Native Runtime | Capacitor 7 | Capacitor 7 | Capacitor 7 |
| Navigation | Angular Router (lazy-loaded) | @ionic/react-router | @ionic/vue-router |
| Tab Navigation | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar |
| State Management | Angular Services (Signals/RxJS) | Custom Hooks / Context | Composables / Pinia |
| Translations | @ngx-translate/core | react-i18next | vue-i18n |
| Purchases | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor |
| Ads | @capacitor-community/admob | @capacitor-community/admob | @capacitor-community/admob |
| Notifications | @capacitor/push-notifications | @capacitor/push-notifications | @capacitor/push-notifications |
| Storage | @capacitor/preferences | @capacitor/preferences | @capacitor/preferences |

> **WARNING**: DO NOT USE `localStorage` directly! Use `@capacitor/preferences` instead for cross-platform persistent storage.

---

## Project Creation

When user asks to create an app, you MUST:

1. FIRST ask for the bundle ID (e.g., "What is the bundle ID? Example: com.company.appname")
2. Create the project in the CURRENT directory
3. Then implement all required pages

### Creating a Project (Angular)

```bash
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitor
```

### Creating a Project (React)

```bash
npm install -g @ionic/cli
ionic start app-name blank --type=react --capacitor
```

### Creating a Project (Vue)

```bash
npm install -g @ionic/cli
ionic start app-name blank --type=vue --capacitor
```

### Capacitor Configuration (All Frameworks)

```typescript
// capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.company.appname',
  appName: 'App Name',
  webDir: 'www',
  server: {
    androidScheme: 'https',
  },
};

export default config;
```

> **Note**: For React/Vue projects, `webDir` may be `'dist'` instead of `'www'`. Check your project's build output directory.

### Add Native Platforms

```bash
npx cap add ios
npx cap add android
```

---

## Project Structure

### Angular Project Structure

```
project-root/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.component.html
│   │   ├── app.config.ts
│   │   ├── app.routes.ts
│   │   ├── tabs/
│   │   │   ├── tabs.page.ts
│   │   │   ├── tabs.page.html
│   │   │   ├── tabs.page.scss
│   │   │   └── tabs.routes.ts
│   │   ├── home/
│   │   │   ├── home.page.ts
│   │   │   ├── home.page.html
│   │   │   └── home.page.scss
│   │   ├── explore/
│   │   │   ├── explore.page.ts
│   │   │   ├── explore.page.html
│   │   │   └── explore.page.scss
│   │   ├── settings/
│   │   │   ├── settings.page.ts
│   │   │   ├── settings.page.html
│   │   │   └── settings.page.scss
│   │   ├── paywall/
│   │   │   ├── paywall.page.ts
│   │   │   ├── paywall.page.html
│   │   │   └── paywall.page.scss
│   │   ├── onboarding/
│   │   │   ├── onboarding.page.ts
│   │   │   ├── onboarding.page.html
│   │   │   └── onboarding.page.scss
│   │   ├── services/
│   │   │   ├── theme.service.ts
│   │   │   ├── onboarding.service.ts
│   │   │   ├── ads.service.ts
│   │   │   ├── purchases.service.ts
│   │   │   └── notifications.service.ts
│   │   ├── guards/
│   │   │   └── onboarding.guard.ts
│   │   └── utils/
│   │       ├── admob.ts
│   │       ├── purchases.ts
│   │       ├── onboarding.ts
│   │       ├── theme.ts
│   │       └── notifications.ts
│   ├── assets/
│   │   └── i18n/
│   │       ├── en.json
│   │       └── tr.json
│   ├── theme/
│   │   └── variables.scss
│   ├── global.scss
│   ├── index.html
│   └── main.ts
├── ios/
├── android/
├── capacitor.config.ts
├── angular.json
├── package.json
└── tsconfig.json
```

### React Project Structure

```
project-root/
├── src/
│   ├── App.tsx
│   ├── main.tsx
│   ├── pages/
│   │   ├── OnboardingPage.tsx
│   │   ├── PaywallPage.tsx
│   │   ├── SettingsPage.tsx
│   │   ├── HomePage.tsx
│   │   └── ExplorePage.tsx
│   ├── components/
│   │   ├── TabsLayout.tsx
│   │   └── OnboardingGuard.tsx
│   ├── hooks/
│   │   ├── useTheme.ts
│   │   ├── useOnboarding.ts
│   │   ├── useAds.ts
│   │   ├── usePurchases.ts
│   │   └── useNotifications.ts
│   ├── utils/
│   │   ├── admob.ts
│   │   ├── purchases.ts
│   │   ├── onboarding.ts
│   │   ├── theme.ts
│   │   └── notifications.ts
│   ├── theme/
│   │   └── variables.css
│   └── i18n/
│       ├── index.ts
│       ├── en.json
│       └── tr.json
├── public/
├── ios/
├── android/
├── capacitor.config.ts
├── package.json
└── tsconfig.json
```

### Vue Project Structure

```
project-root/
├── src/
│   ├── App.vue
│   ├── main.ts
│   ├── router/
│   │   └── index.ts
│   ├── views/
│   │   ├── OnboardingPage.vue
│   │   ├── PaywallPage.vue
│   │   ├── SettingsPage.vue
│   │   ├── HomePage.vue
│   │   ├── ExplorePage.vue
│   │   └── TabsLayout.vue
│   ├── composables/
│   │   ├── useTheme.ts
│   │   ├── useOnboarding.ts
│   │   ├── useAds.ts
│   │   ├── usePurchases.ts
│   │   └── useNotifications.ts
│   ├── utils/
│   │   ├── admob.ts
│   │   ├── purchases.ts
│   │   ├── onboarding.ts
│   │   ├── theme.ts
│   │   └── notifications.ts
│   ├── theme/
│   │   └── variables.css
│   └── assets/
│       └── i18n/
│           ├── en.json
│           └── tr.json
├── ios/
├── android/
├── capacitor.config.ts
├── package.json
└── tsconfig.json
```

---

## App Configuration

### App Configuration (Angular)

```typescript
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, RouteReuseStrategy } from '@angular/router';
import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular/standalone';
import { provideHttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { routes } from './app.routes';

export function createTranslateLoader(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideIonicAngular({ mode: 'md' }),
    provideHttpClient(),
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    importProvidersFrom(
      TranslateModule.forRoot({
        defaultLanguage: 'en',
        loader: {
          provide: TranslateLoader,
          useFactory: createTranslateLoader,
          deps: [HttpClient],
        },
      })
    ),
  ],
};
```

```html
<!-- app.component.html -->
<ion-app>
  <ion-router-outlet></ion-router-outlet>
</ion-app>
```

```typescript
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { IonApp, IonRouterOutlet } from '@ionic/angular/standalone';
import { AdsService } from './services/ads.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [IonApp, IonRouterOutlet],
  templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
  constructor(private adsService: AdsService) {}

  async ngOnInit() {
    await this.adsService.initialize();
  }
}
```

### App Configuration (React)

```tsx
// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

/* Core CSS required for Ionic components */
import '@ionic/react/css/core.css';
import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';
import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';

import './theme/variables.css';
import './i18n';

const root = createRoot(document.getElementById('root')!);
root.render(<App />);
```

```tsx
// App.tsx
import { useEffect } from 'react';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { Route, Redirect } from 'react-router-dom';
import OnboardingPage from './pages/OnboardingPage';
import PaywallPage from './pages/PaywallPage';
import TabsLayout from './components/TabsLayout';
import { OnboardingGuard } from './components/OnboardingGuard';
import { initializeAdMob } from './utils/admob';

setupIonicReact({ mode: 'md' });

const App: React.FC = () => {
  useEffect(() => {
    initializeAdMob();
  }, []);

  return (
    <IonApp>
      <IonReactRouter>
        <IonRouterOutlet>
          <Route exact path="/onboarding" component={OnboardingPage} />
          <Route exact path="/paywall" component={PaywallPage} />
          <Route path="/tabs">
            <OnboardingGuard>
              <TabsLayout />
            </OnboardingGuard>
          </Route>
          <Route exact path="/">
            <Redirect to="/tabs" />
          </Route>
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  );
};

export default App;
```

```typescript
// i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './en.json';
import tr from './tr.json';

const browserLang = navigator.language.split('-')[0];

i18n.use(initReactI18next).init({
  resources: {
    en: { translation: en },
    tr: { translation: tr },
  },
  lng: ['en', 'tr'].includes(browserLang) ? browserLang : 'en',
  fallbackLng: 'en',
  interpolation: { escapeValue: false },
});

export default i18n;
```

### App Configuration (Vue)

```typescript
// main.ts
import { createApp } from 'vue';
import { IonicVue } from '@ionic/vue';
import App from './App.vue';
import router from './router';
import { createI18n } from 'vue-i18n';
import en from './assets/i18n/en.json';
import tr from './assets/i18n/tr.json';

/* Core CSS required for Ionic components */
import '@ionic/vue/css/core.css';
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import '@ionic/vue/css/display.css';

import './theme/variables.css';

const browserLang = navigator.language.split('-')[0];

const i18n = createI18n({
  legacy: false,
  locale: ['en', 'tr'].includes(browserLang) ? browserLang : 'en',
  fallbackLocale: 'en',
  messages: { en, tr },
});

const app = createApp(App);
app.use(IonicVue, { mode: 'md' });
app.use(router);
app.use(i18n);
router.isReady().then(() => app.mount('#app'));
```

```vue
<!-- App.vue -->
<template>
  <ion-app>
    <ion-router-outlet />
  </ion-app>
</template>

<script setup lang="ts">
import { IonApp, IonRouterOutlet } from '@ionic/vue';
import { onMounted } from 'vue';
import { initializeAdMob } from './utils/admob';

onMounted(async () => {
  await initializeAdMob();
});
</script>
```

---

## Routing

### Routing (Angular)

```typescript
// app.routes.ts
import { Routes } from '@angular/router';
import { onboardingGuard } from './guards/onboarding.guard';

export const routes: Routes = [
  {
    path: 'onboarding',
    loadComponent: () => import('./onboarding/onboarding.page').then(m => m.OnboardingPage),
  },
  {
    path: 'paywall',
    loadComponent: () => import('./paywall/paywall.page').then(m => m.PaywallPage),
  },
  {
    path: 'tabs',
    loadChildren: () => import('./tabs/tabs.routes').then(m => m.tabsRoutes),
    canActivate: [onboardingGuard],
  },
  {
    path: '',
    redirectTo: 'tabs',
    pathMatch: 'full',
  },
];
```

```typescript
// tabs/tabs.routes.ts
import { Routes } from '@angular/router';
import { TabsPage } from './tabs.page';

export const tabsRoutes: Routes = [
  {
    path: '',
    component: TabsPage,
    children: [
      {
        path: 'home',
        loadComponent: () => import('../home/home.page').then(m => m.HomePage),
      },
      {
        path: 'explore',
        loadComponent: () => import('../explore/explore.page').then(m => m.ExplorePage),
      },
      {
        path: 'settings',
        loadComponent: () => import('../settings/settings.page').then(m => m.SettingsPage),
      },
      {
        path: '',
        redirectTo: 'home',
        pathMatch: 'full',
      },
    ],
  },
];
```

### Routing (React)

Routes are defined in `App.tsx` (see App Configuration above). Tab routes are defined in the `TabsLayout` component:

```tsx
// components/TabsLayout.tsx
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
import { Route, Redirect } from 'react-router-dom';
import { home, compass, settings } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import { useEffect } from 'react';
import HomePage from '../pages/HomePage';
import ExplorePage from '../pages/ExplorePage';
import SettingsPage from '../pages/SettingsPage';
import { showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';

const TabsLayout: React.FC = () => {
  const { t } = useTranslation();

  useEffect(() => {
    isPremiumUser().then((premium) => {
      if (!premium) showBannerAd();
    });
    return () => { hideBannerAd(); };
  }, []);

  return (
    <IonTabs>
      <IonRouterOutlet>
        <Route exact path="/tabs/home" component={HomePage} />
        <Route exact path="/tabs/explore" component={ExplorePage} />
        <Route exact path="/tabs/settings" component={SettingsPage} />
        <Route exact path="/tabs">
          <Redirect to="/tabs/home" />
        </Route>
      </IonRouterOutlet>
      <IonTabBar slot="bottom">
        <IonTabButton tab="home" href="/tabs/home">
          <IonIcon icon={home} />
          <IonLabel>{t('tabs.home')}</IonLabel>
        </IonTabButton>
        <IonTabButton tab="explore" href="/tabs/explore">
          <IonIcon icon={compass} />
          <IonLabel>{t('tabs.explore')}</IonLabel>
        </IonTabButton>
        <IonTabButton tab="settings" href="/tabs/settings">
          <IonIcon icon={settings} />
          <IonLabel>{t('tabs.settings')}</IonLabel>
        </IonTabButton>
      </IonTabBar>
    </IonTabs>
  );
};

export default TabsLayout;
```

### Routing (Vue)

```typescript
// router/index.ts
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import { isOnboardingCompleted } from '../utils/onboarding';
import TabsLayout from '../views/TabsLayout.vue';

const routes: RouteRecordRaw[] = [
  {
    path: '/onboarding',
    component: () => import('../views/OnboardingPage.vue'),
  },
  {
    path: '/paywall',
    component: () => import('../views/PaywallPage.vue'),
  },
  {
    path: '/tabs/',
    component: TabsLayout,
    children: [
      {
        path: '',
        redirect: '/tabs/home',
      },
      {
        path: 'home',
        component: () => import('../views/HomePage.vue'),
      },
      {
        path: 'explore',
        component: () => import('../views/ExplorePage.vue'),
      },
      {
        path: 'settings',
        component: () => import('../views/SettingsPage.vue'),
      },
    ],
  },
  {
    path: '/',
    redirect: '/tabs/',
  },
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
});

router.beforeEach(async (to, _from, next) => {
  if (to.path.startsWith('/tabs') || to.path === '/') {
    const completed = await isOnboardingCompleted();
    if (!completed) {
      return next('/onboarding');
    }
  }
  next();
});

export default router;
```

```vue
<!-- views/TabsLayout.vue -->
<template>
  <ion-page>
    <ion-tabs>
      <ion-router-outlet />
      <ion-tab-bar slot="bottom">
        <ion-tab-button tab="home" href="/tabs/home">
          <ion-icon :icon="home" />
          <ion-label>{{ t('tabs.home') }}</ion-label>
        </ion-tab-button>
        <ion-tab-button tab="explore" href="/tabs/explore">
          <ion-icon :icon="compass" />
          <ion-label>{{ t('tabs.explore') }}</ion-label>
        </ion-tab-button>
        <ion-tab-button tab="settings" href="/tabs/settings">
          <ion-icon :icon="settings" />
          <ion-label>{{ t('tabs.settings') }}</ion-label>
        </ion-tab-button>
      </ion-tab-bar>
    </ion-tabs>
  </ion-page>
</template>

<script setup lang="ts">
import {
  IonPage, IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel,
} from '@ionic/vue';
import { home, compass, settings } from 'ionicons/icons';
import { useI18n } from 'vue-i18n';
import { onMounted, onUnmounted } from 'vue';
import { showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';

const { t } = useI18n();

onMounted(async () => {
  const premium = await isPremiumUser();
  if (!premium) await showBannerAd();
});

onUnmounted(async () => {
  await hideBannerAd();
});
</script>
```

---

## Shared Utility Functions (Framework-Agnostic)

These utility files contain pure TypeScript with Capacitor plugin calls. They are used by all frameworks. Each framework wraps them in its own pattern (Angular services, React hooks, Vue composables).

### Storage (All Frameworks)

```typescript
import { Preferences } from '@capacitor/preferences';

// Set a value
await Preferences.set({ key: 'onboardingCompleted', value: 'true' });

// Get a value
const { value } = await Preferences.get({ key: 'onboardingCompleted' });
console.log(value); // 'true'

// Remove a value
await Preferences.remove({ key: 'onboardingCompleted' });
```

### Onboarding State Utility

```typescript
// utils/onboarding.ts
import { Preferences } from '@capacitor/preferences';

const KEY = 'onboardingCompleted';

export async function isOnboardingCompleted(): Promise<boolean> {
  const { value } = await Preferences.get({ key: KEY });
  return value === 'true';
}

export async function setOnboardingCompleted(completed: boolean): Promise<void> {
  await Preferences.set({ key: KEY, value: String(completed) });
}

export async function resetOnboarding(): Promise<void> {
  await Preferences.remove({ key: KEY });
}
```

### Theme Utility

```typescript
// utils/theme.ts
import { Preferences } from '@capacitor/preferences';

export type ThemeMode = 'light' | 'dark' | 'system';

const KEY = 'themeMode';

export async function getTheme(): Promise<ThemeMode> {
  const { value } = await Preferences.get({ key: KEY });
  return (value as ThemeMode) || 'system';
}

export async function setTheme(mode: ThemeMode): Promise<void> {
  await Preferences.set({ key: KEY, value: mode });
  applyTheme(mode);
}

export function applyTheme(mode: ThemeMode): void {
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const isDark = mode === 'dark' || (mode === 'system' && prefersDark);
  document.documentElement.classList.toggle('ion-palette-dark', isDark);
}
```

### AdMob Utility

```typescript
// utils/admob.ts
import { AdMob, BannerAdOptions, BannerAdSize, BannerAdPosition } from '@capacitor-community/admob';
import { Capacitor } from '@capacitor/core';

let initialized = false;

export async function initializeAdMob(): Promise<void> {
  if (!Capacitor.isNativePlatform()) return;

  await AdMob.initialize({
    initializeForTesting: true, // Set to false in production
  });
  initialized = true;
}

export async function showBannerAd(): Promise<void> {
  if (!initialized) return;

  const options: BannerAdOptions = {
    adId: 'ca-app-pub-3940256099942544/6300978111', // Test ID - replace in production
    adSize: BannerAdSize.ADAPTIVE_BANNER,
    position: BannerAdPosition.BOTTOM_CENTER,
    isTesting: true, // Set to false in production
  };

  await AdMob.showBanner(options);
}

export async function hideBannerAd(): Promise<void> {
  if (!initialized) return;
  await AdMob.hideBanner();
}
```

For development/testing, use test Ad IDs:

- Banner: `ca-app-pub-3940256099942544/6300978111`

Do NOT skip AdMob initialization or the plugin will not work correctly.

### RevenueCat Utility

```typescript
// utils/purchases.ts
import { Capacitor } from '@capacitor/core';
import { Purchases, LOG_LEVEL, PURCHASES_ERROR_CODE, PurchasesPackage } from '@revenuecat/purchases-capacitor';

let purchasesInitialized = false;

export async function initializePurchases(): Promise<void> {
  if (!Capacitor.isNativePlatform()) return;

  await Purchases.setLogLevel({ level: LOG_LEVEL.DEBUG }); // Use WARN in production

  const apiKey = Capacitor.getPlatform() === 'ios'
    ? 'appl_YOUR_IOS_API_KEY'
    : 'goog_YOUR_ANDROID_API_KEY';

  await Purchases.configure({ apiKey });
  purchasesInitialized = true;
}

export async function isPremiumUser(): Promise<boolean> {
  if (!purchasesInitialized) return false;
  try {
    const { customerInfo } = await Purchases.getCustomerInfo();
    return Object.keys(customerInfo.entitlements.active).length > 0;
  } catch {
    return false;
  }
}

export async function getOfferings(): Promise<PurchasesPackage[]> {
  if (!purchasesInitialized) return [];
  try {
    const { offerings } = await Purchases.getOfferings();
    return offerings?.current?.availablePackages ?? [];
  } catch {
    return [];
  }
}

export async function purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
  try {
    const { customerInfo } = await Purchases.purchasePackage({ aPackage: pkg });
    return Object.keys(customerInfo.entitlements.active).length > 0;
  } catch (error: unknown) {
    if ((error as { code?: string })?.code === PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
      return false;
    }
    throw error;
  }
}

export async function restorePurchases(): Promise<boolean> {
  try {
    const { customerInfo } = await Purchases.restorePurchases();
    return Object.keys(customerInfo.entitlements.active).length > 0;
  } catch {
    return false;
  }
}
```

### Notifications Utility

```typescript
// utils/notifications.ts
import { Capacitor } from '@capacitor/core';
import { PushNotifications } from '@capacitor/push-notifications';

export async function requestNotificationPermission(): Promise<boolean> {
  if (!Capacitor.isNativePlatform()) return false;

  const result = await PushNotifications.requestPermissions();
  if (result.receive === 'granted') {
    await PushNotifications.register();
    return true;
  }
  return false;
}

export async function addNotificationListeners(): Promise<void> {
  await PushNotifications.addListener('registration', (token) => {
    console.log('Push registration success, token:', token.value);
  });

  await PushNotifications.addListener('registrationError', (error) => {
    console.error('Push registration error:', error);
  });

  await PushNotifications.addListener('pushNotificationReceived', (notification) => {
    console.log('Push notification received:', notification);
  });

  await PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
    console.log('Push notification action:', action);
  });
}
```

---

## Framework-Specific Service Wrappers

### Angular Services

```typescript
// services/onboarding.service.ts
import { Injectable } from '@angular/core';
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';

@Injectable({ providedIn: 'root' })
export class OnboardingService {
  isCompleted = isOnboardingCompleted;
  setCompleted = setOnboardingCompleted;
  reset = resetOnboarding;
}
```

```typescript
// services/theme.service.ts
import { Injectable } from '@angular/core';
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  async initialize(): Promise<void> {
    const theme = await getTheme();
    applyTheme(theme);
  }

  getTheme = getTheme;
  setTheme = setTheme;
}
```

```typescript
// services/ads.service.ts
import { Injectable } from '@angular/core';
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';

@Injectable({ providedIn: 'root' })
export class AdsService {
  async initialize(): Promise<void> {
    await initializeAdMob();
  }

  async showBanner(): Promise<void> {
    if (await isPremiumUser()) return;
    await showBannerAd();
  }

  async hideBanner(): Promise<void> {
    await hideBannerAd();
  }
}
```

```typescript
// services/purchases.service.ts
import { Injectable } from '@angular/core';
import {
  initializePurchases,
  isPremiumUser,
  getOfferings,
  purchasePackage,
  restorePurchases,
} from '../utils/purchases';

@Injectable({ providedIn: 'root' })
export class PurchasesService {
  initialize = initializePurchases;
  isPremium = isPremiumUser;
  getOfferings = getOfferings;
  purchase = purchasePackage;
  restorePurchases = restorePurchases;
}
```

```typescript
// services/notifications.service.ts
import { Injectable } from '@angular/core';
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';

@Injectable({ providedIn: 'root' })
export class NotificationsService {
  requestPermission = requestNotificationPermission;
  addListeners = addNotificationListeners;
}
```

### React Hooks

```typescript
// hooks/useOnboarding.ts
import { useCallback } from 'react';
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';

export function useOnboarding() {
  const isCompleted = useCallback(() => isOnboardingCompleted(), []);
  const setCompleted = useCallback((v: boolean) => setOnboardingCompleted(v), []);
  const reset = useCallback(() => resetOnboarding(), []);
  return { isCompleted, setCompleted, reset };
}
```

```typescript
// hooks/useTheme.ts
import { useCallback } from 'react';
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';

export function useTheme() {
  const initialize = useCallback(async () => {
    const theme = await getTheme();
    applyTheme(theme);
  }, []);

  return {
    initialize,
    getTheme: useCallback(() => getTheme(), []),
    setTheme: useCallback((mode: ThemeMode) => setTheme(mode), []),
  };
}
```

```typescript
// hooks/useAds.ts
import { useCallback } from 'react';
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';

export function useAds() {
  const initialize = useCallback(() => initializeAdMob(), []);
  const showBanner = useCallback(async () => {
    if (await isPremiumUser()) return;
    await showBannerAd();
  }, []);
  const hideBanner = useCallback(() => hideBannerAd(), []);
  return { initialize, showBanner, hideBanner };
}
```

```typescript
// hooks/usePurchases.ts
import { useCallback } from 'react';
import {
  initializePurchases,
  isPremiumUser,
  getOfferings,
  purchasePackage,
  restorePurchases,
} from '../utils/purchases';
import { PurchasesPackage } from '@revenuecat/purchases-capacitor';

export function usePurchases() {
  return {
    initialize: useCallback(() => initializePurchases(), []),
    isPremium: useCallback(() => isPremiumUser(), []),
    getOfferings: useCallback(() => getOfferings(), []),
    purchase: useCallback((pkg: PurchasesPackage) => purchasePackage(pkg), []),
    restorePurchases: useCallback(() => restorePurchases(), []),
  };
}
```

```typescript
// hooks/useNotifications.ts
import { useCallback } from 'react';
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';

export function useNotifications() {
  return {
    requestPermission: useCallback(() => requestNotificationPermission(), []),
    addListeners: useCallback(() => addNotificationListeners(), []),
  };
}
```

### Vue Composables

```typescript
// composables/useOnboarding.ts
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';

export function useOnboarding() {
  return {
    isCompleted: isOnboardingCompleted,
    setCompleted: setOnboardingCompleted,
    reset: resetOnboarding,
  };
}
```

```typescript
// composables/useTheme.ts
import { getTheme, setTheme, applyTheme, ThemeMode } from '../utils/theme';

export function useTheme() {
  const initialize = async () => {
    const theme = await getTheme();
    applyTheme(theme);
  };

  return { initialize, getTheme, setTheme };
}
```

```typescript
// composables/useAds.ts
import { initializeAdMob, showBannerAd, hideBannerAd } from '../utils/admob';
import { isPremiumUser } from '../utils/purchases';

export function useAds() {
  const initialize = () => initializeAdMob();
  const showBanner = async () => {
    if (await isPremiumUser()) return;
    await showBannerAd();
  };
  const hideBanner = () => hideBannerAd();
  return { initialize, showBanner, hideBanner };
}
```

```typescript
// composables/usePurchases.ts
import {
  initializePurchases,
  isPremiumUser,
  getOfferings,
  purchasePackage,
  restorePurchases,
} from '../utils/purchases';

export function usePurchases() {
  return {
    initialize: initializePurchases,
    isPremium: isPremiumUser,
    getOfferings,
    purchase: purchasePackage,
    restorePurchases,
  };
}
```

```typescript
// composables/useNotifications.ts
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';

export function useNotifications() {
  return {
    requestPermission: requestNotificationPermission,
    addListeners: addNotificationListeners,
  };
}
```

---

## Onboarding Guard

### Onboarding Guard (Angular)

```typescript
// guards/onboarding.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { isOnboardingCompleted } from '../utils/onboarding';

export const onboardingGuard: CanActivateFn = async () => {
  const router = inject(Router);

  const completed = await isOnboardingCompleted();

  if (!completed) {
    router.navigateByUrl('/onboarding', { replaceUrl: true });
    return false;
  }

  return true;
};
```

### Onboarding Guard (React)

```tsx
// components/OnboardingGuard.tsx
import { useEffect, useState } from 'react';
import { useIonRouter } from '@ionic/react';
import { isOnboardingCompleted } from '../utils/onboarding';

export const OnboardingGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const router = useIonRouter();
  const [checked, setChecked] = useState(false);

  useEffect(() => {
    isOnboardingCompleted().then((completed) => {
      if (!completed) {
        router.push('/onboarding', 'forward', 'replace');
      } else {
        setChecked(true);
      }
    });
  }, []);

  return checked ? <>{children}</> : null;
};
```

### Onboarding Guard (Vue)

The Vue onboarding guard is implemented as a `router.beforeEach` hook. See the Routing (Vue) section above.

---

## Onboarding Page

### Shared Video CSS (All Frameworks)

```css
.background-video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
.gradient-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.7));
  z-index: 1;
}
.onboarding-slides {
  position: relative;
  z-index: 2;
  height: 100%;
}
```

Do NOT just reference the video without actually rendering the `<video>` element. Use native HTML5 `<video>` - NOT canvas, NOT animated GIFs, NOT external players.

### Onboarding Page (Angular)

```html
<!-- onboarding/onboarding.page.html -->
<ion-content [fullscreen]="true" class="onboarding-content">
  <video
    #bgVideo
    [src]="videoUrl"
    autoplay
    loop
    muted
    playsinline
    class="background-video"
  ></video>
  <div class="gradient-overlay"></div>
  <div class="onboarding-slides">
    <!-- Swiper slides content here -->
  </div>
</ion-content>
```

```scss
// onboarding/onboarding.page.scss
.background-video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

.gradient-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7));
  z-index: 1;
}

.onboarding-slides {
  position: relative;
  z-index: 2;
  height: 100%;
}
```

```typescript
// onboarding/onboarding.page.ts
import { Component } from '@angular/core';
import { IonContent, IonButton, IonIcon } from '@ionic/angular/standalone';
import { Router } from '@angular/router';
import { OnboardingService } from '../services/onboarding.service';

const VIDEO_URL =
  'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';

@Component({
  selector: 'app-onboarding',
  standalone: true,
  imports: [IonContent, IonButton, IonIcon],
  templateUrl: './onboarding.page.html',
  styleUrls: ['./onboarding.page.scss'],
})
export class OnboardingPage {
  videoUrl = VIDEO_URL;

  constructor(
    private router: Router,
    private onboardingService: OnboardingService
  ) {}

  async completeOnboarding() {
    await this.onboardingService.setCompleted(true);
    this.router.navigateByUrl('/paywall', { replaceUrl: true });
  }
}
```

### Onboarding Page (React)

```tsx
// pages/OnboardingPage.tsx
import { IonContent, IonButton, IonIcon, useIonRouter } from '@ionic/react';
import { useOnboarding } from '../hooks/useOnboarding';
import './OnboardingPage.css';

const VIDEO_URL =
  'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';

const OnboardingPage: React.FC = () => {
  const router = useIonRouter();
  const { setCompleted } = useOnboarding();

  const completeOnboarding = async () => {
    await setCompleted(true);
    router.push('/paywall', 'forward', 'replace');
  };

  return (
    <IonContent fullscreen className="onboarding-content">
      <video
        src={VIDEO_URL}
        autoPlay
        loop
        muted
        playsInline
        className="background-video"
      />
      <div className="gradient-overlay" />
      <div className="onboarding-slides">
        {/* Swiper slides content here */}
      </div>
    </IonContent>
  );
};

export default OnboardingPage;
```

### Onboarding Page (Vue)

```vue
<!-- views/OnboardingPage.vue -->
<template>
  <ion-content :fullscreen="true" class="onboarding-content">
    <video
      :src="videoUrl"
      autoplay
      loop
      muted
      playsinline
      class="background-video"
    />
    <div class="gradient-overlay" />
    <div class="onboarding-slides">
      <!-- Swiper slides content here -->
    </div>
  </ion-content>
</template>

<script setup lang="ts">
import { IonContent, IonButton, IonIcon } from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useOnboarding } from '../composables/useOnboarding';

const VIDEO_URL =
  'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';

const videoUrl = VIDEO_URL;
const router = useRouter();
const { setCompleted } = useOnboarding();

async function completeOnboarding() {
  await setCompleted(true);
  router.replace('/paywall');
}
</script>

<style scoped>
.background-video {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  z-index: 0;
}
.gradient-overlay {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background: linear-gradient(to bottom, rgba(0,0,0,0.3), rgba(0,0,0,0.7));
  z-index: 1;
}
.onboarding-slides {
  position: relative;
  z-index: 2;
  height: 100%;
}
</style>
```

---

## Paywall Page

**IMPORTANT**: Paywall MUST appear immediately after onboarding completes.

Paywall MUST have two subscription options:

1. **Weekly** - Default option
2. **Yearly** - With "50% OFF" badge (recommended, should be highlighted)

Three buttons: Subscribe, Continue with ads, Restore Purchases.

Flow: `Onboarding ->

…(truncated)
