mcpField
I
ionic-skills

ionic-skills

github.com/erkamyaman開発1654 回インストール1 週間前に更新
リポジトリ

ionic-skills について

ionic-skillsは、Angular、React、VueでIonic Capacitorのモバイルアプリを開発するためのガイドです。ナビゲーション、必須ページ、プラグイン、ストレージの規約と、各フレームワーク向けの実装パターンを定めています。

主な機能

  1. 01Angular、React、Vueに対応
  2. 02オンボーディング、ペイウォール、設定ページを規定
  3. 03ion-tabsによるタブナビゲーション
  4. 04Capacitorプラグインとストレージの規約
  5. 05ルーティングと設定の例を掲載

活用シーン

  1. 01Ionic Capacitorアプリの開発開始
  2. 02フレームワーク別のアプリ構成設計
  3. 03ストレージとネイティブプラグインの実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/erkamyaman/ionic-capacitor-skills --skill ionic-skills

インストール後、Claude Code で/ionic-skillsを入力して読み込まれたか確認します。

同梱ファイルmetadata.json

詳細

Ionic Capacitor アプリ開発ガイド

IMPORTANT: これはプロジェクトではなく、SKILL ファイルです。このフォルダーで npm install を絶対に実行しないでください。この場所にコードファイルを絶対に作成しないでください。新しいプロジェクトを作成する際は、必ず最初にユーザーにプロジェクトのパスを確認するか、別のディレクトリ(例:~/Projects/app-name)に作成してください。

このガイドでは、Angular、React、または Vue を使用した Ionic Capacitor による本番環境対応のモバイルアプリの構築方法を説明します。


必須要件

新しい Ionic プロジェクトを作成する際は、以下のすべてを必ず含めてください。

必須ページ(必ず作成)
  • オンボーディングページ - 全画面の背景動画とグラデーションオーバーレイを備えた、スワイプ式のオンボーディング
  • ペイウォールページ - RevenueCat のペイウォールページ(オンボーディングの後に表示)
  • 設定ページ - 言語、テーマ、通知、オンボーディングのリセットオプションを備えた設定ページ
必須ナビゲーション(必ず使用)
  • タブナビゲーションには ion-tabs と ion-tab-bar を使用してください。カスタムタブの実装やサードパーティ製タブライブラリは絶対に使用しないでください。
必須ライブラリ(必ずインストール)
Angular
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob @ngx-translate/core @ngx-translate/http-loader swiper
React
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob react-i18next i18next i18next-http-backend swiper
Vue
npm install @capacitor/preferences @capacitor/push-notifications @capacitor/splash-screen @capacitor/status-bar @revenuecat/purchases-capacitor @capacitor-community/admob vue-i18n swiper
共通ライブラリ(すべてのフレームワーク)
  • @revenuecat/purchases-capacitor(RevenueCat)
  • @capacitor-community/admob(AdMob)
  • @capacitor/push-notifications(プッシュ通知)
  • @capacitor/preferences(キーと値のストレージ)
  • @capacitor/splash-screen(スプラッシュ画面の制御)
  • @capacitor/status-bar(ステータスバーのスタイル設定)
  • swiper(オンボーディングのスライド)
フレームワーク別の i18n ライブラリ
フレームワークライブラリ用途
Angular@ngx-translate/core + @ngx-translate/http-loadertranslate パイプ
Reactreact-i18next + i18nextuseTranslation() フック
Vuevue-i18nuseI18n() コンポーザブル / $t()

禁止事項(絶対に使用しない)

