ionic-skills
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIonic Capacitor Application Development Guide
Ionic Capacitor 应用开发指南
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.
重要提示:这是一个SKILL文件,不是项目。切勿在此文件夹中运行。切勿在此处创建代码文件。创建新项目时,务必先询问用户项目路径,或者在单独的目录中创建(例如:npm install)。~/Projects/app-name
本指南介绍如何使用Ionic Capacitor结合Angular、React或Vue构建生产级移动应用。
MANDATORY REQUIREMENTS
强制要求
When creating a new Ionic project, you MUST include ALL of the following:
创建新Ionic项目时,必须包含以下所有内容:
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
- 引导页 - 基于滑动的引导流程,带全屏背景视频和渐变遮罩
- 付费墙页面 - RevenueCat付费墙页面(引导流程完成后显示)
- 设置页面 - 包含语言、主题、通知和重置引导选项的设置页面
Required Navigation (ALWAYS USE)
必须使用的导航
- Use with
ion-tabsfor tab navigation - NEVER use custom tab implementations or third-party tab librariesion-tab-bar
- 使用搭配
ion-tabs实现标签导航 - 切勿使用自定义标签实现或第三方标签库ion-tab-bar
Required Libraries (ALWAYS INSTALL)
必须安装的库
Angular
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 swiperbash
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 swiperReact
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 swiperbash
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 swiperVue
Vue
bash
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob vue-i18n swiperbash
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob vue-i18n swiperShared Libraries (All Frameworks)
全框架共享库
- (RevenueCat)
@revenuecat/purchases-capacitor - (AdMob)
@capacitor-community/admob - (Push Notifications)
@capacitor/push-notifications - (Key-value storage)
@capacitor/preferences - (Splash screen control)
@capacitor/splash-screen - (Status bar styling)
@capacitor/status-bar - (Onboarding slides)
swiper
- (RevenueCat)
@revenuecat/purchases-capacitor - (AdMob)
@capacitor-community/admob - (推送通知)
@capacitor/push-notifications - (键值存储)
@capacitor/preferences - (启动屏控制)
@capacitor/splash-screen - (状态栏样式)
@capacitor/status-bar - (引导页滑动组件)
swiper
Framework-Specific i18n Libraries
框架专属i18n库
| Framework | Library | Usage |
|---|---|---|
| Angular | | |
| React | | |
| Vue | | |
| 框架 | 库 | 使用方式 |
|---|---|---|
| Angular | | |
| React | | |
| Vue | | |
FORBIDDEN (NEVER USE)
禁止事项(切勿使用)
All Frameworks
全框架通用
- ❌ directly - Use
localStorageinstead@capacitor/preferences - ❌ - Use
@ionic/storageinstead@capacitor/preferences - ❌ Custom tab bars - Use +
ion-tabsinsteadion-tab-bar - ❌ plugins - Use Capacitor plugins instead
cordova-plugin-* - ❌ type - Always use proper TypeScript types
any - ❌ or other deprecated ad libraries - ONLY use
ngx-admob-free@capacitor-community/admob - ❌ Synchronous Capacitor calls - Always Capacitor plugin methods
await
- ❌ 直接使用- 请改用
localStorage@capacitor/preferences - ❌ - 请改用
@ionic/storage@capacitor/preferences - ❌ 自定义标签栏 - 请改用+
ion-tabsion-tab-bar - ❌ 插件 - 请改用Capacitor插件
cordova-plugin-* - ❌ 类型 - 始终使用正确的TypeScript类型
any - ❌ 或其他已废弃的广告库 - 仅允许使用
ngx-admob-free@capacitor-community/admob - ❌ 同步调用Capacitor方法 - 始终使用处理Capacitor插件方法
await
Angular-Specific
Angular专属
- ❌ NgModules for new pages/components - Use standalone components
- ❌ in standalone components - Import individual components (
IonicModule,IonButton, etc.)IonContent - ❌ Inline or
templateinstyles- Use separate@Component,.html,.tsfiles with.scssandtemplateUrlstyleUrls - ❌ (deprecated) - Use
@angular/http@angular/common/http
- ❌ 为新页面/组件使用NgModules - 请使用独立组件
- ❌ 在独立组件中导入- 请导入单个组件(如
IonicModule、IonButton等)IonContent - ❌ 在中使用内联
@Component或template- 请使用单独的styles、.html、.ts文件搭配.scss和templateUrlstyleUrls - ❌ (已废弃) - 请使用
@angular/http@angular/common/http
React-Specific
React专属
- ❌ Class components - Use functional components with hooks
- ❌ Direct DOM manipulation - Use React refs and state
- ❌ imports - Use
@ionic/angular@ionic/react
- ❌ 类组件 - 请使用带钩子的函数式组件
- ❌ 直接操作DOM - 请使用React refs和状态
- ❌ 导入- 请使用
@ionic/angular@ionic/react
Vue-Specific
Vue专属
- ❌ Options API for new code - Use Composition API with
<script setup> - ❌ Direct DOM manipulation - Use Vue refs and reactivity
- ❌ imports - Use
@ionic/angular@ionic/vue
- ❌ 为新代码使用选项式API - 请使用组合式API搭配
<script setup> - ❌ 直接操作DOM - 请使用Vue refs和响应式API
- ❌ 导入- 请使用
@ionic/angular@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 USEdirectly! UselocalStorageinstead for cross-platform persistent storage.@capacitor/preferences
| 分类 | Angular | React | Vue |
|---|---|---|---|
| 框架 | Ionic 8 + Angular 19 | Ionic 8 + React 19 | Ionic 8 + Vue 3.5 |
| 原生运行时 | Capacitor 7 | Capacitor 7 | Capacitor 7 |
| 路由 | Angular Router(懒加载) | @ionic/react-router | @ionic/vue-router |
| 标签导航 | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar | ion-tabs + ion-tab-bar |
| 状态管理 | Angular Services(Signals/RxJS) | 自定义Hooks / Context | 组合式函数 / Pinia |
| 国际化 | @ngx-translate/core | react-i18next | vue-i18n |
| 支付 | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor | @revenuecat/purchases-capacitor |
| 广告 | @capacitor-community/admob | @capacitor-community/admob | @capacitor-community/admob |
| 通知 | @capacitor/push-notifications | @capacitor/push-notifications | @capacitor/push-notifications |
| 存储 | @capacitor/preferences | @capacitor/preferences | @capacitor/preferences |
警告:切勿直接使用!请使用localStorage实现跨平台持久化存储。@capacitor/preferences
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
- Then implement all required pages
当用户要求创建应用时,必须:
- 首先询问Bundle ID(例如:"请提供Bundle ID?示例:com.company.appname")
- 在当前目录创建项目
- 然后实现所有必填页面
Creating a Project (Angular)
创建Angular项目
bash
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitorbash
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitorCreating a Project (React)
创建React项目
bash
npm install -g @ionic/cli
ionic start app-name blank --type=react --capacitorbash
npm install -g @ionic/cli
ionic start app-name blank --type=react --capacitorCreating a Project (Vue)
创建Vue项目
bash
npm install -g @ionic/cli
ionic start app-name blank --type=vue --capacitorbash
npm install -g @ionic/cli
ionic start app-name blank --type=vue --capacitorCapacitor Configuration (All Frameworks)
Capacitor配置(全框架)
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,may bewebDirinstead of'dist'. Check your project's build output directory.'www'
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;注意:对于React/Vue项目,可能为webDir而非'dist'。请检查项目的构建输出目录。'www'
Add Native Platforms
添加原生平台
bash
npx cap add ios
npx cap add androidbash
npx cap add ios
npx cap add androidProject Structure
项目结构
Angular Project Structure
Angular项目结构
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.jsonproject-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.jsonReact Project Structure
React项目结构
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.jsonproject-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.jsonVue Project Structure
Vue项目结构
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.jsonproject-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.jsonApp Configuration
应用配置
App Configuration (Angular)
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();
}
}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)
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;tsx
// main.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
/* Ionic组件所需的核心CSS */
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)
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>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';
/* Ionic组件所需的核心CSS */
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)
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',
},
],
},
];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)
React路由
Routes are defined in (see App Configuration above). Tab routes are defined in the component:
App.tsxTabsLayouttsx
// 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;路由定义在中(见上述应用配置部分)。标签路由定义在组件中:
App.tsxTabsLayouttsx
// 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)
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>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).
这些工具文件包含纯TypeScript代码,调用Capacitor插件,可被所有框架使用。每个框架会用各自的模式封装这些工具(Angular服务、React钩子、Vue组合式函数)。
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' });typescript
import { Preferences } from '@capacitor/preferences';
// 设置值
await Preferences.set({ key: 'onboardingCompleted', value: 'true' });
// 获取值
const { value } = await Preferences.get({ key: 'onboardingCompleted' });
console.log(value); // 'true'
// 删除值
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 });
}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);
}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
AdMob工具
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.
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, // 生产环境请设为false
});
initialized = true;
}
export async function showBannerAd(): Promise<void> {
if (!initialized) return;
const options: BannerAdOptions = {
adId: 'ca-app-pub-3940256099942544/6300978111', // 测试ID - 生产环境请替换
adSize: BannerAdSize.ADAPTIVE_BANNER,
position: BannerAdPosition.BOTTOM_CENTER,
isTesting: true, // 生产环境请设为false
};
await AdMob.showBanner(options);
}
export async function hideBannerAd(): Promise<void> {
if (!initialized) return;
await AdMob.hideBanner();
}开发/测试时,请使用测试广告ID:
- 横幅广告:
ca-app-pub-3940256099942544/6300978111
切勿跳过AdMob初始化,否则插件无法正常工作。
RevenueCat Utility
RevenueCat工具
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;
}
}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 }); // 生产环境请使用WARN
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);
});
}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('推送注册成功,Token:', token.value);
});
await PushNotifications.addListener('registrationError', (error) => {
console.error('推送注册失败:', error);
});
await PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('收到推送通知:', notification);
});
await PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
console.log('推送通知操作:', action);
});
}Framework-Specific Service Wrappers
框架专属服务封装
Angular Services
Angular服务
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;
}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
React钩子
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(), []),
};
}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
Vue组合式函数
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,
};
}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)
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;
};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)
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;
};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)
Vue引导页守卫
The Vue onboarding guard is implemented as a hook. See the Routing (Vue) section above.
router.beforeEachVue引导页守卫已通过钩子实现,请查看上述Vue路由配置部分。
router.beforeEachOnboarding Page
引导页
Shared Video CSS (All Frameworks)
全框架共享视频CSS
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 element. Use native HTML5 - NOT canvas, NOT animated GIFs, NOT external players.
<video><video>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%;
}切勿仅引用视频链接却不渲染元素。请使用原生HTML5 - 切勿使用canvas、GIF动画或外部播放器。
<video><video>Onboarding Page (Angular)
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 });
}
}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滑动内容 -->
</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)
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;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滑动内容 */}
</div>
</IonContent>
);
};
export default OnboardingPage;Onboarding Page (Vue)
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>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滑动内容 */}
</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:
- Weekly - Default option
- Yearly - With "50% OFF" badge (recommended, should be highlighted)
Three buttons: Subscribe, Continue with ads, Restore Purchases.
Flow:
Onboarding -> Paywall -> Main App (tabs)重要提示:引导流程完成后必须立即显示付费墙。
付费墙必须包含两种订阅选项:
- 周订阅 - 默认选项
- 年订阅 - 带"50% OFF"标识(推荐选项,需高亮显示)
三个按钮:订阅、继续使用广告版、恢复购买。
流程:
引导页 -> 付费墙 -> 主应用(标签页)Paywall Page (Angular)
Angular付费墙
html
<!-- paywall/paywall.page.html -->
<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>scss
// paywall/paywall.page.scss
.paywall-container {
// Add your paywall styles here
}
.subscription-options {
// Add your subscription options styles here
}
.option-card {
// Add your option card styles here
&.selected {
// Selected state styles
}
}typescript
// paywall/paywall.page.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonButton, IonIcon, IonBadge,
} from '@ionic/angular/standalone';
import { NgFor, NgIf, NgClass } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-paywall',
standalone: true,
imports: [
IonContent, IonButton, IonIcon, IonBadge,
NgFor, NgIf, NgClass, TranslateModule,
],
templateUrl: './paywall.page.html',
styleUrls: ['./paywall.page.scss'],
})
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
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 });
}
}
}html
<!-- paywall/paywall.page.html -->
<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>scss
// paywall/paywall.page.scss
.paywall-container {
// 添加付费墙样式
}
.subscription-options {
// 添加订阅选项样式
}
.option-card {
// 添加选项卡片样式
&.selected {
// 选中状态样式
}
}typescript
// paywall/paywall.page.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonButton, IonIcon, IonBadge,
} from '@ionic/angular/standalone';
import { NgFor, NgIf, NgClass } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-paywall',
standalone: true,
imports: [
IonContent, IonButton, IonIcon, IonBadge,
NgFor, NgIf, NgClass, TranslateModule,
],
templateUrl: './paywall.page.html',
styleUrls: ['./paywall.page.scss'],
})
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() {
// 使用RevenueCat处理购买
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 });
}
}
}Paywall Page (React)
React付费墙
tsx
// pages/PaywallPage.tsx
import { useState } from 'react';
import {
IonContent, IonButton, IonBadge, useIonRouter,
} from '@ionic/react';
import { useTranslation } from 'react-i18next';
import { usePurchases } from '../hooks/usePurchases';
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
const PaywallPage: React.FC = () => {
const { t } = useTranslation();
const router = useIonRouter();
const { restorePurchases } = usePurchases();
const [selectedPlan, setSelectedPlan] = useState('weekly');
const subscribe = async () => {
// Use RevenueCat to process purchase
router.push('/tabs', 'forward', 'replace');
};
const skip = () => {
router.push('/tabs', 'forward', 'replace');
};
const restore = async () => {
const restored = await restorePurchases();
if (restored) {
router.push('/tabs', 'forward', 'replace');
}
};
return (
<IonContent fullscreen>
<div className="paywall-container">
<h1>{t('paywall.title')}</h1>
<div className="subscription-options">
{subscriptionOptions.map((option) => (
<div
key={option.id}
className={`option-card ${selectedPlan === option.id ? 'selected' : ''}`}
onClick={() => setSelectedPlan(option.id)}
>
{option.badge && <IonBadge color="danger">{option.badge}</IonBadge>}
<h3>{t(option.title)}</h3>
<p>{option.price}</p>
</div>
))}
</div>
<IonButton expand="block" onClick={subscribe}>
{t('paywall.subscribe')}
</IonButton>
<IonButton fill="clear" onClick={skip}>
{t('paywall.skip')}
</IonButton>
<IonButton fill="clear" size="small" onClick={restore}>
{t('paywall.restore')}
</IonButton>
</div>
</IonContent>
);
};
export default PaywallPage;tsx
// pages/PaywallPage.tsx
import { useState } from 'react';
import {
IonContent, IonButton, IonBadge, useIonRouter,
} from '@ionic/react';
import { useTranslation } from 'react-i18next';
import { usePurchases } from '../hooks/usePurchases';
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
const PaywallPage: React.FC = () => {
const { t } = useTranslation();
const router = useIonRouter();
const { restorePurchases } = usePurchases();
const [selectedPlan, setSelectedPlan] = useState('weekly');
const subscribe = async () => {
// 使用RevenueCat处理购买
router.push('/tabs', 'forward', 'replace');
};
const skip = () => {
router.push('/tabs', 'forward', 'replace');
};
const restore = async () => {
const restored = await restorePurchases();
if (restored) {
router.push('/tabs', 'forward', 'replace');
}
};
return (
<IonContent fullscreen>
<div className="paywall-container">
<h1>{t('paywall.title')}</h1>
<div className="subscription-options">
{subscriptionOptions.map((option) => (
<div
key={option.id}
className={`option-card ${selectedPlan === option.id ? 'selected' : ''}`}
onClick={() => setSelectedPlan(option.id)}
>
{option.badge && <IonBadge color="danger">{option.badge}</IonBadge>}
<h3>{t(option.title)}</h3>
<p>{option.price}</p>
</div>
))}
</div>
<IonButton expand="block" onClick={subscribe}>
{t('paywall.subscribe')}
</IonButton>
<IonButton fill="clear" onClick={skip}>
{t('paywall.skip')}
</IonButton>
<IonButton fill="clear" size="small" onClick={restore}>
{t('paywall.restore')}
</IonButton>
</div>
</IonContent>
);
};
export default PaywallPage;Paywall Page (Vue)
Vue付费墙
vue
<!-- views/PaywallPage.vue -->
<template>
<ion-content :fullscreen="true">
<div class="paywall-container">
<h1>{{ t('paywall.title') }}</h1>
<div class="subscription-options">
<div
v-for="option in subscriptionOptions"
:key="option.id"
class="option-card"
:class="{ selected: selectedPlan === option.id }"
@click="selectedPlan = option.id"
>
<ion-badge v-if="option.badge" color="danger">{{ option.badge }}</ion-badge>
<h3>{{ t(option.title) }}</h3>
<p>{{ option.price }}</p>
</div>
</div>
<ion-button expand="block" @click="subscribe">
{{ t('paywall.subscribe') }}
</ion-button>
<ion-button fill="clear" @click="skip">
{{ t('paywall.skip') }}
</ion-button>
<ion-button fill="clear" size="small" @click="restore">
{{ t('paywall.restore') }}
</ion-button>
</div>
</ion-content>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { IonContent, IonButton, IonBadge } from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { usePurchases } from '../composables/usePurchases';
const { t } = useI18n();
const router = useRouter();
const { restorePurchases } = usePurchases();
const selectedPlan = ref('weekly');
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
async function subscribe() {
// Use RevenueCat to process purchase
router.replace('/tabs');
}
function skip() {
router.replace('/tabs');
}
async function restore() {
const restored = await restorePurchases();
if (restored) {
router.replace('/tabs');
}
}
</script>vue
<!-- views/PaywallPage.vue -->
<template>
<ion-content :fullscreen="true">
<div class="paywall-container">
<h1>{{ t('paywall.title') }}</h1>
<div class="subscription-options">
<div
v-for="option in subscriptionOptions"
:key="option.id"
class="option-card"
:class="{ selected: selectedPlan === option.id }"
@click="selectedPlan = option.id"
>
<ion-badge v-if="option.badge" color="danger">{{ option.badge }}</ion-badge>
<h3>{{ t(option.title) }}</h3>
<p>{{ option.price }}</p>
</div>
</div>
<ion-button expand="block" @click="subscribe">
{{ t('paywall.subscribe') }}
</ion-button>
<ion-button fill="clear" @click="skip">
{{ t('paywall.skip') }}
</ion-button>
<ion-button fill="clear" size="small" @click="restore">
{{ t('paywall.restore') }}
</ion-button>
</div>
</ion-content>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { IonContent, IonButton, IonBadge } from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { usePurchases } from '../composables/usePurchases';
const { t } = useI18n();
const router = useRouter();
const { restorePurchases } = usePurchases();
const selectedPlan = ref('weekly');
const subscriptionOptions = [
{ id: 'weekly', title: 'paywall.weekly', price: '$4.99/week', badge: null },
{ id: 'yearly', title: 'paywall.yearly', price: '$129.99/year', badge: '50% OFF' },
];
async function subscribe() {
// 使用RevenueCat处理购买
router.replace('/tabs');
}
function skip() {
router.replace('/tabs');
}
async function restore() {
const restored = await restorePurchases();
if (restored) {
router.replace('/tabs');
}
}
</script>Tab Navigation
标签导航
Common Ionicons
常用Ionicons图标
| Purpose | Ionicon Name |
|---|---|
| Home | home |
| Explore | compass |
| Settings | settings |
| Profile | person |
| Search | search |
| Favorites | heart |
| Notifications | notifications |
| 用途 | 图标名称 |
|---|---|
| 首页 | home |
| 发现 | compass |
| 设置 | settings |
| 个人资料 | person |
| 搜索 | search |
| 收藏 | heart |
| 通知 | notifications |
Tab Navigation (Angular)
Angular标签导航
html
<!-- tabs/tabs.page.html -->
<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>typescript
// 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],
templateUrl: './tabs.page.html',
styleUrls: ['./tabs.page.scss'],
})
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();
}
}html
<!-- tabs/tabs.page.html -->
<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>typescript
// 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],
templateUrl: './tabs.page.html',
styleUrls: ['./tabs.page.scss'],
})
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();
}
}Tab Navigation (React)
React标签导航
See the component in the Routing (React) section above.
TabsLayout请查看上述React路由配置中的组件。
TabsLayoutTab Navigation (Vue)
Vue标签导航
See the component in the Routing (Vue) section above.
TabsLayout.vue请查看上述Vue路由配置中的组件。
TabsLayout.vueSettings Page
设置页面
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 Page (Angular)
Angular设置页面
html
<!-- settings/settings.page.html -->
<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>scss
// settings/settings.page.scss
// Add your settings page styles heretypescript
// settings/settings.page.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/angular/standalone';
import { NgIf } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Preferences } from '@capacitor/preferences';
import { ThemeService } from '../services/theme.service';
import { OnboardingService } from '../services/onboarding.service';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-settings',
standalone: true,
imports: [
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
NgIf, TranslateModule,
],
templateUrl: './settings.page.html',
styleUrls: ['./settings.page.scss'],
})
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: CustomEvent) {
const lang = event.detail.value;
this.translate.use(lang);
Preferences.set({ key: 'language', value: lang });
}
changeTheme(event: CustomEvent) {
this.themeService.setTheme(event.detail.value);
}
toggleNotifications(event: CustomEvent) {
this.notificationsEnabled = event.detail.checked;
}
removeAds() {
this.router.navigateByUrl('/paywall');
}
async resetOnboarding() {
await this.onboardingService.reset();
this.router.navigateByUrl('/onboarding', { replaceUrl: true });
}
}html
<!-- settings/settings.page.html -->
<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>scss
// settings/settings.page.scss
// 添加设置页面样式typescript
// settings/settings.page.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import {
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/angular/standalone';
import { NgIf } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Preferences } from '@capacitor/preferences';
import { ThemeService } from '../services/theme.service';
import { OnboardingService } from '../services/onboarding.service';
import { PurchasesService } from '../services/purchases.service';
@Component({
selector: 'app-settings',
standalone: true,
imports: [
IonContent, IonHeader, IonTitle, IonToolbar,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
NgIf, TranslateModule,
],
templateUrl: './settings.page.html',
styleUrls: ['./settings.page.scss'],
})
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: CustomEvent) {
const lang = event.detail.value;
this.translate.use(lang);
Preferences.set({ key: 'language', value: lang });
}
changeTheme(event: CustomEvent) {
this.themeService.setTheme(event.detail.value);
}
toggleNotifications(event: CustomEvent) {
this.notificationsEnabled = event.detail.checked;
}
removeAds() {
this.router.navigateByUrl('/paywall');
}
async resetOnboarding() {
await this.onboardingService.reset();
this.router.navigateByUrl('/onboarding', { replaceUrl: true });
}
}Settings Page (React)
React设置页面
tsx
// pages/SettingsPage.tsx
import { useState, useEffect } from 'react';
import {
IonContent, IonHeader, IonTitle, IonToolbar, IonPage,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
useIonRouter,
} from '@ionic/react';
import { language, colorPalette, notifications, star, refresh } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../hooks/useTheme';
import { useOnboarding } from '../hooks/useOnboarding';
import { usePurchases } from '../hooks/usePurchases';
import { ThemeMode } from '../utils/theme';
const SettingsPage: React.FC = () => {
const { t, i18n } = useTranslation();
const router = useIonRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const [currentLang, setCurrentLang] = useState('en');
const [currentTheme, setCurrentTheme] = useState<ThemeMode>('system');
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const [premium, setPremium] = useState(false);
useEffect(() => {
setCurrentLang(i18n.language || 'en');
getTheme().then(setCurrentTheme);
isPremium().then(setPremium);
}, []);
const changeLanguage = (lang: string) => {
setCurrentLang(lang);
i18n.changeLanguage(lang);
Preferences.set({ key: 'language', value: lang });
};
const changeTheme = (mode: ThemeMode) => {
setCurrentTheme(mode);
setTheme(mode);
};
const resetOnboarding = async () => {
await reset();
router.push('/onboarding', 'forward', 'replace');
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('settings.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem>
<IonIcon icon={language} slot="start" />
<IonLabel>{t('settings.language')}</IonLabel>
<IonSelect value={currentLang} onIonChange={(e) => changeLanguage(e.detail.value)}>
<IonSelectOption value="en">English</IonSelectOption>
<IonSelectOption value="tr">Türkçe</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={colorPalette} slot="start" />
<IonLabel>{t('settings.theme')}</IonLabel>
<IonSelect value={currentTheme} onIonChange={(e) => changeTheme(e.detail.value)}>
<IonSelectOption value="system">{t('settings.system')}</IonSelectOption>
<IonSelectOption value="light">{t('settings.light')}</IonSelectOption>
<IonSelectOption value="dark">{t('settings.dark')}</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={notifications} slot="start" />
<IonLabel>{t('settings.notifications')}</IonLabel>
<IonToggle
checked={notificationsEnabled}
onIonChange={(e) => setNotificationsEnabled(e.detail.checked)}
/>
</IonItem>
{!premium && (
<IonItem button onClick={() => router.push('/paywall')}>
<IonIcon icon={star} slot="start" />
<IonLabel>{t('settings.removeAds')}</IonLabel>
</IonItem>
)}
<IonItem button onClick={resetOnboarding}>
<IonIcon icon={refresh} slot="start" />
<IonLabel>{t('settings.resetOnboarding')}</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
export default SettingsPage;tsx
// pages/SettingsPage.tsx
import { useState, useEffect } from 'react';
import {
IonContent, IonHeader, IonTitle, IonToolbar, IonPage,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
useIonRouter,
} from '@ionic/react';
import { language, colorPalette, notifications, star, refresh } from 'ionicons/icons';
import { useTranslation } from 'react-i18next';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../hooks/useTheme';
import { useOnboarding } from '../hooks/useOnboarding';
import { usePurchases } from '../hooks/usePurchases';
import { ThemeMode } from '../utils/theme';
const SettingsPage: React.FC = () => {
const { t, i18n } = useTranslation();
const router = useIonRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const [currentLang, setCurrentLang] = useState('en');
const [currentTheme, setCurrentTheme] = useState<ThemeMode>('system');
const [notificationsEnabled, setNotificationsEnabled] = useState(false);
const [premium, setPremium] = useState(false);
useEffect(() => {
setCurrentLang(i18n.language || 'en');
getTheme().then(setCurrentTheme);
isPremium().then(setPremium);
}, []);
const changeLanguage = (lang: string) => {
setCurrentLang(lang);
i18n.changeLanguage(lang);
Preferences.set({ key: 'language', value: lang });
};
const changeTheme = (mode: ThemeMode) => {
setCurrentTheme(mode);
setTheme(mode);
};
const resetOnboarding = async () => {
await reset();
router.push('/onboarding', 'forward', 'replace');
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('settings.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList>
<IonItem>
<IonIcon icon={language} slot="start" />
<IonLabel>{t('settings.language')}</IonLabel>
<IonSelect value={currentLang} onIonChange={(e) => changeLanguage(e.detail.value)}>
<IonSelectOption value="en">English</IonSelectOption>
<IonSelectOption value="tr">Türkçe</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={colorPalette} slot="start" />
<IonLabel>{t('settings.theme')}</IonLabel>
<IonSelect value={currentTheme} onIonChange={(e) => changeTheme(e.detail.value)}>
<IonSelectOption value="system">{t('settings.system')}</IonSelectOption>
<IonSelectOption value="light">{t('settings.light')}</IonSelectOption>
<IonSelectOption value="dark">{t('settings.dark')}</IonSelectOption>
</IonSelect>
</IonItem>
<IonItem>
<IonIcon icon={notifications} slot="start" />
<IonLabel>{t('settings.notifications')}</IonLabel>
<IonToggle
checked={notificationsEnabled}
onIonChange={(e) => setNotificationsEnabled(e.detail.checked)}
/>
</IonItem>
{!premium && (
<IonItem button onClick={() => router.push('/paywall')}>
<IonIcon icon={star} slot="start" />
<IonLabel>{t('settings.removeAds')}</IonLabel>
</IonItem>
)}
<IonItem button onClick={resetOnboarding}>
<IonIcon icon={refresh} slot="start" />
<IonLabel>{t('settings.resetOnboarding')}</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
export default SettingsPage;Settings Page (Vue)
Vue设置页面
vue
<!-- views/SettingsPage.vue -->
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('settings.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-icon name="language" slot="start" />
<ion-label>{{ t('settings.language') }}</ion-label>
<ion-select :value="currentLang" @ion-change="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-label>{{ t('settings.theme') }}</ion-label>
<ion-select :value="currentTheme" @ion-change="changeTheme($event)">
<ion-select-option value="system">{{ t('settings.system') }}</ion-select-option>
<ion-select-option value="light">{{ t('settings.light') }}</ion-select-option>
<ion-select-option value="dark">{{ t('settings.dark') }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="notifications" slot="start" />
<ion-label>{{ t('settings.notifications') }}</ion-label>
<ion-toggle :checked="notificationsEnabled" @ion-change="toggleNotifications($event)" />
</ion-item>
<ion-item v-if="!premium" button @click="removeAds">
<ion-icon name="star" slot="start" />
<ion-label>{{ t('settings.removeAds') }}</ion-label>
</ion-item>
<ion-item button @click="resetOnboardingFlow">
<ion-icon name="refresh" slot="start" />
<ion-label>{{ t('settings.resetOnboarding') }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../composables/useTheme';
import { useOnboarding } from '../composables/useOnboarding';
import { usePurchases } from '../composables/usePurchases';
import type { ThemeMode } from '../utils/theme';
const { t, locale } = useI18n();
const router = useRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const currentLang = ref('en');
const currentTheme = ref<ThemeMode>('system');
const notificationsEnabled = ref(false);
const premium = ref(false);
onMounted(async () => {
currentLang.value = locale.value || 'en';
currentTheme.value = await getTheme();
premium.value = await isPremium();
});
function changeLanguage(event: CustomEvent) {
const lang = event.detail.value;
currentLang.value = lang;
locale.value = lang;
Preferences.set({ key: 'language', value: lang });
}
function changeTheme(event: CustomEvent) {
const mode = event.detail.value as ThemeMode;
currentTheme.value = mode;
setTheme(mode);
}
function toggleNotifications(event: CustomEvent) {
notificationsEnabled.value = event.detail.checked;
}
function removeAds() {
router.push('/paywall');
}
async function resetOnboardingFlow() {
await reset();
router.replace('/onboarding');
}
</script>vue
<!-- views/SettingsPage.vue -->
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('settings.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>
<ion-icon name="language" slot="start" />
<ion-label>{{ t('settings.language') }}</ion-label>
<ion-select :value="currentLang" @ion-change="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-label>{{ t('settings.theme') }}</ion-label>
<ion-select :value="currentTheme" @ion-change="changeTheme($event)">
<ion-select-option value="system">{{ t('settings.system') }}</ion-select-option>
<ion-select-option value="light">{{ t('settings.light') }}</ion-select-option>
<ion-select-option value="dark">{{ t('settings.dark') }}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-icon name="notifications" slot="start" />
<ion-label>{{ t('settings.notifications') }}</ion-label>
<ion-toggle :checked="notificationsEnabled" @ion-change="toggleNotifications($event)" />
</ion-item>
<ion-item v-if="!premium" button @click="removeAds">
<ion-icon name="star" slot="start" />
<ion-label>{{ t('settings.removeAds') }}</ion-label>
</ion-item>
<ion-item button @click="resetOnboardingFlow">
<ion-icon name="refresh" slot="start" />
<ion-label>{{ t('settings.resetOnboarding') }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
IonList, IonItem, IonLabel, IonIcon, IonToggle, IonSelect, IonSelectOption,
} from '@ionic/vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Preferences } from '@capacitor/preferences';
import { useTheme } from '../composables/useTheme';
import { useOnboarding } from '../composables/useOnboarding';
import { usePurchases } from '../composables/usePurchases';
import type { ThemeMode } from '../utils/theme';
const { t, locale } = useI18n();
const router = useRouter();
const { getTheme, setTheme } = useTheme();
const { reset } = useOnboarding();
const { isPremium } = usePurchases();
const currentLang = ref('en');
const currentTheme = ref<ThemeMode>('system');
const notificationsEnabled = ref(false);
const premium = ref(false);
onMounted(async () => {
currentLang.value = locale.value || 'en';
currentTheme.value = await getTheme();
premium.value = await isPremium();
});
function changeLanguage(event: CustomEvent) {
const lang = event.detail.value;
currentLang.value = lang;
locale.value = lang;
Preferences.set({ key: 'language', value: lang });
}
function changeTheme(event: CustomEvent) {
const mode = event.detail.value as ThemeMode;
currentTheme.value = mode;
setTheme(mode);
}
function toggleNotifications(event: CustomEvent) {
notificationsEnabled.value = event.detail.checked;
}
function removeAds() {
router.push('/paywall');
}
async function resetOnboardingFlow() {
await reset();
router.replace('/onboarding');
}
</script>Localization
国际化
Translation Files (Shared - All Frameworks)
翻译文件(全框架共享)
json
// 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"
}
}json
// 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"
}
}json
// 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"
}
}json
// 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"
}
}TURKISH LOCALIZATION (IMPORTANT)
土耳其语国际化(重要)
When writing , you MUST use correct Turkish characters:
tr.json- ı (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"
编写时,必须使用正确的土耳其语字符:
tr.json- ı(无点小写i) - 切勿使用i
- İ(有点大写I) - 切勿使用I
- ü, Ü, ö, Ö, ç, Ç, ş, Ş, ğ, Ğ
示例:
- ✅ "Ayarlar", "Giriş", "Çıkış", "Başla", "İleri", "Güncelle"
- ❌ "Ayarlar", "Giris", "Cikis", "Basla", "Ileri", "Guncelle"
i18n Setup (Angular)
Angular i18n配置
i18n is configured in using . See the App Configuration (Angular) section.
app.config.ts@ngx-translate/coreUsage in template files:
.htmlhtml
{{ 'settings.title' | translate }}Detect language in :
app.component.tstypescript
import { TranslateService } from '@ngx-translate/core';
constructor(private translate: TranslateService) {
const browserLang = navigator.language.split('-')[0];
translate.setDefaultLang('en');
translate.use(['en', 'tr'].includes(browserLang) ? browserLang : 'en');
}已在中使用配置i18n,请查看上述Angular应用配置部分。
app.config.ts@ngx-translate/core在模板中的使用方式:
.htmlhtml
{{ 'settings.title' | translate }}在中检测语言:
app.component.tstypescript
import { TranslateService } from '@ngx-translate/core';
constructor(private translate: TranslateService) {
const browserLang = navigator.language.split('-')[0];
translate.setDefaultLang('en');
translate.use(['en', 'tr'].includes(browserLang) ? browserLang : 'en');
}i18n Setup (React)
React i18n配置
i18n is configured in . See the App Configuration (React) section.
i18n/index.tsUsage in components:
tsx
import { useTranslation } from 'react-i18next';
const MyComponent: React.FC = () => {
const { t } = useTranslation();
return <h1>{t('settings.title')}</h1>;
};已在中配置i18n,请查看上述React应用配置部分。
i18n/index.ts在组件中的使用方式:
tsx
import { useTranslation } from 'react-i18next';
const MyComponent: React.FC = () => {
const { t } = useTranslation();
return <h1>{t('settings.title')}</h1>;
};i18n Setup (Vue)
Vue i18n配置
i18n is configured in using . See the App Configuration (Vue) section.
main.tsvue-i18nUsage in templates:
vue
<template>
<h1>{{ t('settings.title') }}</h1>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>已在中使用配置i18n,请查看上述Vue应用配置部分。
main.tsvue-i18n在模板中的使用方式:
vue
<template>
<h1>{{ t('settings.title') }}</h1>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>Framework Best Practices
框架最佳实践
Angular Best Practices
Angular最佳实践
ALWAYS use standalone components with separate HTML, TS, and SCSS files:
❌ WRONG (NgModules + IonicModule):
typescript
// home.module.ts
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [HomePage],
})
export class HomePageModule {}❌ WRONG (inline templates):
typescript
@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 {}✅ CORRECT (separate .html, .ts, .scss files):
html
<!-- home/home.page.html -->
<ion-header>
<ion-toolbar>
<ion-title>{{ 'home.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<!-- content -->
</ion-content>scss
// home/home.page.scss
// Page-specific styles heretypescript
// home/home.page.ts
@Component({
selector: 'app-home',
standalone: true,
imports: [IonContent, IonHeader, IonTitle, IonToolbar, TranslateModule],
templateUrl: './home.page.html',
styleUrls: ['./home.page.scss'],
})
export class HomePage {}Import individual Ionic components (e.g., , ) - NEVER import in standalone components.
ALWAYS use + - NEVER use inline or .
IonContentIonButtonIonicModuletemplateUrlstyleUrlstemplatestyles始终使用独立组件,并搭配单独的HTML、TS和SCSS文件:
❌ 错误示例(NgModules + IonicModule):
typescript
// home.module.ts
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [HomePage],
})
export class HomePageModule {}❌ 错误示例(内联模板):
typescript
@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>
<!-- 内容 -->
</ion-content>
`,
})
export class HomePage {}✅ 正确示例(单独的.html、.ts、.scss文件):
html
<!-- home/home.page.html -->
<ion-header>
<ion-toolbar>
<ion-title>{{ 'home.title' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<!-- 内容 -->
</ion-content>scss
// home/home.page.scss
// 页面专属样式typescript
// home/home.page.ts
@Component({
selector: 'app-home',
standalone: true,
imports: [IonContent, IonHeader, IonTitle, IonToolbar, TranslateModule],
templateUrl: './home.page.html',
styleUrls: ['./home.page.scss'],
})
export class HomePage {}导入单个Ionic组件(如、) - 切勿在独立组件中导入。
始终使用 + - 切勿使用内联或。
IonContentIonButtonIonicModuletemplateUrlstyleUrlstemplatestylesReact Best Practices
React最佳实践
ALWAYS use functional components with hooks:
❌ WRONG:
tsx
class HomePage extends React.Component {
render() {
return <IonContent>...</IonContent>;
}
}✅ CORRECT:
tsx
import { IonContent, IonHeader, IonTitle, IonToolbar, IonPage } from '@ionic/react';
import { useTranslation } from 'react-i18next';
const HomePage: React.FC = () => {
const { t } = useTranslation();
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('home.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
{/* content */}
</IonContent>
</IonPage>
);
};
export default HomePage;Always wrap page content in for proper Ionic page transitions and lifecycle.
<IonPage>始终使用带钩子的函数式组件:
❌ 错误示例:
tsx
class HomePage extends React.Component {
render() {
return <IonContent>...</IonContent>;
}
}✅ 正确示例:
tsx
import { IonContent, IonHeader, IonTitle, IonToolbar, IonPage } from '@ionic/react';
import { useTranslation } from 'react-i18next';
const HomePage: React.FC = () => {
const { t } = useTranslation();
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>{t('home.title')}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
{/* 内容 */}
</IonContent>
</IonPage>
);
};
export default HomePage;始终将页面内容包裹在中,以确保正确的Ionic页面过渡和生命周期。
<IonPage>Vue Best Practices
Vue最佳实践
ALWAYS use Composition API with :
<script setup>❌ WRONG:
vue
<script>
export default {
data() {
return { title: 'Home' };
},
};
</script>✅ CORRECT:
vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('home.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<!-- content -->
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>Always wrap page content in for proper Ionic page transitions and lifecycle.
<ion-page>始终使用组合式API搭配:
<script setup>❌ 错误示例:
vue
<script>
export default {
data() {
return { title: 'Home' };
},
};
</script>✅ 正确示例:
vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ t('home.title') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<!-- 内容 -->
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>始终将页面内容包裹在中,以确保正确的Ionic页面过渡和生命周期。
<ion-page>POST-CREATION CLEANUP (ALWAYS DO)
创建后清理(必须执行)
After creating a new Ionic project, you MUST:
- Remove default generated pages that conflict with your structure
- Ensure the build configuration has the correct output directory
- Verify has the correct
capacitor.config.tsandappIdappName - Check that all Ionic imports use the correct framework-specific package
创建新Ionic项目后,必须:
- 删除与你的结构冲突的默认生成页面
- 确保构建配置中的输出目录正确
- 验证中的
capacitor.config.ts和appId正确appName - 检查所有Ionic导入是否使用了正确的框架专属包
AFTER COMPLETING CODE (ALWAYS RUN)
代码完成后(必须运行)
Angular
Angular
bash
npm install
npx ng build
npx cap syncbash
npm install
npx ng build
npx cap syncReact
React
bash
npm install
npm run build
npx cap syncbash
npm install
npm run build
npx cap syncVue
Vue
bash
npm install
npm run build
npx cap sync- installs any new dependencies
npm install - Build compiles the project
- syncs web assets and plugins to native projects
cap sync
Do NOT skip these steps.
bash
npm install
npm run build
npx cap sync- 安装所有新依赖
npm install - 构建命令编译项目
- 将Web资源和插件同步到原生项目
cap sync
切勿跳过这些步骤。
Development Commands
开发命令
Angular
Angular
bash
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/emulatorbash
npm install
ionic serve # 在浏览器中运行
ionic build # 生产环境构建
npx cap sync # 将Web资源同步到原生项目
npx cap open ios # 在Xcode中打开
npx cap open android # 在Android Studio中打开
npx cap run ios # 构建并运行在iOS设备/模拟器
npx cap run android # 构建并运行在Android设备/模拟器React
React
bash
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
npx cap run android # Build and run on Androidbash
npm install
ionic serve # 在浏览器中运行
ionic build # 生产环境构建
npx cap sync # 将Web资源同步到原生项目
npx cap open ios # 在Xcode中打开
npx cap open android # 在Android Studio中打开
npx cap run ios # 构建并运行在iOS
npx cap run android # 构建并运行在AndroidVue
Vue
bash
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
npx cap run android # Build and run on Androidbash
npm install
ionic serve # 在浏览器中运行
ionic build # 生产环境构建
npx cap sync # 将Web资源同步到原生项目
npx cap open ios # 在Xcode中打开
npx cap open android # 在Android Studio中打开
npx cap run ios # 构建并运行在iOS
npx cap run android # 构建并运行在AndroidCoding Standards
编码规范
All Frameworks
全框架通用
- Strict TypeScript - no types
any - Avoid hardcoded strings - use translation keys
- Use for persistent storage
@capacitor/preferences - Lazy-load all pages
- Use to guard native-only code
Capacitor.isNativePlatform() - Always Capacitor plugin methods
await
- 严格使用TypeScript - 禁止使用类型
any - 避免硬编码字符串 - 使用翻译键
- 使用进行持久化存储
@capacitor/preferences - 懒加载所有页面
- 使用包裹仅原生平台可用的代码
Capacitor.isNativePlatform() - 始终使用处理Capacitor插件方法
await
Angular-Specific
Angular专属
- Use standalone components (NEVER NgModules for pages/components)
- ALWAYS use separate ,
.html,.tsfiles - NEVER inline.scssortemplatestyles - Use and
templateUrlinstyleUrlsdecorator@Component - Use Angular's function or constructor injection
inject() - Lazy-load via in routes
loadComponent - Use Angular Signals for reactive state when possible
- Import individual Ionic components - NEVER
IonicModule - Use from
addIcons()to register iconsionicons
- 使用独立组件(切勿为页面/组件使用NgModules)
- 始终使用单独的、
.html、.ts文件 - 切勿使用内联.scss或templatestyles - 在装饰器中使用
@Component和templateUrlstyleUrls - 使用Angular的函数或构造函数注入
inject() - 通过路由中的实现懒加载
loadComponent - 尽可能使用Angular Signals进行响应式状态管理
- 导入单个Ionic组件 - 切勿导入
IonicModule - 使用中的
ionicons注册图标addIcons()
React-Specific
React专属
- Use functional components with hooks
- Wrap pages in
<IonPage> - Use for navigation
useIonRouter() - Use for i18n
useTranslation() - Import icons directly from
ionicons/icons
- 使用带钩子的函数式组件
- 将页面包裹在中
<IonPage> - 使用进行导航
useIonRouter() - 使用进行国际化
useTranslation() - 直接从导入图标
ionicons/icons
Vue-Specific
Vue专属
- Use Composition API with
<script setup lang="ts"> - Wrap pages in
<ion-page> - Use from
useRouter()for navigationvue-router - Use for i18n
useI18n() - Import icons from and pass via
ionicons/iconsprop:icon
- 使用组合式API搭配
<script setup lang="ts"> - 将页面包裹在中
<ion-page> - 使用的
vue-router进行导航useRouter() - 使用进行国际化
useI18n() - 从导入图标,并通过
ionicons/icons属性传递:icon
Important Notes
重要注意事项
- iOS permissions are defined in (added via Capacitor plugins)
Info.plist - Android permissions are defined in (added via Capacitor plugins)
AndroidManifest.xml - Always run after installing new Capacitor plugins
npx cap sync - Use to guard native-only code
Capacitor.isNativePlatform() - Test in browser with for rapid development, then test on devices
ionic serve
- iOS权限定义在中(由Capacitor插件自动添加)
Info.plist - Android权限定义在中(由Capacitor插件自动添加)
AndroidManifest.xml - 安装新Capacitor插件后始终运行
npx cap sync - 使用包裹仅原生平台可用的代码
Capacitor.isNativePlatform() - 使用在浏览器中快速开发,然后在设备上测试
ionic serve
App Store & Play Store Notes
App Store和Play Store注意事项
- iOS ATT permission required for personalized ads
- Restore purchases must work correctly
- Target SDK must be up to date
- Use before each native build
npx cap sync
- iOS需要ATT权限才能使用个性化广告
- 恢复购买功能必须正常工作
- 目标SDK必须是最新版本
- 每次原生构建前运行
npx cap sync
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
- 所有语言的UI测试
- 暗色/亮色主题
- 通知功能
- 订阅流程
- 恢复购买
- 离线支持
- 多种屏幕尺寸
- 浏览器(ionic serve)和原生平台
After Development
开发完成后
bash
npm run build
npx cap sync
npx cap open ios
npx cap open androidNOTE:copies the built web app to native projects and syncs plugins. Run it after every build before testing on native platforms.cap sync
bash
npm run build
npx cap sync
npx cap open ios
npx cap open android注意:会将构建后的Web应用复制到原生项目并同步插件。在原生平台测试前,每次构建后都要运行该命令。cap sync