ionic-skills

📁 erkamyaman/ionic-skills 📅 Feb 8, 2026
2
总安装量
2
周安装量
#75804
全站排名
安装命令
npx skills add https://github.com/erkamyaman/ionic-skills --skill ionic-skills

Agent 安装分布

junie 2
cursor 2
claude-code 2
antigravity 2
gemini-cli 2
replit 1

Skill 文档

Ionic Capacitor Angular 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 is created to provide context when working with Ionic Capacitor Angular projects using Claude Code.

MANDATORY REQUIREMENTS

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

Required Pages (ALWAYS CREATE)

  • src/app/onboarding/onboarding.page.ts – Swipe-based onboarding with fullscreen background video and gradient overlay
  • src/app/paywall/paywall.page.ts – RevenueCat paywall page (shown after onboarding)
  • src/app/settings/settings.page.ts – Settings page with language, theme, notifications, and reset onboarding options

Onboarding Video Implementation (REQUIRED)

The onboarding page MUST have a fullscreen background video. Use a native HTML5 <video> element:

// onboarding.page.ts
import { Component, OnInit } 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],
  template: `
    <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>
  `,
  styles: [`
    .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%;
    }
  `]
})
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 });
  }
}

Do NOT just reference the video without actually rendering the <video> element.

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 App Configuration (ALWAYS SET UP)

// 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],
        },
      })
    ),
  ],
};
// 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',
  },
];

Required Libraries (ALWAYS INSTALL)

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

Libraries:

  • @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)
  • @ngx-translate/core + @ngx-translate/http-loader (i18n)
  • swiper (Onboarding slides)

AdMob Configuration (REQUIRED)

You MUST initialize AdMob in the app component:

// 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],
  template: `
    <ion-app>
      <ion-router-outlet></ion-router-outlet>
    </ion-app>
  `,
})
export class AppComponent implements OnInit {
  constructor(private adsService: AdsService) {}

  async ngOnInit() {
    await this.adsService.initialize();
  }
}
// services/ads.service.ts
import { Injectable } from '@angular/core';
import { AdMob, BannerAdOptions, BannerAdSize, BannerAdPosition } from '@capacitor-community/admob';
import { Capacitor } from '@capacitor/core';
import { PurchasesService } from './purchases.service';

@Injectable({ providedIn: 'root' })
export class AdsService {
  private initialized = false;

  constructor(private purchasesService: PurchasesService) {}

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

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

  async showBanner(): Promise<void> {
    if (!this.initialized) return;

    const isPremium = await this.purchasesService.isPremium();
    if (isPremium) 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);
  }

  async hideBanner(): Promise<void> {
    if (!this.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.

Banner Ad Implementation (REQUIRED)

You MUST show banner ads in the tabs layout. Use the AdsService:

// tabs/tabs.page.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { home, compass, settings } from 'ionicons/icons';
import { TranslateModule } from '@ngx-translate/core';
import { AdsService } from '../services/ads.service';

@Component({
  selector: 'app-tabs',
  standalone: true,
  imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, TranslateModule],
  template: `
    <ion-tabs>
      <ion-tab-bar slot="bottom">
        <ion-tab-button tab="home">
          <ion-icon name="home"></ion-icon>
          <ion-label>{{ 'tabs.home' | translate }}</ion-label>
        </ion-tab-button>

        <ion-tab-button tab="explore">
          <ion-icon name="compass"></ion-icon>
          <ion-label>{{ 'tabs.explore' | translate }}</ion-label>
        </ion-tab-button>

        <ion-tab-button tab="settings">
          <ion-icon name="settings"></ion-icon>
          <ion-label>{{ 'tabs.settings' | translate }}</ion-label>
        </ion-tab-button>
      </ion-tab-bar>
    </ion-tabs>
  `,
})
export class TabsPage implements OnInit, OnDestroy {
  constructor(private adsService: AdsService) {
    addIcons({ home, compass, settings });
  }

  async ngOnInit() {
    await this.adsService.showBanner();
  }

  async ngOnDestroy() {
    await this.adsService.hideBanner();
  }
}
// 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',
      },
    ],
  },
];
  • Banner ad is managed via @capacitor-community/admob native overlay
  • Use AdsService to show/hide banners based on premium status

TURKISH LOCALIZATION (IMPORTANT)

When writing tr.json, you MUST use correct Turkish characters:

  • ı (lowercase dotless i) – NOT i
  • İ (uppercase dotted I) – NOT I
  • ü, Ü, ö, Ö, ç, Ç, ş, Ş, ğ, Ğ