すべてのフレームワーク
  • ❌ localStorage を直接使用しないでください。代わりに @capacitor/preferences を使用してください。
  • ❌ @ionic/storage を使用しないでください。代わりに @capacitor/preferences を使用してください。
  • ❌ カスタムタブバーを使用しないでください。代わりに ion-tabs + ion-tab-bar を使用してください。
  • ❌ cordova-plugin-* プラグインを使用しないでください。代わりに Capacitor プラグインを使用してください。
  • ❌ any 型を使用しないでください。必ず適切な TypeScript 型を使用してください。
  • ❌ ngx-admob-free やその他の非推奨の広告ライブラリを使用しないでください。@capacitor-community/admob のみを使用してください。
  • ❌ Capacitor の同期呼び出しを使用しないでください。Capacitor プラグインのメソッドは必ず await してください。
Angular 固有
  • ❌ 新しいページやコンポーネントに NgModules を使用しないでください。スタンドアロンコンポーネントを使用してください。
  • ❌ スタンドアロンコンポーネントで IonicModule を使用しないでください。個々のコンポーネント(IonButton、IonContent など)をインポートしてください。
  • ❌ @Component 内に template や styles をインラインで記述しないでください。templateUrl と styleUrls を使用して、別々の .html、.ts、.scss ファイルを使用してください。
  • ❌ @angular/http(非推奨)を使用しないでください。@angular/common/http を使用してください。
React 固有
  • ❌ クラスコンポーネントを使用しないでください。フックを使った関数コンポーネントを使用してください。
  • ❌ DOM を直接操作しないでください。React の参照と状態を使用してください。
  • ❌ @ionic/angular のインポートを使用しないでください。@ionic/react を使用してください。
Vue 固有
  • ❌ 新しいコードに Options API を使用しないでください。<script setup> を使った Composition API を使用してください。
  • ❌ DOM を直接操作しないでください。Vue の参照とリアクティビティを使用してください。
  • ❌ @ionic/angular のインポートを使用しないでください。@ionic/vue を使用してください。

技術スタック

懸念事項AngularReactVue
フレームワークIonic 8 + Angular 19Ionic 8 + React 19Ionic 8 + Vue 3.5
ネイティブランタイムCapacitor 7Capacitor 7Capacitor 7
ナビゲーションAngular Router(遅延読み込み)@ionic/react-router@ionic/vue-router
タブナビゲーションion-tabs + ion-tab-barion-tabs + ion-tab-barion-tabs + ion-tab-bar
状態管理Angular Services(Signals/RxJS)カスタムフック / ContextComposables / Pinia
翻訳@ngx-translate/corereact-i18nextvue-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

WARNING:localStorageを直接使用しないでください!クロスプラットフォームの永続ストレージには、代わりに@capacitor/preferencesを使用してください。


プロジェクトの作成

ユーザーからアプリの作成を依頼された場合、必ず次の手順を実行してください。

  1. 最初にバンドル ID を確認します(例:「バンドル ID は何ですか?例:com.company.appname」)
  2. 現在のディレクトリにプロジェクトを作成します
  3. その後、必要なすべてのページを実装します
プロジェクトの作成(Angular)
npm install -g @ionic/cli
ionic start app-name blank --type=angular --capacitor
プロジェクトの作成(React)
npm install -g @ionic/cli
ionic start app-name blank --type=react --capacitor
プロジェクトの作成(Vue)
npm install -g @ionic/cli
ionic start app-name blank --type=vue --capacitor
Capacitor の設定(すべてのフレームワーク)
// 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:React/Vue プロジェクトでは、webDirは'www'ではなく'dist'の場合があります。プロジェクトのビルド出力ディレクトリを確認してください。

ネイティブプラットフォームの追加
npx cap add ios
npx cap add android

プロジェクト構成

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.json
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.json
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.json

アプリの設定

アプリの設定(Angular)
// 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],
        },
      })
    ),
  ],
};

<ion-app>
  <ion-router-outlet></ion-router-outlet>
</ion-app>
// 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();
  }
}
アプリの設定(React)
// 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 />);
// 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;
// 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;
アプリの設定(Vue)
// 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'));

<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>

ルーティング

ルーティング(Angular)
// 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',
  },
];
// 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',
      },
    ],
  },
];
ルーティング(React)

