Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 116 additions & 44 deletions src/app/[locale]/user/watch-ads/page.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,153 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/shared/Forms/Buttons/Buttons';
import MembershipIcon from '@/components/shared/membership/MembershipIcon';
import { MembershipClassType } from '@/constants/types';
import { createWatchAdsSession, completeWatchAdsSegment } from'../../../../services/watchAdsApi'

const tiers: [string, string, MembershipClassType | null][] = [
['Single mappi', 'Watch ads for 10 minutes', null],
['White membership', 'Watch ads for 50 minutes', MembershipClassType.WHITE],
['Green membership', 'Watch ads for 1 hour and 40 minutes', MembershipClassType.GREEN],
['Gold membership', 'Watch ads for 4 hours and 10 minutes', MembershipClassType.GOLD],
['Double Gold membership', 'Watch ads for 8 hours and 20 minutes', MembershipClassType.DOUBLE_GOLD],
['Triple Gold membership', 'Watch ads for 16 hours and 40 minutes', MembershipClassType.TRIPLE_GOLD],
];

declare const Pi: any;

export default function WatchAdsPage() {
const ready = useRef(false);
const [log, setLog] = useState<string[]>([]);
const push = (m: string) => setLog(prev => [m, ...prev].slice(0, 60));
const [sessionId, setSessionId] = useState<string | null>(null);
const [earnedSecs, setEarnedSecs] = useState<number>(0);
const [status, setStatus] = useState<string>("");

useEffect(() => {
(async () => {
if (typeof window === 'undefined' || !window.Pi) {
push('Pi SDK not found. Open this page in Pi Browser.');
return;
}
if (typeof window === 'undefined' || !window.Pi) return;

try {
await Pi.init({ version: '2.0' });
push('Pi.init OK');

const feats = await Pi.nativeFeaturesList();
push(`nativeFeaturesList: ${JSON.stringify(feats)}`);
if (!feats.includes('ad_network')) {
push('ad_network not supported. Update Pi Browser.');
return;
}
if (!feats.includes('ad_network')) return;

try {
await Pi.authenticate(); // no payment callbacks
push('Pi.authenticate OK');
await Pi.authenticate();
} catch {
push('User skipped auth; rewarded may not show.');
console.warn('User skipped authentication.');
}

// Create backend watch-ads session
const data = await createWatchAdsSession();
if (data?._id) {
setSessionId(data._id);
setEarnedSecs(data.earnedSecs ?? 0);
setStatus(data.status ?? "unknown");
}

ready.current = true;
} catch (e: any) {
push(`Init/Auth error: ${e?.message || String(e)}`);
} catch (err: any) {
console.error('Initialization error:', err);
}
})();
}, []);

const showRewarded = async () => {
if (!ready.current) return push('SDK not ready yet.');
if (!ready.current) return;
try {
const isReady = await Pi.Ads.isAdReady('rewarded');
push(`isAdReady(rewarded): ${JSON.stringify(isReady)}`);

if (!isReady.ready) {
const req = await Pi.Ads.requestAd('rewarded');
push(`requestAd(rewarded): ${JSON.stringify(req)}`);
if (req.result !== 'AD_LOADED') {
push('No rewarded ad available right now.');
return;
}
if (req.result !== 'AD_LOADED') return;
}

const show = await Pi.Ads.showAd('rewarded'); // { result, adId? }
push(`showAd(rewarded): ${JSON.stringify(show)}`);
if (show.result === 'AD_REWARDED') push('Rewarded completed.');
} catch (e: any) {
push(`showRewarded error: ${e?.message || String(e)}`);
const show = await Pi.Ads.showAd('rewarded');
if (show.result === 'AD_REWARDED' && sessionId) {
// Complete segment
const updated = await completeWatchAdsSegment(sessionId, show.adId);
if (updated) {
setEarnedSecs(Number(updated.earnedSecs ?? 0));
setStatus(updated.status ?? "unknown");
}
}
} catch (err: any) {
console.error('Error showing ad:', err);
}
};

return (
<div className="p-6 max-w-xl mx-auto">
<h1 className="text-xl font-bold mb-4">Watch Ads (Test)</h1>
<button className="px-4 py-2 bg-blue-600 text-white rounded mb-4" onClick={showRewarded}>
Show Rewarded
</button>

<div className="border rounded p-3 text-sm max-h-72 overflow-auto bg-gray-50">
{log.length === 0 ? <div>No logs yet.</div> : (
<ul className="space-y-2">
{log.map((l, i) => <li key={i} className="font-mono break-all">{l}</li>)}
</ul>
)}
<main className="mx-auto w-full max-w-[335px] border border-tertiary bg-background p-4 mt-8 text-center">
<div aria-hidden className="-mx-4 -mt-4 mb-4 h-[6px] rounded-t-[6px] bg-primary" />

<h1 className="text-[17px] font-bold text-[#1e1e1e] mb-4">
Watch Ads to Buy Membership
</h1>

<section className="mb-6">
<p className="text-[15px] font-semibold text-[#333333] mb-3">
How many ad minutes do I need:
</p>

<ul className="space-y-3">
{tiers.map(([title, desc, tier], i) => (
<li key={i}>
<div className="flex items-center justify-center gap-2 text-[#1e1e1e]">
<span className="whitespace-nowrap">{title}</span>
{tier && (
<MembershipIcon
category={tier}
className="inline-block align-middle"
styleComponent={{
width:
tier === MembershipClassType.TRIPLE_GOLD
? '28px'
: tier === MembershipClassType.DOUBLE_GOLD
? '24px'
: '20px',
height:
tier === MembershipClassType.TRIPLE_GOLD
? '28px'
: tier === MembershipClassType.DOUBLE_GOLD
? '24px'
: '20px',
objectFit: 'contain',
verticalAlign: 'middle',
}}
/>
)}
</div>
<p className="text-[12px] text-[#6b6b6b]">{desc}</p>
</li>
))}
</ul>
</section>

<p className="text-[14px] font-2xl mb-4">
Ads are presented in 10 minute blocks
</p>

<div className="mb-6 flex justify-center">
<Button
label="Watch Ad Block"
styles={{
color: '#ffc153',
backgroundColor: 'var(--default-primary-color)',
height: '40px',
padding: '10px 15px',
}}
onClick={showRewarded}
/>
</div>
</div>

<p className="text-[14px] text-[#333333] text-gray-800">
Ad minutes watched so far
<br />
<span className="text-[13px] text-[#6b6b6b]">
{Math.floor(Number(earnedSecs) / 3600)} hours,{' '}
{Math.floor((Number(earnedSecs) / 60) % 60)} min and{' '}
{Math.floor(Number(earnedSecs) % 60)} seconds
</span>
</p>
</main>
);
}
39 changes: 34 additions & 5 deletions src/components/shared/membership/MembershipIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,45 @@ function MembershipIcon({ category, className, styleComponent }: {
return null
}
}