Example:

  • ✅ “Ayarlar”, “Giriş”, “Çıkış”, “Başla”, “İleri”, “Güncelle”
  • ❌ “Ayarlar”, “Giris”, “Cikis”, “Basla”, “Ileri”, “Guncelle”

FORBIDDEN (NEVER USE)

  • ❌ localStorage directly – Use @capacitor/preferences instead
  • ❌ NgModules for new pages/components – Use standalone components instead
  • ❌ @ionic/storage – Use @capacitor/preferences instead
  • ❌ Custom tab bars – Use ion-tabs + ion-tab-bar instead
  • ❌ cordova-plugin-* plugins – Use Capacitor plugins instead
  • ❌ @angular/http (deprecated) – Use @angular/common/http 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 Best Practices (IMPORTANT)

ALWAYS use standalone components with proper imports:

❌ WRONG:

@Component({
  selector: 'app-home',
  templateUrl: './home.page.html',
})
export class HomePage {}

// home.module.ts
@NgModule({
  imports: [CommonModule, IonicModule],
  declarations: [HomePage],
})
export class HomePageModule {}

✅ CORRECT:

@Component({
  selector: 'app-home',
  standalone: true,
  imports: [IonContent, IonHeader, IonTitle, IonToolbar, TranslateModule],
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>{{ 'home.title' | translate }}</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <!-- content -->
    </ion-content>
  `,
})
export class HomePage {}

Import individual Ionic components (e.g., IonContent, IonButton) – NEVER import IonicModule in standalone components.

POST-CREATION CLEANUP (ALWAYS DO)

After creating a new Ionic project, you MUST:

  1. Remove default generated pages that conflict with your structure
  2. Ensure angular.json has the correct build configuration
  3. Verify capacitor.config.ts has the correct appId and appName
  4. Check that all Ionic imports use individual standalone component imports

AFTER COMPLETING CODE (ALWAYS RUN)

When you finish writing/modifying code, you MUST run these commands in order:

npm install
npx ng build
npx cap sync
  1. npm install installs any new dependencies
  2. ng build builds the Angular project
  3. cap sync syncs web assets and plugins to native projects

Do NOT skip these steps.


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 using:
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitor
  1. Update capacitor.config.ts with the bundle ID:
import type { CapacitorConfig } from '@capacitor/cli';

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

export default config;
  1. Then cd into the project and start implementing all required pages
  2. Do NOT ask for project path – always use current directory
  3. Add native platforms:
npx cap add ios
npx cap add android

Technology Stack

  • Framework: Ionic 8, Angular 19 (standalone components)
  • Native Runtime: Capacitor 6
  • Navigation: Angular Router with lazy-loaded standalone components
  • Tab Navigation: ion-tabs + ion-tab-bar
  • State Management: Angular Services with Signals or RxJS BehaviorSubjects
  • Translations: @ngx-translate/core + @ngx-translate/http-loader
  • Purchases: RevenueCat (@revenuecat/purchases-capacitor)
  • Advertisements: Google AdMob (@capacitor-community/admob)
  • Notifications: @capacitor/push-notifications
  • Storage: @capacitor/preferences (key-value storage)
  • Animations: Angular Animations (@angular/animations) or CSS animations

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

Storage usage example:

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' });

Project Structure

project-root/
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.config.ts
│   │   ├── app.routes.ts
│   │   ├── tabs/
│   │   │   ├── tabs.page.ts
│   │   │   └── tabs.routes.ts
│   │   ├── home/
│   │   │   └── home.page.ts
│   │   ├── explore/
│   │   │   └── explore.page.ts
│   │   ├── settings/
│   │   │   └── settings.page.ts
│   │   ├── paywall/
│   │   │   └── paywall.page.ts
│   │   ├── onboarding/
│   │   │   └── onboarding.page.ts
│   │   ├── services/
│   │   │   ├── theme.service.ts
│   │   │   ├── onboarding.service.ts
│   │   │   ├── ads.service.ts
│   │   │   ├── purchases.service.ts
│   │   │   └── notifications.service.ts
│   │   └── guards/
│   │       └── onboarding.guard.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

Tab Navigation (ion-tabs)

Ionic uses ion-tabs with ion-tab-bar for native tab navigation:

import { Component } from '@angular/core';
import { IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { home, compass, settings, heart, search, notifications, person } from 'ionicons/icons';
import { TranslateModule } from '@ngx-translate/core';

@Component({
  selector: 'app-tabs',
  standalone: true,
  imports: [IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel, TranslateModule],
  template: `
    <ion-tabs>
      <ion-tab-bar slot="bottom">
        <ion-tab-button tab="home">
          <ion-icon name="home"></ion-icon>
          <ion-label>{{ 'tabs.home' | translate }}</ion-label>
        </ion-tab-button>
        <ion-tab-button tab="explore">
          <ion-icon name="compass"></ion-icon>
          <ion-label>{{ 'tabs.explore' | translate }}</ion-label>
        </ion-tab-button>
        <ion-tab-button tab="settings">
          <ion-icon name="settings"></ion-icon>
          <ion-label>{{ 'tabs.settings' | translate }}</ion-label>
        </ion-tab-button>
      </ion-tab-bar>
    </ion-tabs>
  `,
})
export class TabsPage {
  constructor() {
    addIcons({ home, compass, settings });
  }
}

Common Ionicons

Purpose Ionicon Name
Home home
Explore compass
Settings settings
Profile person
Search search
Favorites heart
Notifications notifications

Onboarding Guard (REQUIRED)

// guards/onboarding.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { OnboardingService } from '../services/onboarding.service';

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

  const completed = await onboardingService.isCompleted();

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

  return true;
};
// services/onboarding.service.ts
import { Injectable } from '@angular/core';
import { Preferences } from '@capacitor/preferences';

@Injectable({ providedIn: 'root' })
export class OnboardingService {
  private readonly KEY = 'onboardingCompleted';

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

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

  async reset(): Promise<void> {
    await Preferences.remove({ key: this.KEY });
  }
}

RevenueCat Integration (REQUIRED)

// services/purchases.service.ts
import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { Purchases, LOG_LEVEL, PURCHASES_ERROR_CODE, PurchasesPackage } from '@revenuecat/purchases-capacitor';

@Injectable({ providedIn: 'root' })
export class PurchasesService {
  private initialized = false;

  async initialize(): 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 });
    this.initialized = true;
  }

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

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

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

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

Onboarding & Paywall Flow (CRITICAL)

  • Files: src/app/onboarding/onboarding.page.ts, src/app/paywall/paywall.page.ts
  • Swipe-based screens with fullscreen background video
  • CSS gradient overlay on video
  • IMPORTANT: Paywall MUST appear immediately after onboarding completes
// In onboarding.page.ts - when user completes onboarding:
async completeOnboarding() {
  await this.onboardingService.setCompleted(true);
  this.router.navigateByUrl('/paywall', { replaceUrl: true });
}
// In paywall.page.ts - after purchase or skip:
continue() {
  this.router.navigateByUrl('/tabs', { replaceUrl: true });
}

Flow: Onboarding → Paywall → Main App (tabs)

Paywall Subscription Options (REQUIRED)

Paywall MUST have two subscription options:

  1. Weekly – Default option
  2. Yearly – With “50% OFF” badge (recommended, should be highlighted)
// paywall.page.ts
@Component({
  selector: 'app-paywall',
  standalone: true,
  imports: [
    IonContent, IonButton, IonIcon, IonBadge,
    NgFor, NgIf, NgClass, TranslateModule,
  ],
  template: `
    <ion-content [fullscreen]="true">
      <div class="paywall-container">
        <h1>{{ 'paywall.title' | translate }}</h1>

        <div class="subscription-options">
          <div
            *ngFor="let option of subscriptionOptions"
            class="option-card"
            [class.selected]="selectedPlan === option.id"
            (click)="selectedPlan = option.id"
          >
            <ion-badge *ngIf="option.badge" color="danger">{{ option.badge }}</ion-badge>
            <h3>{{ option.title | translate }}</h3>
            <p>{{ option.price }}</p>
          </div>
        </div>

        <ion-button expand="block" (click)="subscribe()">
          {{ 'paywall.subscribe' | translate }}
        </ion-button>

        <ion-button fill="clear" (click)="skip()">
          {{ 'paywall.skip' | translate }}
        </ion-button>

        <ion-button fill="clear" size="small" (click)="restore()">
          {{ 'paywall.restore' | translate }}
        </ion-button>
      </div>
    </ion-content>
  `,
})
export class PaywallPage {
  selectedPlan = 'weekly';

  subscriptionOptions = [
    {
      id: 'weekly',
      title: 'paywall.weekly',
      price: '$4.99/week',
      badge: null,
    },
    {
      id: 'yearly',
      title: 'paywall.yearly',
      price: '$129.99/year',
      badge: '50% OFF',
    },
  ];

  constructor(
    private router: Router,
    private purchasesService: PurchasesService,
  ) {}

  async subscribe() {
    // Use RevenueCat to process purchase
    // Navigate to tabs on success
    this.router.navigateByUrl('/tabs', { replaceUrl: true });
  }

  skip() {
    this.router.navigateByUrl('/tabs', { replaceUrl: true });
  }

  async restore() {
    const restored = await this.purchasesService.restorePurchases();
    if (restored) {
      this.router.navigateByUrl('/tabs', { replaceUrl: true });
    }
  }
}
  • Yearly option should show the discount badge prominently
  • Default selection can be weekly, but yearly should be visually recommended
  • Use RevenueCat package identifiers to match these options

Settings Page Options (REQUIRED)

Settings page MUST include:

  1. Language – Change app language
  2. Theme – Light/Dark/System
  3. Notifications – Enable/disable notifications
  4. Remove Ads – Navigate to paywall (hidden if already premium)
  5. Reset Onboarding – Restart onboarding flow (for testing/demo)
// settings/settings.page.ts
@Component({
  selector: 'app-settings',
  standalone: true,
  imports: [
    IonContent, IonHeader, IonTitle, IonToolbar,
    IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
    NgIf, TranslateModule,
  ],
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>{{ 'settings.title' | translate }}</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <ion-list>
        <ion-item>
          <ion-icon name="language" slot="start"></ion-icon>
          <ion-label>{{ 'settings.language' | translate }}</ion-label>
          <ion-select [value]="currentLang" (ionChange)="changeLanguage($event)">
            <ion-select-option value="en">English</ion-select-option>
            <ion-select-option value="tr">Türkçe</ion-select-option>
          </ion-select>
        </ion-item>

        <ion-item>
          <ion-icon name="color-palette" slot="start"></ion-icon>
          <ion-label>{{ 'settings.theme' | translate }}</ion-label>
          <ion-select [value]="currentTheme" (ionChange)="changeTheme($event)">
            <ion-select-option value="system">{{ 'settings.system' | translate }}</ion-select-option>
            <ion-select-option value="light">{{ 'settings.light' | translate }}</ion-select-option>
            <ion-select-option value="dark">{{ 'settings.dark' | translate }}</ion-select-option>
          </ion-select>
        </ion-item>

        <ion-item>
          <ion-icon name="notifications" slot="start"></ion-icon>
          <ion-label>{{ 'settings.notifications' | translate }}</ion-label>
          <ion-toggle [checked]="notificationsEnabled" (ionChange)="toggleNotifications($event)"></ion-toggle>
        </ion-item>

        <ion-item *ngIf="!isPremium" button (click)="removeAds()">
          <ion-icon name="star" slot="start"></ion-icon>
          <ion-label>{{ 'settings.removeAds' | translate }}</ion-label>
        </ion-item>

        <ion-item button (click)="resetOnboarding()">
          <ion-icon name="refresh" slot="start"></ion-icon>
          <ion-label>{{ 'settings.resetOnboarding' | translate }}</ion-label>
        </ion-item>
      </ion-list>
    </ion-content>
  `,
})
export class SettingsPage implements OnInit {
  currentLang = 'en';
  currentTheme = 'system';
  notificationsEnabled = false;
  isPremium = false;