ルートはApp.tsxで定義します(上記の「アプリの設定」を参照)。タブルートはTabsLayoutコンポーネントで定義します。

// 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;
ルーティング(Vue)
// 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;

<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>

共通ユーティリティ関数(フレームワーク非依存)

これらのユーティリティファイルには、Capacitor プラグインの呼び出しを含む純粋な TypeScript が記述されています。すべてのフレームワークで使用されます。各フレームワークでは、それぞれのパターン(Angular サービス、React フック、Vue コンポーザブル)でラップします。

ストレージ(すべてのフレームワーク)
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' });
オンボーディング状態ユーティリティ
// 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 });
}
テーマユーティリティ
// 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 ユーティリティ
// 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();
}

開発やテストには、テスト用広告 ID を使用してください。

  • バナー:ca-app-pub-3940256099942544/6300978111

AdMob の初期化を省略しないでください。省略すると、プラグインが正しく動作しません。

RevenueCat ユーティリティ
// 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;
  }
}
通知ユーティリティ
// 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);
  });
}

フレームワーク固有のサービスラッパー

Angular サービス
// 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;
}
// 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;
}
// 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();
  }
}
// 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;
}
// 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/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 };
}
// 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), []),
  };
}
// 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 };
}
// 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(), []),
  };
}
// 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/useOnboarding.ts
import { isOnboardingCompleted, setOnboardingCompleted, resetOnboarding } from '../utils/onboarding';

export function useOnboarding() {
  return {
    isCompleted: isOnboardingCompleted,
    setCompleted: setOnboardingCompleted,
    reset: resetOnboarding,
  };
}
// 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 };
}
// 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 };
}
// composables/usePurchases.ts
import {
  initializePurchases,
  isPremiumUser,
  getOfferings,
  purchasePackage,
  restorePurchases,
} from '../utils/purchases';

export function usePurchases() {
  return {
    initialize: initializePurchases,
    isPremium: isPremiumUser,
    getOfferings,
    purchase: purchasePackage,
    restorePurchases,
  };
}
// composables/useNotifications.ts
import { requestNotificationPermission, addNotificationListeners } from '../utils/notifications';

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

オンボーディングガード

オンボーディングガード(Angular)
// 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;
};
オンボーディングガード(React)
// 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;
};
オンボーディングガード(Vue)

Vue のオンボーディングガードはrouter.beforeEachフックとして実装されています。上記の「ルーティング(Vue)」セクションを参照してください。


オンボーディングページ

共通動画 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 の<video>を使用してください。canvas、アニメーション GIF、外部プレーヤーは使用しないでください。

オンボーディングページ(Angular)

<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">
    
  </div>
</ion-content>
// 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%;
}
// 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 });
  }
}
オンボーディングページ(React)
// 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;
オンボーディングページ(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">
      
    </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>

ペイウォールページ

重要:オンボーディングの完了直後に、必ずペイウォールを表示してください。

ペイウォールには、必ず次の2つのサブスクリプションオプションを用意してください。

  1. 週払い - デフォルトのオプション
  2. 年払い - 「50% OFF」バッジ付き(おすすめとして、強調表示してください)

ボタンは3つです。「購読する」、「広告を見て続ける」、「購入を復元」。

フロー:Onboarding -> Paywall -> Main App (tabs)

ペイウォールページ(Angular)

<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>
// 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
  }
}
// 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 });
    }
  }
}
ペイウォールページ(React)
// 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;
ペイウォールページ(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>

タブナビゲーション

よく使う Ionicons
用途Ionicon 名
ホームhome
探索compass
設定settings
プロフィールperson
検索search
お気に入りheart
通知notifications
タブナビゲーション(Angular)

<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>
// 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();
  }
}
タブナビゲーション(React)

上記のルーティング(React)セクションにある TabsLayout コンポーネントを参照してください。

タブナビゲーション(Vue)

上記のルーティング(Vue)セクションにある TabsLayout.vue コンポーネントを参照してください。


設定ページ

