React Native

モバイルプラットフォームReactクロスプラットフォームJavaScriptTypeScriptMeta

モバイルプラットフォーム

React Native

概要

React Nativeは、Meta(旧Facebook)が開発するクロスプラットフォームモバイル開発フレームワークとして、Reactの宣言的UIパラダイムとJavaScript/TypeScriptによるネイティブアプリ開発を実現します。「Learn Once, Write Anywhere」の哲学のもと、iOS・Android両プラットフォームで真のネイティブパフォーマンスを提供しながら、Web開発者にとって親しみやすい開発体験を提供します。2025年現在、New Architecture(TurboModules + Fabric)の安定化、React 18との完全統合、TypeScript First開発体験、そして強化されたパフォーマンス最適化により、エンタープライズ級アプリケーション開発の最前線プラットフォームとして進化しています。

詳細

React Native 2025年版は、New Architecture の完全安定化により、従来のBridgeベースの制限を突破し、JSI(JavaScript Interface)による直接的なネイティブモジュール通信とFabricレンダラーによる高速UI更新を実現しています。特に注目すべきは、React 18のConcurrent Featuresとの深い統合により、Suspense、Transitions、自動バッチングなどの最新React機能をモバイルアプリでフル活用できることです。Metro bundlerの高速化、Flipper統合の強化、Expo Dev Toolsとの連携、そしてCodegen による自動型生成により、開発効率とアプリケーション品質の両立を図る包括的な開発プラットフォームを提供しています。

主な特徴

  • New Architecture: TurboModules + Fabricによる高速レンダリング
  • React 18統合: Concurrent Features、Suspense、Transitionsサポート
  • TypeScript First: 完全な型安全性と優れた開発体験
  • Hot Reloading: 瞬時のコード変更反映とデバッグ効率
  • ネイティブモジュール: iOS・Android固有機能への直接アクセス
  • 豊富なエコシステム: 膨大なライブラリとコミュニティサポート

2025年の最新機能

  • New Architecture安定版: TurboModules、Fabric、JSIの本格導入
  • React 18完全対応: Concurrent Features、Automatic Batchingサポート
  • TypeScript 5.0対応: 最新型システムと開発ツール統合
  • Performance Optimizations: 起動時間50%短縮、メモリ使用量削減
  • Enhanced Developer Experience: Flipper v2、Metro 改良、デバッグ強化
  • Web Platform Alignment: React Native Web との統合強化

メリット・デメリット

メリット

  • Web開発者にとって学習コストが低いReactベース開発
  • iOS・Android間での高いコード共有率(70-90%)
  • 真のネイティブパフォーマンスとUI体験
  • 豊富なサードパーティライブラリとコミュニティ
  • Hot Reloadingによる高速開発サイクル
  • Meta による継続的な開発と大企業での実績
  • Expo との統合による開発効率化

デメリット

  • 大規模アプリでの複雑性管理の難しさ
  • New Architecture 移行の学習コストと互換性問題
  • ネイティブモジュール開発にはプラットフォーム固有知識が必要
  • アプリサイズがネイティブ開発より大きくなる傾向
  • プラットフォーム固有UIの完全な再現に制限
  • デバッグの複雑さ(JavaScript、Native Bridge、ネイティブコード)

参考ページ

書き方の例

セットアップと基本設定

# React Native CLI のインストール
npm install -g react-native-cli

# 新しいプロジェクトの作成
npx react-native init MyAwesomeApp --template react-native-template-typescript

# プロジェクトディレクトリに移動
cd MyAwesomeApp

# 依存関係のインストール
npm install

# iOS用の CocoaPods 依存関係インストール(macOSのみ)
cd ios && pod install && cd ..

# Android エミュレータでの実行
npx react-native run-android

# iOS シミュレータでの実行(macOSのみ)
npx react-native run-ios

# Metro bundler の手動起動
npx react-native start

# 新規プロジェクト用の必須ライブラリインストール
npm install @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs
npm install react-native-screens react-native-safe-area-context
npm install react-native-gesture-handler react-native-reanimated
npm install @reduxjs/toolkit react-redux
npm install react-native-async-storage/async-storage
// App.tsx - メインアプリケーションコンポーネント
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import { store, persistor } from './src/store/store';
import HomeScreen from './src/screens/HomeScreen';
import ProfileScreen from './src/screens/ProfileScreen';
import LoadingScreen from './src/components/LoadingScreen';

export type RootStackParamList = {
  Home: undefined;
  Profile: { userId: string };
  Settings: undefined;
};

const Stack = createStackNavigator<RootStackParamList>();

const App: React.FC = () => {
  return (
    <Provider store={store}>
      <PersistGate loading={<LoadingScreen />} persistor={persistor}>
        <SafeAreaProvider>
          <NavigationContainer>
            <Stack.Navigator
              initialRouteName="Home"
              screenOptions={{
                headerStyle: {
                  backgroundColor: '#6200EE',
                },
                headerTintColor: '#fff',
                headerTitleStyle: {
                  fontWeight: 'bold',
                },
              }}
            >
              <Stack.Screen 
                name="Home" 
                component={HomeScreen}
                options={{ title: 'ホーム' }}
              />
              <Stack.Screen 
                name="Profile" 
                component={ProfileScreen}
                options={({ route }) => ({ 
                  title: `プロフィール: ${route.params.userId}` 
                })}
              />
            </Stack.Navigator>
          </NavigationContainer>
        </SafeAreaProvider>
      </PersistGate>
    </Provider>
  );
};

export default App;

認証とユーザー管理

// src/services/AuthService.ts - 認証サービス
import AsyncStorage from '@react-native-async-storage/async-storage';
import { GoogleSignin, statusCodes } from '@react-native-google-signin/google-signin';
import { LoginManager, AccessToken } from 'react-native-fbsdk-next';
import auth, { FirebaseAuthTypes } from '@react-native-firebase/auth';

export interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  provider: 'email' | 'google' | 'facebook' | 'apple';
  emailVerified: boolean;
  createdAt: Date;
  lastLoginAt: Date;
}

export interface LoginCredentials {
  email: string;
  password: string;
}