  constructor(
    private router: Router,
    private themeService: ThemeService,
    private onboardingService: OnboardingService,
    private purchasesService: PurchasesService,
    private translate: TranslateService,
  ) {}

  async ngOnInit() {
    this.currentLang = this.translate.currentLang || 'en';
    this.currentTheme = await this.themeService.getTheme();
    this.isPremium = await this.purchasesService.isPremium();
  }

  changeLanguage(event: any) {
    this.translate.use(event.detail.value);
    Preferences.set({ key: 'language', value: event.detail.value });
  }

  changeTheme(event: any) {
    this.themeService.setTheme(event.detail.value);
  }

  toggleNotifications(event: any) {
    this.notificationsEnabled = event.detail.checked;
  }

  removeAds() {
    this.router.navigateByUrl('/paywall');
  }

  async resetOnboarding() {
    await this.onboardingService.reset();
    this.router.navigateByUrl('/onboarding', { replaceUrl: true });
  }
}

Theme Service (Light/Dark/System)

// services/theme.service.ts
import { Injectable } from '@angular/core';
import { Preferences } from '@capacitor/preferences';

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

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private readonly KEY = 'themeMode';

  async initialize(): Promise<void> {
    const theme = await this.getTheme();
    this.applyTheme(theme);
  }

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

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

  private 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);
  }
}