設定ページには、必ず次の項目を含めてください。

  1. 言語 - アプリの言語を変更します
  2. テーマ - ライト/ダーク/システム
  3. 通知 - 通知の有効/無効を切り替えます
  4. 広告を削除 - ペイウォールに移動します(すでにプレミアムの場合は非表示)
  5. オンボーディングをリセット - オンボーディングのフローを再開します(テスト/デモ用)
設定ページ(Angular)

<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>
// settings/settings.page.scss
// Add your settings page styles here
// 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 });
  }
}
設定ページ(React)
// 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;
設定ページ(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>

ローカライズ

翻訳ファイル(共通 - すべてのフレームワーク)
// 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"
  }
}
// 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"
  }
}
トルコ語のローカライズ(重要)

tr.json を作成する際は、必ず正しいトルコ語の文字を使用してください。

  • ı(小文字のドットなし i) - i ではありません
  • İ(大文字のドット付き I) - I ではありません
  • ü、Ü、ö、Ö、ç、Ç、ş、Ş、ğ、Ğ

例:

  • ✅ 「Ayarlar」、「Giriş」、「Çıkış」、「Başla」、「İleri」、「Güncelle」
  • ❌ 「Ayarlar」、「Giris」、「Cikis」、「Basla」、「Ileri」、「Guncelle」
i18n の設定(Angular)

i18n は @ngx-translate/core を使用して app.config.ts で設定されています。アプリの設定(Angular)セクションを参照してください。

.html テンプレートファイルでの使用方法:

{{ 'settings.title' | translate }}

app.component.ts で言語を検出します。

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 の設定(React)

i18n は i18n/index.ts で設定されています。アプリの設定(React)セクションを参照してください。

コンポーネントでの使用方法:

import { useTranslation } from 'react-i18next';

const MyComponent: React.FC = () => {
  const { t } = useTranslation();
  return <h1>{t('settings.title')}</h1>;
};
i18n の設定(Vue)

i18n は vue-i18n を使用して main.ts で設定されています。アプリの設定(Vue)セクションを参照してください。

テンプレートでの使用方法:

<template>
  <h1>{{ t('settings.title') }}</h1>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>

フレームワークのベストプラクティス

Angular のベストプラクティス

必ず HTML、TS、SCSS ファイルを分けたスタンドアロンコンポーネントを使用してください。

❌ 誤り(NgModules + IonicModule):

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

❌ 誤り(インラインテンプレート):

@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 ファイルを分ける):


<ion-header>
  <ion-toolbar>
    <ion-title>{{ 'home.title' | translate }}</ion-title>
  </ion-toolbar>
</ion-header>
<ion-content>
  
</ion-content>
// home/home.page.scss
// Page-specific styles here
// 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 コンポーネント(例:IonContent、IonButton)をインポートしてください。スタンドアロンコンポーネントでは、IonicModule をインポートしないでください。 必ず templateUrl と styleUrls を使用してください。インラインの template や styles は使用しないでください。

React のベストプラクティス

必ずフックを使った関数コンポーネントを使用してください。

❌ 誤り:

class HomePage extends React.Component {
  render() {
    return <IonContent>...</IonContent>;
  }
}

✅ 正しい:

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;

Ionic のページ遷移とライフサイクルを適切に機能させるため、ページのコンテンツは必ず <IonPage> でラップしてください。

Vue のベストプラクティス

必ず <script setup> を使った Composition API を使用してください。

❌ 誤り:

<script>
export default {
  data() {
    return { title: 'Home' };
  },
};
</script>

✅ 正しい:

<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> でラップしてください。


作成後のクリーンアップ(必ず実施)

新しい Ionic プロジェクトを作成した後は、必ず次の作業を行ってください。

  1. プロジェクトの構成と競合する、デフォルトで生成されたページを削除します
  2. ビルド設定で正しい出力ディレクトリが指定されていることを確認します
  3. capacitor.config.ts に正しい appId と appName が設定されていることを確認します
  4. すべての Ionic インポートで、フレームワークに対応した正しいパッケージが使用されていることを確認します