export interface RegisterCredentials extends LoginCredentials {
  name: string;
  confirmPassword: string;
}

class AuthService {
  private static instance: AuthService;
  private currentUser: User | null = null;

  private constructor() {
    this.initializeGoogleSignIn();
    this.setupAuthStateListener();
  }

  public static getInstance(): AuthService {
    if (!AuthService.instance) {
      AuthService.instance = new AuthService();
    }
    return AuthService.instance;
  }

  // Google Sign-In の初期化
  private initializeGoogleSignIn(): void {
    GoogleSignin.configure({
      webClientId: 'your-web-client-id.googleusercontent.com',
      offlineAccess: true,
      hostedDomain: '',
      forceCodeForRefreshToken: true,
    });
  }

  // 認証状態リスナーの設定
  private setupAuthStateListener(): void {
    auth().onAuthStateChanged(async (firebaseUser) => {
      if (firebaseUser) {
        await this.handleUserSession(firebaseUser);
      } else {
        this.currentUser = null;
        await AsyncStorage.removeItem('user_token');
      }
    });
  }

  // ユーザーセッション処理
  private async handleUserSession(firebaseUser: FirebaseAuthTypes.User): Promise<void> {
    try {
      const idToken = await firebaseUser.getIdToken();
      await AsyncStorage.setItem('user_token', idToken);

      this.currentUser = {
        id: firebaseUser.uid,
        email: firebaseUser.email!,
        name: firebaseUser.displayName || 'Unknown User',
        avatar: firebaseUser.photoURL || undefined,
        provider: this.getAuthProvider(firebaseUser),
        emailVerified: firebaseUser.emailVerified,
        createdAt: firebaseUser.metadata.creationTime ? new Date(firebaseUser.metadata.creationTime) : new Date(),
        lastLoginAt: firebaseUser.metadata.lastSignInTime ? new Date(firebaseUser.metadata.lastSignInTime) : new Date(),
      };
    } catch (error) {
      console.error('Handle user session error:', error);
    }
  }

  // プロバイダー判定
  private getAuthProvider(user: FirebaseAuthTypes.User): User['provider'] {
    const providerId = user.providerData[0]?.providerId;
    switch (providerId) {
      case 'google.com':
        return 'google';
      case 'facebook.com':
        return 'facebook';
      case 'apple.com':
        return 'apple';
      default:
        return 'email';
    }
  }

  // メール・パスワード登録
  async registerWithEmail(credentials: RegisterCredentials): Promise<User> {
    try {
      if (credentials.password !== credentials.confirmPassword) {
        throw new Error('パスワードが一致しません');
      }

      if (credentials.password.length < 8) {
        throw new Error('パスワードは8文字以上である必要があります');
      }

      const userCredential = await auth().createUserWithEmailAndPassword(
        credentials.email,
        credentials.password
      );

      await userCredential.user.updateProfile({
        displayName: credentials.name,
      });

      await userCredential.user.sendEmailVerification();

      return this.currentUser!;
    } catch (error: any) {
      console.error('Register error:', error);
      throw new Error(this.getFirebaseErrorMessage(error.code));
    }
  }

  // メール・パスワードログイン
  async loginWithEmail(credentials: LoginCredentials): Promise<User> {
    try {
      await auth().signInWithEmailAndPassword(credentials.email, credentials.password);
      return this.currentUser!;
    } catch (error: any) {
      console.error('Login error:', error);
      throw new Error(this.getFirebaseErrorMessage(error.code));
    }
  }

  // Google ログイン
  async loginWithGoogle(): Promise<User> {
    try {
      await GoogleSignin.hasPlayServices();
      const { idToken } = await GoogleSignin.signIn();
      const googleCredential = auth.GoogleAuthProvider.credential(idToken);
      await auth().signInWithCredential(googleCredential);
      return this.currentUser!;
    } catch (error: any) {
      console.error('Google login error:', error);
      
      if (error.code === statusCodes.SIGN_IN_CANCELLED) {
        throw new Error('ログインがキャンセルされました');
      } else if (error.code === statusCodes.IN_PROGRESS) {
        throw new Error('ログイン処理中です');
      } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
        throw new Error('Google Play Services が利用できません');
      }
      
      throw new Error('Googleログインに失敗しました');
    }
  }

  // Facebook ログイン
  async loginWithFacebook(): Promise<User> {
    try {
      const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
      
      if (result.isCancelled) {
        throw new Error('ログインがキャンセルされました');
      }

      const data = await AccessToken.getCurrentAccessToken();
      if (!data) {
        throw new Error('Facebook アクセストークンの取得に失敗しました');
      }

      const facebookCredential = auth.FacebookAuthProvider.credential(data.accessToken);
      await auth().signInWithCredential(facebookCredential);
      return this.currentUser!;
    } catch (error) {
      console.error('Facebook login error:', error);
      throw error;
    }
  }

  // パスワードリセット
  async resetPassword(email: string): Promise<void> {
    try {
      await auth().sendPasswordResetEmail(email);
    } catch (error: any) {
      console.error('Password reset error:', error);
      throw new Error(this.getFirebaseErrorMessage(error.code));
    }
  }

  // メール確認の再送信
  async resendEmailVerification(): Promise<void> {
    try {
      const user = auth().currentUser;
      if (user && !user.emailVerified) {
        await user.sendEmailVerification();
      }
    } catch (error) {
      console.error('Resend email verification error:', error);
      throw error;
    }
  }

  // ログアウト
  async logout(): Promise<void> {
    try {
      await GoogleSignin.signOut();
      await LoginManager.logOut();
      await auth().signOut();
      this.currentUser = null;
      await AsyncStorage.removeItem('user_token');
    } catch (error) {
      console.error('Logout error:', error);
      throw error;
    }
  }

  // 現在のユーザー取得
  getCurrentUser(): User | null {
    return this.currentUser;
  }

  // ログイン状態確認
  isLoggedIn(): boolean {
    return this.currentUser !== null && auth().currentUser !== null;
  }

  // Firebaseエラーメッセージの変換
  private getFirebaseErrorMessage(errorCode: string): string {
    const errorMessages: { [key: string]: string } = {
      'auth/user-not-found': 'ユーザーが見つかりません',
      'auth/wrong-password': 'パスワードが間違っています',
      'auth/email-already-in-use': 'このメールアドレスは既に使用されています',
      'auth/weak-password': 'パスワードが弱すぎます',
      'auth/invalid-email': 'メールアドレスの形式が正しくありません',
      'auth/too-many-requests': 'リクエストが多すぎます。しばらく待ってから再試行してください',
      'auth/network-request-failed': 'ネットワークエラーが発生しました',
    };

    return errorMessages[errorCode] || '認証エラーが発生しました';
  }
}

