ionic-skills
npx skills add https://github.com/erkamyaman/ionic-skills --skill ionic-skills
Agent 安装分布
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-tabswithion-tab-barfor 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/admobnative overlay - Use
AdsServiceto 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)
- â
localStoragedirectly – Use@capacitor/preferencesinstead - â NgModules for new pages/components – Use standalone components instead
- â
@ionic/storage– Use@capacitor/preferencesinstead - â Custom tab bars – Use
ion-tabs+ion-tab-barinstead - â
cordova-plugin-*plugins – Use Capacitor plugins instead - â
@angular/http(deprecated) – Use@angular/common/httpinstead - â
anytype – Always use proper TypeScript types - â
ngx-admob-freeor other deprecated ad libraries – ONLY use@capacitor-community/admob - â Synchronous Capacitor calls – Always
awaitCapacitor 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:
- Remove default generated pages that conflict with your structure
- Ensure
angular.jsonhas the correct build configuration - Verify
capacitor.config.tshas the correctappIdandappName - 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
npm installinstalls any new dependenciesng buildbuilds the Angular projectcap syncsyncs web assets and plugins to native projects
Do NOT skip these steps.
Project Creation
When user asks to create an app, you MUST:
- FIRST ask for the bundle ID (e.g., “What is the bundle ID? Example: com.company.appname”)
- Create the project in the CURRENT directory using:
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitor
- Update
capacitor.config.tswith 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;
- Then cd into the project and start implementing all required pages
- Do NOT ask for project path – always use current directory
- 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
localStoragedirectly! Use@capacitor/preferencesinstead 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:
- Weekly – Default option
- 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:
- Language – Change app language
- Theme – Light/Dark/System
- Notifications – Enable/disable notifications
- Remove Ads – Navigate to paywall (hidden if already premium)
- 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/corewithTranslateModuleandtranslatepipe - 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
anytypes - Avoid hardcoded strings – use translation keys
- Use
@capacitor/preferencesfor persistent storage - Use Angular’s
inject()function or constructor injection - Lazy-load all pages via
loadComponentin routes - Use Angular Signals for reactive state when possible
- Import individual Ionic components (e.g.,
IonButton) – NEVERIonicModule - Use
addIcons()fromioniconsto register icons in standalone components
Important Notes
- iOS permissions are defined in
Info.plist(added via Capacitor plugins) - Android permissions are defined in
AndroidManifest.xml(added via Capacitor plugins) - Always run
npx cap syncafter installing new Capacitor plugins - Use
Capacitor.isNativePlatform()to guard native-only code - Test in browser with
ionic servefor 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 syncbefore 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 synccopies the built web app to native projects and syncs plugins. Run it after every build before testing on native platforms.