Notifications Service

// services/notifications.service.ts
import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import { PushNotifications } from '@capacitor/push-notifications';

@Injectable({ providedIn: 'root' })
export class NotificationsService {
  async requestPermission(): Promise<boolean> {
    if (!Capacitor.isNativePlatform()) return false;

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

  async addListeners(): 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);
    });
  }
}

Localization

  • Translation files stored in src/assets/i18n/
  • Use @ngx-translate/core with TranslateModule and translate pipe
  • Detect device language with navigator.language
// In app.component.ts or main.ts
const browserLang = navigator.language.split('-')[0];
translate.setDefaultLang('en');
translate.use(['en', 'tr'].includes(browserLang) ? browserLang : 'en');

Translation Files

// src/assets/i18n/en.json
{
  "tabs": {
    "home": "Home",
    "explore": "Explore",
    "settings": "Settings"
  },
  "onboarding": {
    "next": "Next",
    "start": "Get Started",
    "skip": "Skip"
  },
  "paywall": {
    "title": "Go Premium",
    "weekly": "Weekly",
    "yearly": "Yearly",
    "subscribe": "Subscribe",
    "skip": "Continue with ads",
    "restore": "Restore Purchases"
  },
  "settings": {
    "title": "Settings",
    "language": "Language",
    "theme": "Theme",
    "system": "System",
    "light": "Light",
    "dark": "Dark",
    "notifications": "Notifications",
    "removeAds": "Remove Ads",
    "resetOnboarding": "Reset Onboarding"
  }
}
// src/assets/i18n/tr.json
{
  "tabs": {
    "home": "Ana Sayfa",
    "explore": "Keşfet",
    "settings": "Ayarlar"
  },
  "onboarding": {
    "next": "İleri",
    "start": "Başla",
    "skip": "Atla"
  },
  "paywall": {
    "title": "Premium'a Geç",
    "weekly": "Haftalık",
    "yearly": "Yıllık",
    "subscribe": "Abone Ol",
    "skip": "Reklamlı devam et",
    "restore": "Satın Alımları Geri Yükle"
  },
  "settings": {
    "title": "Ayarlar",
    "language": "Dil",
    "theme": "Tema",
    "system": "Sistem",
    "light": "Açık",
    "dark": "Koyu",
    "notifications": "Bildirimler",
    "removeAds": "Reklamları Kaldır",
    "resetOnboarding": "Tanıtımı Sıfırla"
  }
}