export default AuthService.getInstance();

バックエンド統合とAPIアクセス

// src/services/ApiService.ts - APIサービス
import AsyncStorage from '@react-native-async-storage/async-storage';

export interface ApiConfig {
  baseURL: string;
  timeout: number;
  retryAttempts: number;
  retryDelay: number;
}

export interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
  errors?: string[];
}

export interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    pages: number;
  };
}

class ApiService {
  private config: ApiConfig;
  private authToken: string | null = null;

  constructor(config: ApiConfig) {
    this.config = config;
    this.loadAuthToken();
  }

  // 認証トークンの読み込み
  private async loadAuthToken(): Promise<void> {
    try {
      this.authToken = await AsyncStorage.getItem('user_token');
    } catch (error) {
      console.error('Load auth token error:', error);
    }
  }

  // 認証トークンの設定
  public setAuthToken(token: string | null): void {
    this.authToken = token;
  }

  // リクエストヘッダーの構築
  private getHeaders(): Record<string, string> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    };

    if (this.authToken) {
      headers['Authorization'] = `Bearer ${this.authToken}`;
    }

    return headers;
  }

  // HTTP リクエストの実行
  private async executeRequest<T>(
    url: string,
    options: RequestInit,
    attempt: number = 1
  ): Promise<ApiResponse<T>> {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);

      const response = await fetch(`${this.config.baseURL}${url}`, {
        ...options,
        headers: {
          ...this.getHeaders(),
          ...options.headers,
        },
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      const responseData = await response.json();

      if (!response.ok) {
        throw new Error(responseData.message || `HTTP Error: ${response.status}`);
      }

      return {
        data: responseData.data || responseData,
        status: response.status,
        message: responseData.message,
      };
    } catch (error: any) {
      console.error(`API Request failed (attempt ${attempt}):`, error);

      // リトライロジック
      if (attempt < this.config.retryAttempts && this.shouldRetry(error)) {
        await this.delay(this.config.retryDelay * attempt);
        return this.executeRequest<T>(url, options, attempt + 1);
      }

      throw error;
    }
  }

  // リトライ判定
  private shouldRetry(error: any): boolean {
    return (
      error.name === 'AbortError' ||
      error.message.includes('Network request failed') ||
      error.message.includes('500') ||
      error.message.includes('502') ||
      error.message.includes('503')
    );
  }

  // 遅延処理
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // GET リクエスト
  async get<T>(url: string, params?: Record<string, any>): Promise<ApiResponse<T>> {
    let requestUrl = url;
    
    if (params) {
      const searchParams = new URLSearchParams();
      Object.entries(params).forEach(([key, value]) => {
        if (value !== undefined && value !== null) {
          searchParams.append(key, String(value));
        }
      });
      requestUrl += `?${searchParams.toString()}`;
    }

    return this.executeRequest<T>(requestUrl, {
      method: 'GET',
    });
  }

  // POST リクエスト
  async post<T>(url: string, data?: any): Promise<ApiResponse<T>> {
    return this.executeRequest<T>(url, {
      method: 'POST',
      body: data ? JSON.stringify(data) : undefined,
    });
  }

  // PUT リクエスト
  async put<T>(url: string, data?: any): Promise<ApiResponse<T>> {
    return this.executeRequest<T>(url, {
      method: 'PUT',
      body: data ? JSON.stringify(data) : undefined,
    });
  }

  // PATCH リクエスト
  async patch<T>(url: string, data?: any): Promise<ApiResponse<T>> {
    return this.executeRequest<T>(url, {
      method: 'PATCH',
      body: data ? JSON.stringify(data) : undefined,
    });
  }

  // DELETE リクエスト
  async delete<T>(url: string): Promise<ApiResponse<T>> {
    return this.executeRequest<T>(url, {
      method: 'DELETE',
    });
  }

  // ファイルアップロード
  async uploadFile<T>(
    url: string,
    file: {
      uri: string;
      type: string;
      name: string;
    },
    additionalData?: Record<string, string>
  ): Promise<ApiResponse<T>> {
    const formData = new FormData();
    
    formData.append('file', {
      uri: file.uri,
      type: file.type,
      name: file.name,
    } as any);

    if (additionalData) {
      Object.entries(additionalData).forEach(([key, value]) => {
        formData.append(key, value);
      });
    }

    const headers = this.getHeaders();
    delete headers['Content-Type']; // Let the browser set the content-type for FormData

    return this.executeRequest<T>(url, {
      method: 'POST',
      body: formData,
      headers,
    });
  }
}

// デフォルト設定
const defaultConfig: ApiConfig = {
  baseURL: __DEV__ ? 'http://localhost:3000/api' : 'https://api.yourapp.com',
  timeout: 10000,
  retryAttempts: 3,
  retryDelay: 1000,
};

export const apiService = new ApiService(defaultConfig);

// 具体的なAPIエンドポイント
export interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  authorName: string;
  createdAt: string;
  updatedAt: string;
  likes: number;
  comments: number;
  tags: string[];
}

export interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  bio?: string;
  followers: number;
  following: number;
}

export class PostApiService {
  // 投稿一覧取得(ページネーション付き)
  static async getPosts(page: number = 1, limit: number = 10): Promise<PaginatedResponse<Post>> {
    const response = await apiService.get<PaginatedResponse<Post>>('/posts', {
      page,
      limit,
    });
    return response.data;
  }

  // 投稿詳細取得
  static async getPost(postId: string): Promise<Post> {
    const response = await apiService.get<Post>(`/posts/${postId}`);
    return response.data;
  }