const icon = HandleMembership(category);

if (!icon) return null; // Don't render anything for casual members

return (
<div className={`w-7 h-5 relative float-right ${className || ''}`} style={styleComponent}>
<Image src={icon} alt={category} fill style={{ objectFit: 'contain' }}/>
return (
<div
className={`relative ${className || ''}`}
style={{
display: 'inline-block',
width:
category === MembershipClassType.TRIPLE_GOLD
? '30px'
: category === MembershipClassType.DOUBLE_GOLD
? '25px'
: '20px',
height:
category === MembershipClassType.TRIPLE_GOLD
? '30px'
: category === MembershipClassType.DOUBLE_GOLD
? '25px'
: '20px',
verticalAlign: 'middle',
...styleComponent,
}}
>
<Image
src={icon}
alt={category}
fill
style={{
objectFit: 'contain',
width: '100%',
height: '100%',
}}
sizes="auto"
/>
</div>
)
);
}

export default MembershipIcon
38 changes: 38 additions & 0 deletions src/services/watchAdsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import axiosClient from "@/config/client";
import logger from '../../logger.config.mjs';

export const createWatchAdsSession = async () => {
try {
logger.info("Creating new Watch Ads session");
const response = await axiosClient.post("/v1/watch-ads/session");

if (response.status === 200) {
logger.info("Session created successfully", { data: response.data });
return response.data;
} else {
logger.error(`Session creation failed with status ${response.status}`);
return null;
}
} catch (error) {
logger.error("Error creating Watch Ads session:", error);
throw new Error("Failed to initialize Watch Ads session");
}
};

export const completeWatchAdsSegment = async (sessionId: string, adId: string) => {
try {
logger.info(`Completing ad segment for session ${sessionId}`);
const response = await axiosClient.post(`/v1/watch-ads/session/${sessionId}/segment-complete`, { adId });

if (response.status === 200) {
logger.info("Segment completion successful", { data: response.data });
return response.data;
} else {
logger.error(`Segment completion failed with status ${response.status}`);
return null;
}
} catch (error) {
logger.error("Error completing ad segment:", error);
throw new Error("Failed to complete ad segment");
}
};