Development Commands

npm install
ionic serve          # Run in browser
ionic build          # Build for production
npx cap sync         # Sync web assets to native
npx cap open ios     # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios      # Build and run on iOS device/simulator
npx cap run android  # Build and run on Android device/emulator

Coding Standards

  • Use standalone components (NEVER NgModules for pages/components)
  • Strict TypeScript – no any types
  • Avoid hardcoded strings – use translation keys
  • Use @capacitor/preferences for persistent storage
  • Use Angular’s inject() function or constructor injection
  • Lazy-load all pages via loadComponent in routes
  • Use Angular Signals for reactive state when possible
  • Import individual Ionic components (e.g., IonButton) – NEVER IonicModule
  • Use addIcons() from ionicons to register icons in standalone components

Important Notes

  1. iOS permissions are defined in Info.plist (added via Capacitor plugins)
  2. Android permissions are defined in AndroidManifest.xml (added via Capacitor plugins)
  3. Always run npx cap sync after installing new Capacitor plugins
  4. Use Capacitor.isNativePlatform() to guard native-only code
  5. Test in browser with ionic serve for rapid development, then test on devices

App Store & Play Store Notes

  • iOS ATT permission required for personalized ads
  • Restore purchases must work correctly
  • Target SDK must be up to date
  • Use npx cap sync before each native build

Testing Checklist

  • UI tested in all languages
  • Dark / Light mode
  • Notifications
  • Premium flow
  • Restore purchases
  • Offline support
  • Multiple screen sizes
  • Browser (ionic serve) and native platforms

After Development

npm run build
npx cap sync
npx cap open ios
npx cap open android

NOTE: cap sync copies the built web app to native projects and syncs plugins. Run it after every build before testing on native platforms.