  // 投稿作成
  static async createPost(postData: {
    title: string;
    content: string;
    tags: string[];
  }): Promise<Post> {
    const response = await apiService.post<Post>('/posts', postData);
    return response.data;
  }

  // 投稿更新
  static async updatePost(postId: string, updates: Partial<Post>): Promise<Post> {
    const response = await apiService.put<Post>(`/posts/${postId}`, updates);
    return response.data;
  }

  // 投稿削除
  static async deletePost(postId: string): Promise<void> {
    await apiService.delete(`/posts/${postId}`);
  }

  // 投稿いいね
  static async likePost(postId: string): Promise<void> {
    await apiService.post(`/posts/${postId}/like`);
  }

  // 投稿いいね解除
  static async unlikePost(postId: string): Promise<void> {
    await apiService.delete(`/posts/${postId}/like`);
  }

  // 投稿検索
  static async searchPosts(query: string, tags?: string[]): Promise<Post[]> {
    const response = await apiService.get<Post[]>('/posts/search', {
      q: query,
      tags: tags?.join(','),
    });
    return response.data;
  }
}

export class UserApiService {
  // ユーザープロフィール取得
  static async getUserProfile(userId: string): Promise<User> {
    const response = await apiService.get<User>(`/users/${userId}`);
    return response.data;
  }

  // プロフィール更新
  static async updateProfile(updates: Partial<User>): Promise<User> {
    const response = await apiService.put<User>('/users/profile', updates);
    return response.data;
  }

  // アバター画像アップロード
  static async uploadAvatar(imageUri: string): Promise<{ avatarUrl: string }> {
    const response = await apiService.uploadFile<{ avatarUrl: string }>(
      '/users/avatar',
      {
        uri: imageUri,
        type: 'image/jpeg',
        name: 'avatar.jpg',
      }
    );
    return response.data;
  }

  // フォロー
  static async followUser(userId: string): Promise<void> {
    await apiService.post(`/users/${userId}/follow`);
  }

  // アンフォロー
  static async unfollowUser(userId: string): Promise<void> {
    await apiService.delete(`/users/${userId}/follow`);
  }

  // フォロワー一覧
  static async getFollowers(userId: string): Promise<User[]> {
    const response = await apiService.get<User[]>(`/users/${userId}/followers`);
    return response.data;
  }

  // フォロー中ユーザー一覧
  static async getFollowing(userId: string): Promise<User[]> {
    const response = await apiService.get<User[]>(`/users/${userId}/following`);
    return response.data;
  }
}

プッシュ通知とメッセージング

// src/services/NotificationService.ts - プッシュ通知サービス
import PushNotification from 'react-native-push-notification';
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { Platform, PermissionsAndroid, Alert } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import messaging from '@react-native-firebase/messaging';

export interface LocalNotification {
  id?: number;
  title: string;
  message: string;
  date?: Date;
  data?: any;
  actions?: string[];
  category?: string;
}

export interface RemoteNotificationData {
  title?: string;
  body?: string;
  data?: { [key: string]: string };
  badge?: number;
  sound?: string;
}

class NotificationService {
  private static instance: NotificationService;
  private isInitialized = false;
  private fcmToken: string | null = null;

  private constructor() {}

  public static getInstance(): NotificationService {
    if (!NotificationService.instance) {
      NotificationService.instance = new NotificationService();
    }
    return NotificationService.instance;
  }

  // 通知サービスの初期化
  async initialize(): Promise<void> {
    if (this.isInitialized) return;

    try {
      // 権限リクエスト
      await this.requestPermissions();

      // FCM初期化
      await this.initializeFirebaseMessaging();

      // ローカル通知設定
      this.configureLocalNotifications();

      // 通知ハンドラー設定
      this.setupNotificationHandlers();

      this.isInitialized = true;
      console.log('Notification service initialized successfully');
    } catch (error) {
      console.error('Notification service initialization failed:', error);
      throw error;
    }
  }