コードの完成後に実行(必ず実行)

Angular
npm install
npx ng build
npx cap sync
React
npm install
npm run build
npx cap sync
Vue
npm install
npm run build
npx cap sync
  1. npm install で新しい依存関係をインストールします
  2. ビルドでプロジェクトをコンパイルします
  3. cap sync でウェブアセットとプラグインをネイティブプロジェクトに同期します

これらの手順は必ず実行してください。


開発コマンド

Angular
npm install
ionic serve          # Run in browser
ionic build          # Build for production
npx cap sync         # Sync web assets to native
npx cap open ios     # Open in Xcode
npx cap open android # Open in Android Studio
npx cap run ios      # Build and run on iOS device/simulator
npx cap run android  # Build and run on Android device/emulator
React
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 Android
Vue
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 Android

コーディング規約

すべてのフレームワーク
  • TypeScript の厳格な型チェックを使用します。any 型は禁止です
  • 文字列をハードコードせず、翻訳キーを使用します
  • 永続ストレージには @capacitor/preferences を使用します
  • すべてのページを遅延読み込みします
  • ネイティブ専用コードには Capacitor.isNativePlatform() を使用してガードします
  • Capacitor プラグインのメソッドは必ず await します
Angular 固有
  • スタンドアロンコンポーネントを使用します(ページやコンポーネントに NgModules は絶対に使用しません)
  • 必ず個別の .html、.ts、.scss ファイルを使用します。インラインの template や styles は絶対に使用しません
  • @Component デコレーターでは templateUrl と styleUrls を使用します
  • Angular の inject() 関数、またはコンストラクターインジェクションを使用します
  • ルートでは loadComponent を使用して遅延読み込みします
  • 可能な場合は、リアクティブな状態管理に Angular Signals を使用します
  • Ionic コンポーネントを個別にインポートします。IonicModule は絶対に使用しません
  • ionicons の addIcons() を使用してアイコンを登録します
React 固有
  • フックを使用した関数コンポーネントを使用します
  • ページを <IonPage> で囲みます
  • ナビゲーションには useIonRouter() を使用します
  • i18n には useTranslation() を使用します
  • アイコンは ionicons/icons から直接インポートします
Vue 固有
  • <script setup lang="ts"> を使用した Composition API を使用します
  • ページを <ion-page> で囲みます
  • ナビゲーションには vue-router の useRouter() を使用します
  • i18n には useI18n() を使用します
  • アイコンは ionicons/icons からインポートし、:icon プロパティで渡します

重要事項

  1. iOS の権限は Info.plist で定義します(Capacitor プラグインによって追加されます)
  2. Android の権限は AndroidManifest.xml で定義します(Capacitor プラグインによって追加されます)
  3. 新しい Capacitor プラグインをインストールした後は、必ず npx cap sync を実行します
  4. ネイティブ専用コードには Capacitor.isNativePlatform() を使用してガードします
  5. 迅速な開発には ionic serve を使ってブラウザーでテストし、その後デバイスでテストします

App Store と Play Store に関する注意事項

  • パーソナライズ広告には iOS ATT 権限が必要です
  • 購入の復元が正しく動作する必要があります
  • Target SDK は最新の状態に保つ必要があります
  • ネイティブビルドの前には npx cap sync を使用します

テストチェックリスト

  • すべての言語で UI をテスト済み
  • ダークモード/ライトモード
  • 通知
  • プレミアムのフロー
  • 購入の復元
  • オフライン対応
  • 複数の画面サイズ
  • ブラウザー(ionic serve)とネイティブプラットフォーム

開発後

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

注:cap sync はビルド済みのウェブアプリをネイティブプロジェクトにコピーし、プラグインを同期します。ネイティブプラットフォームでテストする前に、ビルドのたびに実行してください。

コメント