Skip to content

예제

React 및 React Native 프로덕션 구현 예제입니다.

Paywall 구현

사용자가 채팅을 완료하고 메인 뷰로 이동한 후 페이월을 표시합니다. 무료 등급 사용자에게 3일 윈도우당 최대 3회로 제한됩니다.

설정

typescript
const paywallExitingThreadFunnel = {
  funnelId: 'paywall--exiting-thread',
  scopes: ['session'],
  steps: [(e) => e.name === 'make_chat', (e) => e.name === 'click_icon_to_main_view'],
};

const paywallRule = {
  nudgeId: 'paywall',
  when: (t) => t.type === 'funnel_completed' && t.funnelId === 'paywall--exiting-thread',
  cooldownDuration: '3d',
  maxPerWindow: 3,
  windowDuration: '3d',
  personalTiers: ['free'],
  buildAction: () => ({
    type: 'paywall',
    data: { entryType: 'exiting_thread' },
  }),
};

const coordinator = startCoordinator({
  funnels: [paywallExitingThreadFunnel],
  nudgeRules: [paywallRule],
  runner: {
    run: async (action) => {
      if (action.type === 'paywall') {
        showPaywall(action.data);
      }
    },
  },
  cooldownStorage: new NudgeCooldownStorage({ storage: localStorage }),
  exposureStorage: new NudgeExposureLogStorage({ storage: localStorage }),
});

React 컴포넌트

typescript
import { useState, useEffect } from 'react';

export const PaywallModal = ({ data, onDismiss }) => {
  const [isOpen, setIsOpen] = useState(false);

  useEffect(() => {
    setIsOpen(true);
  }, []);

  return (
    <Modal open={isOpen} onClose={onDismiss}>
      <PaywallContent entryType={data.entryType} />
    </Modal>
  );
};

React Native 컴포넌트

typescript
import { useEffect } from 'react';
import { Modal } from 'react-native';

export const PaywallModal = ({ data, onDismiss }) => {
  useEffect(() => {
    // 모달 뷰 추적
    tracker.track('paywall_viewed', { entryType: data.entryType });
  }, []);

  return (
    <Modal visible animationType="slide" onRequestClose={onDismiss}>
      <PaywallContent entryType={data.entryType} />
    </Modal>
  );
};

App Review 구현

마일스톤 완료 후 pro/max 등급 사용자에게 리뷰 프롬프트를 표시합니다.

Configuration

typescript
const appReviewRule = {
  nudgeId: 'app-review',
  when: (t) => t.type === 'funnel_completed' && t.funnelId === 'app-usage-milestone',
  cooldownDuration: '7d',
  maxPerWindow: 1,
  windowDuration: '30d',
  personalTiers: ['pro', 'max'],
  buildAction: () => ({
    type: 'show_modal',
    nudgeId: 'app_review',
  }),
};

const coordinator = startCoordinator({
  nudgeRules: [appReviewRule],
  runner: {
    run: async (action) => {
      if (action.type === 'show_modal') {
        showAppReviewDialog(action.nudgeId);
      }
    },
  },
});

React 컴포넌트

typescript
export const AppReviewDialog = ({ onDismiss }) => {
  const handleReview = async () => {
    window.open('https://example.com/review', '_blank');
    onDismiss();
  };

  return (
    <Dialog open onClose={onDismiss}>
      <DialogTitle>앱이 도움이 되었나요?</DialogTitle>
      <DialogActions>
        <Button onClick={onDismiss}>나중에</Button>
        <Button onClick={handleReview}>리뷰 남기기</Button>
      </DialogActions>
    </Dialog>
  );
};

React Native 컴포넌트

typescript
import * as StoreReview from 'expo-store-review';

export const AppReviewDialog = ({ onDismiss }) => {
  const handleReview = async () => {
    await StoreReview.requestReview();
    onDismiss();
  };

  return (
    <Dialog visible onDismiss={onDismiss}>
      <Dialog.Title>앱이 도움이 되었나요?</Dialog.Title>
      <Dialog.Actions>
        <Button onPress={onDismiss}>나중에</Button>
        <Button onPress={handleReview}>리뷰 남기기</Button>
      </Dialog.Actions>
    </Dialog>
  );
};

알림 권한 요청

조건이 충족되면 알림 권한을 요청합니다.

typescript
const notificationPermissionFunnel = {
  funnelId: 'credit-reward--notification-off',
  scopes: ['session'],
  steps: [(e) => e.name === 'is_app_push_approved' && !e.properties.is_approved],
};

const notificationRule = {
  nudgeId: 'request-notification-permission',
  when: (t) => t.type === 'funnel_completed',
  cooldownDuration: '7d',
  buildAction: () => ({
    type: 'notification',
    funnelId: 'credit-reward--notification-off',
  }),
};

다음 단계

복잡한 액션 로직은 커스텀 액션에서 타입 안전한 액션 정의를 참고하세요.

Released under the MIT License.