  // 権限リクエスト
  private async requestPermissions(): Promise<void> {
    try {
      if (Platform.OS === 'ios') {
        const authStatus = await messaging().requestPermission();
        const enabled =
          authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
          authStatus === messaging.AuthorizationStatus.PROVISIONAL;

        if (!enabled) {
          Alert.alert(
            '通知許可',
            'プッシュ通知を受信するには、設定から通知を許可してください。',
            [
              { text: 'キャンセル', style: 'cancel' },
              { text: '設定を開く', onPress: () => this.openAppSettings() },
            ]
          );
        }
      } else if (Platform.OS === 'android') {
        if (Platform.Version >= 33) {
          const granted = await PermissionsAndroid.request(
            PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
          );
          
          if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
            Alert.alert(
              '通知許可',
              'プッシュ通知を受信するには通知を許可してください。'
            );
          }
        }
      }
    } catch (error) {
      console.error('Request permissions error:', error);
    }
  }

  // Firebase Cloud Messaging 初期化
  private async initializeFirebaseMessaging(): Promise<void> {
    try {
      // FCMトークン取得
      this.fcmToken = await messaging().getToken();
      console.log('FCM Token:', this.fcmToken);
      
      // トークンをサーバーに送信
      await this.registerToken(this.fcmToken);

      // トークン更新リスナー
      messaging().onTokenRefresh(async (token) => {
        console.log('FCM Token refreshed:', token);
        this.fcmToken = token;
        await this.registerToken(token);
      });

      // フォアグラウンドメッセージハンドラー
      messaging().onMessage(async (remoteMessage) => {
        console.log('Foreground notification received:', remoteMessage);
        this.handleForegroundNotification(remoteMessage);
      });

      // バックグラウンド・終了状態からの通知タップハンドラー
      messaging().onNotificationOpenedApp((remoteMessage) => {
        console.log('Notification opened app:', remoteMessage);
        this.handleNotificationAction(remoteMessage);
      });

      // アプリ起動時の初期通知チェック
      const initialNotification = await messaging().getInitialNotification();
      if (initialNotification) {
        console.log('App opened from notification:', initialNotification);
        this.handleNotificationAction(initialNotification);
      }
    } catch (error) {
      console.error('Firebase messaging initialization error:', error);
    }
  }

  // ローカル通知設定
  private configureLocalNotifications(): void {
    PushNotification.configure({
      // 通知受信時のコールバック
      onNotification: (notification) => {
        console.log('Local notification received:', notification);
        
        if (notification.userInteraction) {
          this.handleLocalNotificationAction(notification);
        }

        // iOS用の完了通知
        if (Platform.OS === 'ios') {
          notification.finish(PushNotificationIOS.FetchResult.NoData);
        }
      },

      // 通知登録エラー
      onRegistrationError: (err) => {
        console.error('Push notification registration error:', err);
      },

      // iOS用権限リクエスト
      requestPermissions: Platform.OS === 'ios',

      // 通知タップ時の処理
      onAction: (notification) => {
        console.log('Notification action:', notification);
        this.handleNotificationAction(notification);
      },

      // 通知チャンネル設定(Android)
      channelId: 'default-channel-id',
    });

    // Android通知チャンネル作成
    if (Platform.OS === 'android') {
      PushNotification.createChannel(
        {
          channelId: 'default-channel-id',
          channelName: 'Default Channel',
          channelDescription: 'デフォルト通知チャンネル',
          playSound: true,
          soundName: 'default',
          importance: 4,
          vibrate: true,
        },
        (created) => console.log('Notification channel created:', created)
      );
    }
  }

  // 通知ハンドラー設定
  private setupNotificationHandlers(): void {
    // アプリ状態変更時の処理
    if (Platform.OS === 'ios') {
      PushNotificationIOS.addEventListener('notification', this.handleIOSNotification);
      PushNotificationIOS.addEventListener('localNotification', this.handleIOSLocalNotification);
    }
  }

  // iOS通知ハンドラー
  private handleIOSNotification = (notification: any): void => {
    console.log('iOS notification:', notification);
    this.handleNotificationAction(notification);
  };

  // iOSローカル通知ハンドラー
  private handleIOSLocalNotification = (notification: any): void => {
    console.log('iOS local notification:', notification);
    this.handleLocalNotificationAction(notification);
  };

  // フォアグラウンド通知処理
  private handleForegroundNotification(remoteMessage: any): void {
    const { notification, data } = remoteMessage;
    
    // フォアグラウンドでもローカル通知として表示
    PushNotification.localNotification({
      title: notification?.title || 'New Message',
      message: notification?.body || 'You have a new message',
      playSound: true,
      soundName: 'default',
      userInfo: data,
      channelId: 'default-channel-id',
    });
  }

  // 通知アクション処理
  private handleNotificationAction(notification: any): void {
    const { data } = notification;
    
    // 画面遷移やアクション実行
    if (data?.screen) {
      console.log(`Navigate to screen: ${data.screen}`);
      // ナビゲーション処理をここに実装
    }
    
    if (data?.action) {
      console.log(`Execute action: ${data.action}`);
      // アクション処理をここに実装
    }
  }

  // ローカル通知アクション処理
  private handleLocalNotificationAction(notification: any): void {
    console.log('Local notification action:', notification);
    // ローカル通知固有の処理
  }

  // FCMトークンをサーバーに登録
  private async registerToken(token: string): Promise<void> {
    try {
      await AsyncStorage.setItem('fcm_token', token);
      
      // APIサーバーにトークンを送信
      // await apiService.post('/users/fcm-token', { token });
      
      console.log('FCM token registered successfully');
    } catch (error) {
      console.error('Register token error:', error);
    }
  }

  // ローカル通知送信
  sendLocalNotification(notification: LocalNotification): void {
    const notificationConfig: any = {
      title: notification.title,
      message: notification.message,
      playSound: true,
      soundName: 'default',
      userInfo: notification.data,
      channelId: 'default-channel-id',
    };

    if (notification.id) {
      notificationConfig.id = notification.id;
    }

    if (notification.date) {
      notificationConfig.date = notification.date;
      PushNotification.localNotificationSchedule(notificationConfig);
    } else {
      PushNotification.localNotification(notificationConfig);
    }
  }

  // スケジュール通知
  scheduleNotification(notification: LocalNotification, date: Date): void {
    this.sendLocalNotification({
      ...notification,
      date,
    });
  }

  // 通知キャンセル
  cancelNotification(notificationId: number): void {
    PushNotification.cancelLocalNotifications({ id: notificationId.toString() });
  }

  // 全通知キャンセル
  cancelAllNotifications(): void {
    PushNotification.cancelAllLocalNotifications();
  }

  // 通知バッジクリア(iOS)
  clearBadge(): void {
    if (Platform.OS === 'ios') {
      PushNotificationIOS.setApplicationIconBadgeNumber(0);
    }
  }

  // 通知バッジ設定(iOS)
  setBadge(count: number): void {
    if (Platform.OS === 'ios') {
      PushNotificationIOS.setApplicationIconBadgeNumber(count);
    }
  }

  // トピック購読
  async subscribeToTopic(topic: string): Promise<void> {
    try {
      await messaging().subscribeToTopic(topic);
      console.log(`Subscribed to topic: ${topic}`);
    } catch (error) {
      console.error('Subscribe to topic error:', error);
    }
  }

  // トピック購読解除
  async unsubscribeFromTopic(topic: string): Promise<void> {
    try {
      await messaging().unsubscribeFromTopic(topic);
      console.log(`Unsubscribed from topic: ${topic}`);
    } catch (error) {
      console.error('Unsubscribe from topic error:', error);
    }
  }

  // アプリ設定画面を開く
  private openAppSettings(): void {
    if (Platform.OS === 'ios') {
      PushNotificationIOS.requestPermissions();
    } else {
      // Android設定画面への遷移は react-native-app-settings などを使用
    }
  }

  // FCMトークン取得
  getFCMToken(): string | null {
    return this.fcmToken;
  }

  // 通知許可状態確認
  async getPermissionStatus(): Promise<string> {
    if (Platform.OS === 'ios') {
      const authStatus = await messaging().hasPermission();
      switch (authStatus) {
        case messaging.AuthorizationStatus.AUTHORIZED:
          return 'authorized';
        case messaging.AuthorizationStatus.DENIED:
          return 'denied';
        case messaging.AuthorizationStatus.NOT_DETERMINED:
          return 'not_determined';
        case messaging.AuthorizationStatus.PROVISIONAL:
          return 'provisional';
        default:
          return 'unknown';
      }
    } else {
      return 'granted'; // Android での詳細な状態取得は複雑なため簡略化
    }
  }
}

