예제
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',
}),
};다음 단계
복잡한 액션 로직은 커스텀 액션에서 타입 안전한 액션 정의를 참고하세요.