export default NotificationService.getInstance();

分析とパフォーマンス監視

// src/services/AnalyticsService.ts - アナリティクスサービス
import analytics from '@react-native-firebase/analytics';
import crashlytics from '@react-native-firebase/crashlytics';
import perf from '@react-native-firebase/perf';
import { Platform } from 'react-native';
import DeviceInfo from 'react-native-device-info';

export interface AnalyticsEvent {
  name: string;
  parameters?: { [key: string]: any };
  timestamp?: Date;
}

export interface UserProperties {
  [key: string]: string | number | boolean;
}

export interface CustomMetrics {
  [key: string]: number;
}

export interface PerformanceTrace {
  name: string;
  startTime: number;
  metrics?: CustomMetrics;
  attributes?: { [key: string]: string };
}

class AnalyticsService {
  private static instance: AnalyticsService;
  private activeTraces: Map<string, any> = new Map();
  private sessionStartTime: number = Date.now();
  private isInitialized = false;

  private constructor() {}

  public static getInstance(): AnalyticsService {
    if (!AnalyticsService.instance) {
      AnalyticsService.instance = new AnalyticsService();
    }
    return AnalyticsService.instance;
  }

  // アナリティクスサービスの初期化
  async initialize(): Promise<void> {
    if (this.isInitialized) return;

    try {
      // Analytics有効化
      await analytics().setAnalyticsCollectionEnabled(true);
      
      // Crashlytics有効化
      await crashlytics().setCrashlyticsCollectionEnabled(true);

      // デバイス情報の設定
      await this.setDeviceProperties();

      // アプリ起動イベント
      await this.trackAppStart();

      this.isInitialized = true;
      console.log('Analytics service initialized successfully');
    } catch (error) {
      console.error('Analytics service initialization failed:', error);
      this.recordError(error as Error);
    }
  }

  // デバイス情報プロパティ設定
  private async setDeviceProperties(): Promise<void> {
    try {
      const deviceInfo = {
        platform: Platform.OS,
        version: Platform.Version.toString(),
        device_model: await DeviceInfo.getModel(),
        device_brand: await DeviceInfo.getBrand(),
        system_version: await DeviceInfo.getSystemVersion(),
        app_version: await DeviceInfo.getVersion(),
        build_number: await DeviceInfo.getBuildNumber(),
        is_tablet: await DeviceInfo.isTablet(),
        total_memory: await DeviceInfo.getTotalMemory(),
      };

      // Firebase Analyticsにユーザープロパティとして設定
      for (const [key, value] of Object.entries(deviceInfo)) {
        await analytics().setUserProperty(key, String(value));
      }

      // Crashlyticsにも設定
      for (const [key, value] of Object.entries(deviceInfo)) {
        crashlytics().setAttribute(key, String(value));
      }

      console.log('Device properties set:', deviceInfo);
    } catch (error) {
      console.error('Set device properties error:', error);
    }
  }

  // アプリ起動トラッキング
  private async trackAppStart(): Promise<void> {
    try {
      const startupTime = Date.now() - this.sessionStartTime;
      
      await this.trackEvent('app_start', {
        startup_time: startupTime,
        timestamp: new Date().toISOString(),
        session_id: this.generateSessionId(),
      });

      // パフォーマンストレース
      const trace = perf().newTrace('app_startup');
      trace.putMetric('startup_time_ms', startupTime);
      await trace.start();
      await trace.stop();
    } catch (error) {
      console.error('Track app start error:', error);
    }
  }

  // カスタムイベントトラッキング
  async trackEvent(eventName: string, parameters?: { [key: string]: any }): Promise<void> {
    try {
      const eventParams = {
        ...parameters,
        timestamp: new Date().toISOString(),
        session_id: this.generateSessionId(),
      };

      await analytics().logEvent(eventName, eventParams);
      console.log(`Event tracked: ${eventName}`, eventParams);
    } catch (error) {
      console.error('Track event error:', error);
      this.recordError(error as Error);
    }
  }

  // 画面表示トラッキング
  async trackScreenView(screenName: string, screenClass?: string, additionalData?: any): Promise<void> {
    try {
      await analytics().logScreenView({
        screen_name: screenName,
        screen_class: screenClass || screenName,
      });

      // 追加のカスタムイベント
      await this.trackEvent('screen_view', {
        screen_name: screenName,
        screen_class: screenClass || screenName,
        ...additionalData,
      });

      console.log(`Screen view tracked: ${screenName}`);
    } catch (error) {
      console.error('Track screen view error:', error);
    }
  }

  // ユーザープロパティ設定
  async setUserProperties(properties: UserProperties): Promise<void> {
    try {
      for (const [key, value] of Object.entries(properties)) {
        await analytics().setUserProperty(key, String(value));
        crashlytics().setAttribute(key, String(value));
      }
      console.log('User properties set:', properties);
    } catch (error) {
      console.error('Set user properties error:', error);
    }
  }

  // ユーザーID設定
  async setUserId(userId: string): Promise<void> {
    try {
      await analytics().setUserId(userId);
      await crashlytics().setUserId(userId);
      console.log(`User ID set: ${userId}`);
    } catch (error) {
      console.error('Set user ID error:', error);
    }
  }

  // ECイベントトラッキング
  async trackPurchase(
    transactionId: string,
    value: number,
    currency: string,
    items: any[]
  ): Promise<void> {
    try {
      await analytics().logEvent('purchase', {
        transaction_id: transactionId,
        value,
        currency,
        items,
      });

      // 個別アイテムイベント
      for (const item of items) {
        await analytics().logEvent('select_item', {
          item_id: item.item_id,
          item_name: item.item_name,
          item_category: item.item_category,
          value: item.price,
          currency,
        });
      }

      console.log('Purchase tracked:', { transactionId, value, currency });
    } catch (error) {
      console.error('Track purchase error:', error);
    }
  }

  // ユーザーエンゲージメントトラッキング
  async trackUserEngagement(action: string, duration?: number, additionalData?: any): Promise<void> {
    try {
      const parameters: { [key: string]: any } = { 
        action,
        ...additionalData 
      };
      
      if (duration !== undefined) {
        parameters.engagement_time_msec = duration;
      }

      await analytics().logEvent('user_engagement', parameters);
      console.log('User engagement tracked:', parameters);
    } catch (error) {
      console.error('Track user engagement error:', error);
    }
  }

  // パフォーマンストレース開始
  async startPerformanceTrace(traceName: string): Promise<boolean> {
    try {
      const trace = perf().newTrace(traceName);
      await trace.start();
      
      this.activeTraces.set(traceName, {
        trace,
        startTime: Date.now(),
      });

      console.log(`Performance trace started: ${traceName}`);
      return true;
    } catch (error) {
      console.error('Start performance trace error:', error);
      return false;
    }
  }

  // パフォーマンストレース停止
  async stopPerformanceTrace(
    traceName: string, 
    metrics?: CustomMetrics,
    attributes?: { [key: string]: string }
  ): Promise<void> {
    try {
      const traceData = this.activeTraces.get(traceName);
      if (!traceData) {
        console.warn(`Performance trace not found: ${traceName}`);
        return;
      }

      const { trace } = traceData;
      const duration = Date.now() - traceData.startTime;

      // メトリクス追加
      if (metrics) {
        for (const [key, value] of Object.entries(metrics)) {
          trace.putMetric(key, value);
        }
      }

      // 属性追加
      if (attributes) {
        for (const [key, value] of Object.entries(attributes)) {
          trace.putAttribute(key, value);
        }
      }

      // 実行時間メトリック
      trace.putMetric('duration_ms', duration);

      await trace.stop();
      this.activeTraces.delete(traceName);

      console.log(`Performance trace stopped: ${traceName}, duration: ${duration}ms`);
    } catch (error) {
      console.error('Stop performance trace error:', error);
    }
  }

  // 非同期処理のパフォーマンス測定
  async measureAsync<T>(traceName: string, fn: () => Promise<T>): Promise<T> {
    await this.startPerformanceTrace(traceName);
    try {
      const result = await fn();
      await this.stopPerformanceTrace(traceName, { success: 1 });
      return result;
    } catch (error) {
      await this.stopPerformanceTrace(traceName, { success: 0, error: 1 });
      throw error;
    }
  }

  // エラー記録(Crashlytics)
  recordError(error: Error, additionalInfo?: { [key: string]: any }): void {
    try {
      if (additionalInfo) {
        for (const [key, value] of Object.entries(additionalInfo)) {
          crashlytics().setAttribute(key, String(value));
        }
      }

      crashlytics().recordError(error);
      console.log('Error recorded to Crashlytics:', error.message);
    } catch (err) {
      console.error('Record error to Crashlytics failed:', err);
    }
  }

  // カスタムログ
  log(message: string, level: 'debug' | 'info' | 'warning' | 'error' = 'info'): void {
    try {
      crashlytics().log(`[${level.toUpperCase()}] ${message}`);
      console.log(`Crashlytics log: ${message}`);
    } catch (error) {
      console.error('Crashlytics log error:', error);
    }
  }

  // カスタムキー設定
  setCustomKeys(keys: { [key: string]: any }): void {
    try {
      for (const [key, value] of Object.entries(keys)) {
        crashlytics().setAttribute(key, String(value));
      }
      console.log('Custom keys set for crash reports:', keys);
    } catch (error) {
      console.error('Set custom keys error:', error);
    }
  }

  // ネットワークリクエストトラッキング
  async trackNetworkRequest(
    url: string,
    method: string,
    statusCode: number,
    duration: number,
    responseSize?: number
  ): Promise<void> {
    try {
      await this.trackEvent('network_request', {
        url,
        method,
        status_code: statusCode,
        duration_ms: duration,
        response_size: responseSize || 0,
        success: statusCode >= 200 && statusCode < 300 ? 1 : 0,
      });
    } catch (error) {
      console.error('Track network request error:', error);
    }
  }

  // セッション時間計算
  getSessionDuration(): number {
    return Date.now() - this.sessionStartTime;
  }

  // セッションID生成
  private generateSessionId(): string {
    return `session_${this.sessionStartTime}_${Math.random().toString(36).substr(2, 9)}`;
  }

  // アプリ終了時のクリーンアップ
  async cleanup(): Promise<void> {
    try {
      // アクティブなトレースを停止
      for (const [traceName] of this.activeTraces) {
        await this.stopPerformanceTrace(traceName);
      }

      // セッション終了イベント
      await this.trackEvent('session_end', {
        session_duration: this.getSessionDuration(),
      });

      console.log('Analytics service cleanup completed');
    } catch (error) {
      console.error('Analytics cleanup error:', error);
    }
  }

  // テスト用クラッシュ(開発環境のみ)
  testCrash(): void {
    if (__DEV__) {
      crashlytics().crash();
    }
  }
}

export default AnalyticsService.getInstance();

// 便利なヘルパー関数
export const performanceMonitor = {
  // API呼び出しの測定
  async measureApiCall<T>(apiName: string, apiCall: () => Promise<T>): Promise<T> {
    const analyticsService = AnalyticsService.getInstance();
    return analyticsService.measureAsync(`api_${apiName}`, apiCall);
  },

  // 画面レンダリング時間の測定
  async measureScreenRender(screenName: string, renderFunction: () => Promise<void>): Promise<void> {
    const analyticsService = AnalyticsService.getInstance();
    return analyticsService.measureAsync(`screen_render_${screenName}`, renderFunction);
  },

  // データベース操作の測定
  async measureDatabaseOperation<T>(operation: string, dbCall: () => Promise<T>): Promise<T> {
    const analyticsService = AnalyticsService.getInstance();
    return analyticsService.measureAsync(`db_${operation}`, dbCall);
  },
};

デプロイメントと設定

# metro.config.js - Metro bundler 設定
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

const defaultConfig = getDefaultConfig(__dirname);

const config = {
  resolver: {
    assetExts: [...defaultConfig.resolver.assetExts, 'bin'],
    sourceExts: [...defaultConfig.resolver.sourceExts, 'svg'],
  },
  transformer: {
    babelTransformerPath: require.resolve('react-native-svg-transformer'),
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  watchFolders: [
    // 追加の監視フォルダがあれば設定
  ],
};

module.exports = mergeConfig(defaultConfig, config);
// package.json - Build & Deploy Scripts
{
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "build:android": "cd android && ./gradlew assembleRelease",
    "build:android:bundle": "cd android && ./gradlew bundleRelease",
    "build:ios": "cd ios && xcodebuild -workspace MyAwesomeApp.xcworkspace -scheme MyAwesomeApp -configuration Release -destination generic/platform=iOS -archivePath build/MyAwesomeApp.xcarchive archive",
    "build:ios:simulator": "cd ios && xcodebuild -workspace MyAwesomeApp.xcworkspace -scheme MyAwesomeApp -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 14' build",
    "clean": "npx react-native-clean-project",
    "clean:android": "cd android && ./gradlew clean",
    "clean:ios": "cd ios && xcodebuild clean",
    "postinstall": "cd ios && pod install",
    "analyze:bundle": "npx react-native-bundle-visualizer",
    "type-check": "tsc --noEmit",
    "test:coverage": "jest --coverage",
    "test:e2e": "detox test",
    "test:e2e:build": "detox build",
    "deploy:android": "cd android && ./gradlew assembleRelease && echo 'APK built successfully'",
    "deploy:ios": "cd ios && xcodebuild -exportArchive -archivePath build/MyAwesomeApp.xcarchive -exportPath build -exportOptionsPlist ExportOptions.plist"
  }
}
# release.sh - リリース自動化スクリプト
#!/bin/bash

# React Native アプリケーション リリーススクリプト
set -e

echo "🚀 React Native アプリケーション リリース開始"

# バージョン情報の取得
VERSION=$(node -p "require('./package.json').version")
echo "📱 アプリバージョン: $VERSION"

# 環境チェック
echo "🔍 環境チェック中..."

# Node.js バージョンチェック
NODE_VERSION=$(node -v)
echo "Node.js バージョン: $NODE_VERSION"

# React Native CLI チェック
if ! command -v react-native &> /dev/null; then
    echo "❌ React Native CLI がインストールされていません"
    exit 1
fi

# 依存関係のインストール
echo "📦 依存関係をインストール中..."
npm ci

# TypeScript 型チェック
echo "🔍 TypeScript 型チェック中..."
npm run type-check

# ESLint チェック
echo "🔍 ESLint チェック中..."
npm run lint

# テスト実行
echo "🧪 テスト実行中..."
npm run test

# Metro bundler キャッシュクリア
echo "🧹 Metro キャッシュクリア中..."
npx react-native start --reset-cache &
sleep 5
kill %1

# Android ビルド
if [[ "$1" == "android" || "$1" == "both" ]]; then
    echo "🤖 Android ビルド開始..."
    
    # Android クリーン
    cd android
    ./gradlew clean
    
    # リリースビルド
    ./gradlew assembleRelease
    
    # APK サイズチェック
    APK_SIZE=$(du -h app/build/outputs/apk/release/app-release.apk | cut -f1)
    echo "📱 APK サイズ: $APK_SIZE"
    
    # APK ファイル移動
    cp app/build/outputs/apk/release/app-release.apk ../builds/android/
    
    cd ..
    echo "✅ Android ビルド完了"
fi

# iOS ビルド(macOSのみ)
if [[ "$1" == "ios" || "$1" == "both" ]] && [[ "$OSTYPE" == "darwin"* ]]; then
    echo "🍎 iOS ビルド開始..."
    
    # CocoaPods 更新
    cd ios
    pod install --repo-update
    
    # アーカイブ作成
    xcodebuild -workspace MyAwesomeApp.xcworkspace \
               -scheme MyAwesomeApp \
               -configuration Release \
               -destination generic/platform=iOS \
               -archivePath build/MyAwesomeApp.xcarchive \
               archive
    
    # IPA エクスポート
    xcodebuild -exportArchive \
               -archivePath build/MyAwesomeApp.xcarchive \
               -exportPath build \
               -exportOptionsPlist ExportOptions.plist
    
    # IPA ファイル移動
    cp build/MyAwesomeApp.ipa ../builds/ios/
    
    cd ..
    echo "✅ iOS ビルド完了"
fi

# バンドル分析
echo "📊 バンドル分析中..."
npx react-native-bundle-visualizer --platform android --dev false --output analysis/android-bundle-analysis.html
npx react-native-bundle-visualizer --platform ios --dev false --output analysis/ios-bundle-analysis.html

# リリースノート生成
echo "📝 リリースノート生成中..."
cat > "builds/release-notes-v$VERSION.md" << EOF
# Release Notes v$VERSION

## 📱 Application Information
- Version: $VERSION
- Build Date: $(date)
- React Native Version: $(npx react-native --version)
- Node.js Version: $NODE_VERSION

## 📦 Build Artifacts
- Android APK: builds/android/app-release.apk
- iOS IPA: builds/ios/MyAwesomeApp.ipa

## 🧪 Quality Assurance
- ✅ TypeScript Type Check Passed
- ✅ ESLint Check Passed
- ✅ Unit Tests Passed
- ✅ Bundle Analysis Completed

## 📊 Bundle Analysis
- Android Bundle Analysis: analysis/android-bundle-analysis.html
- iOS Bundle Analysis: analysis/ios-bundle-analysis.html

---
Generated automatically by release script.
EOF

echo "🎉 リリース完了!"
echo "📄 リリースノート: builds/release-notes-v$VERSION.md"
echo "📱 ビルドファイル: builds/ ディレクトリを確認してください"

# 成功時の通知(オプション)
if command -v osascript &> /dev/null; then
    osascript -e "display notification \"React Native アプリのリリースが完了しました!\" with title \"🎉 ビルド成功\""
fi