@playbasis-ai/qwikcard-sdk 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/SDK_HANDOVER_GUIDE.md +129 -0
- package/dist/PlaybasisProvider.d.ts +19 -0
- package/dist/PlaybasisProvider.d.ts.map +1 -0
- package/dist/PlaybasisProvider.js +24 -0
- package/dist/QwikCardApp.d.ts +9 -0
- package/dist/QwikCardApp.d.ts.map +1 -0
- package/dist/QwikCardApp.js +210 -0
- package/dist/api/client.d.ts +66 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +196 -0
- package/dist/components/Badge.d.ts +8 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +34 -0
- package/dist/components/BadgeIcon.d.ts +10 -0
- package/dist/components/BadgeIcon.d.ts.map +1 -0
- package/dist/components/BadgeIcon.js +51 -0
- package/dist/components/Button.d.ts +10 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +40 -0
- package/dist/components/GradientCard.d.ts +9 -0
- package/dist/components/GradientCard.d.ts.map +1 -0
- package/dist/components/GradientCard.js +28 -0
- package/dist/components/PointsBalance.d.ts +8 -0
- package/dist/components/PointsBalance.d.ts.map +1 -0
- package/dist/components/PointsBalance.js +85 -0
- package/dist/components/ProgressBar.d.ts +8 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +41 -0
- package/dist/components/QuestProgress.d.ts +10 -0
- package/dist/components/QuestProgress.d.ts.map +1 -0
- package/dist/components/QuestProgress.js +94 -0
- package/dist/components/RadialGauge.d.ts +8 -0
- package/dist/components/RadialGauge.d.ts.map +1 -0
- package/dist/components/RadialGauge.js +53 -0
- package/dist/components/RewardCard.d.ts +10 -0
- package/dist/components/RewardCard.d.ts.map +1 -0
- package/dist/components/RewardCard.js +64 -0
- package/dist/components/RulesModal.d.ts +7 -0
- package/dist/components/RulesModal.d.ts.map +1 -0
- package/dist/components/RulesModal.js +106 -0
- package/dist/hooks/useBadges.d.ts +12 -0
- package/dist/hooks/useBadges.d.ts.map +1 -0
- package/dist/hooks/useBadges.js +42 -0
- package/dist/hooks/usePoints.d.ts +13 -0
- package/dist/hooks/usePoints.d.ts.map +1 -0
- package/dist/hooks/usePoints.js +70 -0
- package/dist/hooks/useQuests.d.ts +12 -0
- package/dist/hooks/useQuests.d.ts.map +1 -0
- package/dist/hooks/useQuests.js +41 -0
- package/dist/hooks/useQwikApp.d.ts +16 -0
- package/dist/hooks/useQwikApp.d.ts.map +1 -0
- package/dist/hooks/useQwikApp.js +42 -0
- package/dist/hooks/useRewards.d.ts +12 -0
- package/dist/hooks/useRewards.d.ts.map +1 -0
- package/dist/hooks/useRewards.js +56 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/theme/context.d.ts +8 -0
- package/dist/theme/context.d.ts.map +1 -0
- package/dist/theme/context.js +8 -0
- package/dist/theme/tokens.d.ts +35 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +33 -0
- package/dist/types/index.d.ts +119 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/web/widgetAssets.d.ts +4 -0
- package/dist/web/widgetAssets.d.ts.map +1 -0
- package/dist/web/widgetAssets.js +5 -0
- package/dist/web/widgetHtml.d.ts +2 -0
- package/dist/web/widgetHtml.d.ts.map +1 -0
- package/dist/web/widgetHtml.js +299 -0
- package/dist/web/widgetTypes.d.ts +128 -0
- package/dist/web/widgetTypes.d.ts.map +1 -0
- package/dist/web/widgetTypes.js +1 -0
- package/package.json +86 -0
- package/src/PlaybasisProvider.tsx +72 -0
- package/src/QwikCardApp.tsx +302 -0
- package/src/api/client.ts +307 -0
- package/src/components/Badge.tsx +51 -0
- package/src/components/BadgeIcon.tsx +97 -0
- package/src/components/Button.tsx +70 -0
- package/src/components/GradientCard.tsx +49 -0
- package/src/components/PointsBalance.tsx +122 -0
- package/src/components/ProgressBar.tsx +65 -0
- package/src/components/QuestProgress.tsx +153 -0
- package/src/components/RadialGauge.tsx +101 -0
- package/src/components/RewardCard.tsx +123 -0
- package/src/components/RulesModal.tsx +171 -0
- package/src/hooks/useBadges.ts +59 -0
- package/src/hooks/usePoints.ts +91 -0
- package/src/hooks/useQuests.ts +60 -0
- package/src/hooks/useQwikApp.ts +49 -0
- package/src/hooks/useRewards.ts +74 -0
- package/src/index.ts +34 -0
- package/src/theme/context.tsx +17 -0
- package/src/theme/tokens.ts +68 -0
- package/src/types/index.ts +176 -0
- package/src/web/widgetAssets.d.ts +3 -0
- package/src/web/widgetAssets.ts +6 -0
- package/src/web/widgetHtml.ts +302 -0
- package/src/web/widgetTypes.ts +146 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, Image, StyleSheet } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { GradientCard } from './GradientCard';
|
|
5
|
+
import { Button } from './Button';
|
|
6
|
+
import { useTheme } from '../theme/context';
|
|
7
|
+
import type { Reward } from '../types';
|
|
8
|
+
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Props
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
interface RewardCardProps {
|
|
14
|
+
reward: Reward;
|
|
15
|
+
onRedeem?: () => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
style?: object;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Component
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export function RewardCard({ reward, onRedeem, disabled = false, style }: RewardCardProps) {
|
|
25
|
+
const theme = useTheme();
|
|
26
|
+
const isAvailable = reward.available && !disabled;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<GradientCard style={[styles.container, style]}>
|
|
30
|
+
{reward.imageUrl ? (
|
|
31
|
+
<Image source={{ uri: reward.imageUrl }} style={styles.image} resizeMode="cover" />
|
|
32
|
+
) : (
|
|
33
|
+
<View style={[styles.imagePlaceholder, { backgroundColor: 'rgba(255, 69, 0, 0.1)' }]}>
|
|
34
|
+
<Text style={styles.placeholderText}>🎁</Text>
|
|
35
|
+
</View>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
<View style={styles.content}>
|
|
39
|
+
<Text style={[styles.name, { color: theme.colors.text.primary }]}>{reward.name}</Text>
|
|
40
|
+
<Text
|
|
41
|
+
style={[styles.description, { color: theme.colors.text.secondary }]}
|
|
42
|
+
numberOfLines={2}
|
|
43
|
+
>
|
|
44
|
+
{reward.description}
|
|
45
|
+
</Text>
|
|
46
|
+
|
|
47
|
+
<View style={styles.footer}>
|
|
48
|
+
<View style={styles.cost}>
|
|
49
|
+
<Text style={[styles.costAmount, { color: theme.colors.text.primary }]}>
|
|
50
|
+
{reward.cost}
|
|
51
|
+
</Text>
|
|
52
|
+
<Text style={[styles.costCurrency, { color: theme.colors.text.secondary }]}>
|
|
53
|
+
{reward.currency}
|
|
54
|
+
</Text>
|
|
55
|
+
</View>
|
|
56
|
+
|
|
57
|
+
{onRedeem && (
|
|
58
|
+
<Button
|
|
59
|
+
title={isAvailable ? 'Redeem' : 'Unavailable'}
|
|
60
|
+
onPress={onRedeem}
|
|
61
|
+
variant={isAvailable ? 'primary' : 'secondary'}
|
|
62
|
+
style={{ opacity: isAvailable ? 1 : 0.6 }}
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
</View>
|
|
66
|
+
</View>
|
|
67
|
+
</GradientCard>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
// Styles
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const styles = StyleSheet.create({
|
|
76
|
+
container: {
|
|
77
|
+
marginVertical: 8,
|
|
78
|
+
},
|
|
79
|
+
image: {
|
|
80
|
+
width: '100%',
|
|
81
|
+
height: 140,
|
|
82
|
+
},
|
|
83
|
+
imagePlaceholder: {
|
|
84
|
+
width: '100%',
|
|
85
|
+
height: 140,
|
|
86
|
+
justifyContent: 'center',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
},
|
|
89
|
+
placeholderText: {
|
|
90
|
+
fontSize: 48,
|
|
91
|
+
},
|
|
92
|
+
content: {
|
|
93
|
+
padding: 16,
|
|
94
|
+
},
|
|
95
|
+
name: {
|
|
96
|
+
fontSize: 18,
|
|
97
|
+
fontWeight: '700',
|
|
98
|
+
marginBottom: 4,
|
|
99
|
+
},
|
|
100
|
+
description: {
|
|
101
|
+
fontSize: 14,
|
|
102
|
+
marginBottom: 16,
|
|
103
|
+
},
|
|
104
|
+
footer: {
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
justifyContent: 'space-between',
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
},
|
|
109
|
+
cost: {
|
|
110
|
+
flexDirection: 'row',
|
|
111
|
+
alignItems: 'baseline',
|
|
112
|
+
},
|
|
113
|
+
costAmount: {
|
|
114
|
+
fontSize: 20,
|
|
115
|
+
fontWeight: '800',
|
|
116
|
+
},
|
|
117
|
+
costCurrency: {
|
|
118
|
+
fontSize: 12,
|
|
119
|
+
marginLeft: 4,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export default RewardCard;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
ScrollView,
|
|
8
|
+
TouchableOpacity,
|
|
9
|
+
SafeAreaView,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { useTheme } from '../theme/context';
|
|
13
|
+
import { GradientCard } from './GradientCard';
|
|
14
|
+
|
|
15
|
+
interface RulesModalProps {
|
|
16
|
+
visible: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function RulesModal({ visible, onClose }: RulesModalProps) {
|
|
21
|
+
const theme = useTheme();
|
|
22
|
+
|
|
23
|
+
const rules = [
|
|
24
|
+
{ icon: '🛍️', title: 'Spend & Earn', desc: 'Earn 1 XP for every $5 spent on your QwikCard.' },
|
|
25
|
+
{ icon: '🧾', title: 'Pay Bills', desc: 'Get +50 XP and 1 Qwik Coin for every bill paid.' },
|
|
26
|
+
{
|
|
27
|
+
icon: '🔥',
|
|
28
|
+
title: 'Streaks',
|
|
29
|
+
desc: 'Maintain a daily login streak for massive XP milestones.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
icon: '🤝',
|
|
33
|
+
title: 'Refer Friends',
|
|
34
|
+
desc: 'Earn +500 XP and 5 Qwik Coins when friends activate.',
|
|
35
|
+
},
|
|
36
|
+
{ icon: '📊', title: 'Credit Checks', desc: 'Check your credit score weekly to earn +50 XP.' },
|
|
37
|
+
{
|
|
38
|
+
icon: '💡',
|
|
39
|
+
title: 'Financial Literacy',
|
|
40
|
+
desc: 'Complete quizzes to earn +100 XP per lesson.',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Modal visible={visible} animationType="slide" transparent={true} onRequestClose={onClose}>
|
|
46
|
+
<View style={styles.overlay}>
|
|
47
|
+
<SafeAreaView style={styles.safeArea}>
|
|
48
|
+
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
|
|
49
|
+
<View style={styles.header}>
|
|
50
|
+
<Text style={[styles.title, { color: theme.colors.text.primary }]}>How to Play</Text>
|
|
51
|
+
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
|
|
52
|
+
<Text style={{ fontSize: 24, color: theme.colors.text.secondary }}>✕</Text>
|
|
53
|
+
</TouchableOpacity>
|
|
54
|
+
</View>
|
|
55
|
+
|
|
56
|
+
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
57
|
+
<Text style={[styles.subtitle, { color: theme.colors.text.secondary }]}>
|
|
58
|
+
Level up your financial future by building healthy habits. Here is how you earn
|
|
59
|
+
rewards:
|
|
60
|
+
</Text>
|
|
61
|
+
|
|
62
|
+
{rules.map((rule, index) => (
|
|
63
|
+
<GradientCard key={index} style={styles.ruleCard}>
|
|
64
|
+
<View style={styles.ruleRow}>
|
|
65
|
+
<View style={styles.iconContainer}>
|
|
66
|
+
<Text style={{ fontSize: 24 }}>{rule.icon}</Text>
|
|
67
|
+
</View>
|
|
68
|
+
<View style={styles.textContainer}>
|
|
69
|
+
<Text style={[styles.ruleTitle, { color: theme.colors.text.primary }]}>
|
|
70
|
+
{rule.title}
|
|
71
|
+
</Text>
|
|
72
|
+
<Text style={[styles.ruleDesc, { color: theme.colors.text.secondary }]}>
|
|
73
|
+
{rule.desc}
|
|
74
|
+
</Text>
|
|
75
|
+
</View>
|
|
76
|
+
</View>
|
|
77
|
+
</GradientCard>
|
|
78
|
+
))}
|
|
79
|
+
|
|
80
|
+
<TouchableOpacity
|
|
81
|
+
onPress={onClose}
|
|
82
|
+
style={[styles.actionButton, { backgroundColor: theme.colors.primary }]}
|
|
83
|
+
>
|
|
84
|
+
<Text style={styles.actionButtonText}>Got it!</Text>
|
|
85
|
+
</TouchableOpacity>
|
|
86
|
+
</ScrollView>
|
|
87
|
+
</View>
|
|
88
|
+
</SafeAreaView>
|
|
89
|
+
</View>
|
|
90
|
+
</Modal>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const styles = StyleSheet.create({
|
|
95
|
+
overlay: {
|
|
96
|
+
flex: 1,
|
|
97
|
+
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
|
98
|
+
},
|
|
99
|
+
safeArea: {
|
|
100
|
+
flex: 1,
|
|
101
|
+
justifyContent: 'flex-end',
|
|
102
|
+
},
|
|
103
|
+
container: {
|
|
104
|
+
borderTopLeftRadius: 32,
|
|
105
|
+
borderTopRightRadius: 32,
|
|
106
|
+
height: '90%',
|
|
107
|
+
padding: 24,
|
|
108
|
+
},
|
|
109
|
+
header: {
|
|
110
|
+
flexDirection: 'row',
|
|
111
|
+
justifyContent: 'space-between',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
marginBottom: 20,
|
|
114
|
+
},
|
|
115
|
+
title: {
|
|
116
|
+
fontSize: 24,
|
|
117
|
+
fontWeight: '900',
|
|
118
|
+
},
|
|
119
|
+
closeButton: {
|
|
120
|
+
padding: 8,
|
|
121
|
+
},
|
|
122
|
+
scrollContent: {
|
|
123
|
+
paddingBottom: 40,
|
|
124
|
+
},
|
|
125
|
+
subtitle: {
|
|
126
|
+
fontSize: 14,
|
|
127
|
+
lineHeight: 20,
|
|
128
|
+
marginBottom: 24,
|
|
129
|
+
},
|
|
130
|
+
ruleCard: {
|
|
131
|
+
padding: 16,
|
|
132
|
+
marginBottom: 12,
|
|
133
|
+
borderRadius: 20,
|
|
134
|
+
},
|
|
135
|
+
ruleRow: {
|
|
136
|
+
flexDirection: 'row',
|
|
137
|
+
alignItems: 'center',
|
|
138
|
+
},
|
|
139
|
+
iconContainer: {
|
|
140
|
+
width: 48,
|
|
141
|
+
height: 48,
|
|
142
|
+
borderRadius: 14,
|
|
143
|
+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
144
|
+
alignItems: 'center',
|
|
145
|
+
justifyContent: 'center',
|
|
146
|
+
marginRight: 16,
|
|
147
|
+
},
|
|
148
|
+
textContainer: {
|
|
149
|
+
flex: 1,
|
|
150
|
+
},
|
|
151
|
+
ruleTitle: {
|
|
152
|
+
fontSize: 16,
|
|
153
|
+
fontWeight: '700',
|
|
154
|
+
marginBottom: 2,
|
|
155
|
+
},
|
|
156
|
+
ruleDesc: {
|
|
157
|
+
fontSize: 12,
|
|
158
|
+
lineHeight: 16,
|
|
159
|
+
},
|
|
160
|
+
actionButton: {
|
|
161
|
+
marginTop: 24,
|
|
162
|
+
padding: 18,
|
|
163
|
+
borderRadius: 18,
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
},
|
|
166
|
+
actionButtonText: {
|
|
167
|
+
color: 'white',
|
|
168
|
+
fontSize: 16,
|
|
169
|
+
fontWeight: '800',
|
|
170
|
+
},
|
|
171
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePlaybasis } from '../PlaybasisProvider';
|
|
4
|
+
import type { Badge } from '../types';
|
|
5
|
+
|
|
6
|
+
interface UseBadgesReturn {
|
|
7
|
+
badges: Badge[];
|
|
8
|
+
earnedBadges: Badge[];
|
|
9
|
+
allBadges: Badge[];
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
refresh: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useBadges(): UseBadgesReturn {
|
|
16
|
+
const { client, playerId } = usePlaybasis();
|
|
17
|
+
const [badges, setBadges] = useState<Badge[]>([]);
|
|
18
|
+
const [allBadges, setAllBadges] = useState<Badge[]>([]);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [error, setError] = useState<Error | null>(null);
|
|
21
|
+
|
|
22
|
+
const fetchBadges = useCallback(async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Fetch all available badges
|
|
28
|
+
const all = await client.getBadges();
|
|
29
|
+
setAllBadges(all);
|
|
30
|
+
|
|
31
|
+
// Fetch player's earned badges
|
|
32
|
+
if (playerId) {
|
|
33
|
+
const playerBadges = await client.getPlayerBadges(playerId);
|
|
34
|
+
setBadges(playerBadges);
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch badges'));
|
|
38
|
+
} finally {
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
}, [client, playerId]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
fetchBadges();
|
|
45
|
+
}, [fetchBadges]);
|
|
46
|
+
|
|
47
|
+
const earnedBadges = badges.filter((b) => b.isEarned);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
badges,
|
|
51
|
+
earnedBadges,
|
|
52
|
+
allBadges,
|
|
53
|
+
loading,
|
|
54
|
+
error,
|
|
55
|
+
refresh: fetchBadges,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default useBadges;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePlaybasis } from '../PlaybasisProvider';
|
|
4
|
+
import type { PointBalance, Currency } from '../types';
|
|
5
|
+
|
|
6
|
+
interface UsePointsReturn {
|
|
7
|
+
balances: PointBalance[];
|
|
8
|
+
xp: number;
|
|
9
|
+
qwikCoins: number;
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
refresh: () => Promise<void>;
|
|
13
|
+
earn: (currency: Currency, amount: number, reason?: string) => Promise<PointBalance | null>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function usePoints(): UsePointsReturn {
|
|
17
|
+
const { client, playerId } = usePlaybasis();
|
|
18
|
+
const [balances, setBalances] = useState<PointBalance[]>([]);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [error, setError] = useState<Error | null>(null);
|
|
21
|
+
|
|
22
|
+
const fetchBalances = useCallback(async () => {
|
|
23
|
+
if (!playerId) {
|
|
24
|
+
setBalances([]);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const playerBalances = await client.getBalances(playerId);
|
|
34
|
+
setBalances(playerBalances);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch balances'));
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}, [client, playerId]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
fetchBalances();
|
|
44
|
+
}, [fetchBalances]);
|
|
45
|
+
|
|
46
|
+
const earn = useCallback(
|
|
47
|
+
async (currency: Currency, amount: number, reason?: string): Promise<PointBalance | null> => {
|
|
48
|
+
if (!playerId) return null;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const balance = await client.earnPoints({
|
|
52
|
+
playerId,
|
|
53
|
+
currency,
|
|
54
|
+
amount,
|
|
55
|
+
reason,
|
|
56
|
+
});
|
|
57
|
+
// Update local state
|
|
58
|
+
setBalances((prev) => {
|
|
59
|
+
const idx = prev.findIndex((b) => b.currency === currency);
|
|
60
|
+
if (idx >= 0) {
|
|
61
|
+
const updated = [...prev];
|
|
62
|
+
updated[idx] = balance;
|
|
63
|
+
return updated;
|
|
64
|
+
}
|
|
65
|
+
return [...prev, balance];
|
|
66
|
+
});
|
|
67
|
+
return balance;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
setError(err instanceof Error ? err : new Error('Failed to earn points'));
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[client, playerId],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Helper getters for common currencies
|
|
77
|
+
const xp = balances.find((b) => b.currency === 'xp')?.balance ?? 0;
|
|
78
|
+
const qwikCoins = balances.find((b) => b.currency === 'coins')?.balance ?? 0;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
balances,
|
|
82
|
+
xp,
|
|
83
|
+
qwikCoins,
|
|
84
|
+
loading,
|
|
85
|
+
error,
|
|
86
|
+
refresh: fetchBalances,
|
|
87
|
+
earn,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default usePoints;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePlaybasis } from '../PlaybasisProvider';
|
|
4
|
+
import type { Quest } from '../types';
|
|
5
|
+
|
|
6
|
+
interface UseQuestsReturn {
|
|
7
|
+
quests: Quest[];
|
|
8
|
+
completedQuests: Quest[];
|
|
9
|
+
activeQuests: Quest[];
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
refresh: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useQuests(): UseQuestsReturn {
|
|
16
|
+
const { client, playerId } = usePlaybasis();
|
|
17
|
+
const [quests, setQuests] = useState<Quest[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState<Error | null>(null);
|
|
20
|
+
|
|
21
|
+
const fetchQuests = useCallback(async () => {
|
|
22
|
+
if (!playerId) {
|
|
23
|
+
setQuests([]);
|
|
24
|
+
setLoading(false);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const playerQuests = await client.getPlayerQuests(playerId);
|
|
33
|
+
setQuests(playerQuests);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch quests'));
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
}, [client, playerId]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetchQuests();
|
|
43
|
+
}, [fetchQuests]);
|
|
44
|
+
|
|
45
|
+
const activeQuests = quests.filter(
|
|
46
|
+
(q) => q.status === 'in_progress' || q.status === 'not_started',
|
|
47
|
+
);
|
|
48
|
+
const completedQuests = quests.filter((q) => q.status === 'completed');
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
quests,
|
|
52
|
+
activeQuests,
|
|
53
|
+
completedQuests,
|
|
54
|
+
loading,
|
|
55
|
+
error,
|
|
56
|
+
refresh: fetchQuests,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default useQuests;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePlaybasis } from '../PlaybasisProvider';
|
|
4
|
+
import { usePoints } from './usePoints';
|
|
5
|
+
import { useQuests } from './useQuests';
|
|
6
|
+
import { useRewards } from './useRewards';
|
|
7
|
+
import { useBadges } from './useBadges';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates and aggregates all SDK data hooks.
|
|
11
|
+
* Use this to ensure the "App" has all necessary data loaded.
|
|
12
|
+
*/
|
|
13
|
+
export function useQwikApp() {
|
|
14
|
+
const { client, playerId } = usePlaybasis();
|
|
15
|
+
const { balances, refresh: refreshPoints, loading: loadingPoints } = usePoints();
|
|
16
|
+
const { quests, loading: loadingQuests } = useQuests();
|
|
17
|
+
const { rewards, loading: loadingRewards } = useRewards();
|
|
18
|
+
const { badges, loading: loadingBadges } = useBadges();
|
|
19
|
+
|
|
20
|
+
const [isReady, setIsReady] = useState(false);
|
|
21
|
+
|
|
22
|
+
// Check if critical data is loaded
|
|
23
|
+
const isLoading = loadingPoints || loadingQuests || loadingRewards || loadingBadges;
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!isLoading && playerId) {
|
|
27
|
+
setIsReady(true);
|
|
28
|
+
}
|
|
29
|
+
}, [isLoading, playerId]);
|
|
30
|
+
|
|
31
|
+
const refreshAll = async () => {
|
|
32
|
+
await Promise.all([
|
|
33
|
+
refreshPoints(),
|
|
34
|
+
// Add other refresh methods if exposed by hooks
|
|
35
|
+
]);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isReady,
|
|
40
|
+
isLoading,
|
|
41
|
+
refreshAll,
|
|
42
|
+
data: {
|
|
43
|
+
balances,
|
|
44
|
+
quests,
|
|
45
|
+
rewards,
|
|
46
|
+
badges,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePlaybasis } from '../PlaybasisProvider';
|
|
4
|
+
import type { Reward, RedemptionResult } from '../types';
|
|
5
|
+
|
|
6
|
+
interface UseRewardsReturn {
|
|
7
|
+
rewards: Reward[];
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
refresh: () => Promise<void>;
|
|
11
|
+
redeem: (rewardId: string) => Promise<RedemptionResult>;
|
|
12
|
+
redeeming: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useRewards(): UseRewardsReturn {
|
|
16
|
+
const { client, playerId } = usePlaybasis();
|
|
17
|
+
const [rewards, setRewards] = useState<Reward[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [redeeming, setRedeeming] = useState(false);
|
|
20
|
+
const [error, setError] = useState<Error | null>(null);
|
|
21
|
+
|
|
22
|
+
const fetchRewards = useCallback(async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const allRewards = await client.getRewards();
|
|
28
|
+
setRewards(allRewards);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setError(err instanceof Error ? err : new Error('Failed to fetch rewards'));
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}, [client]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
fetchRewards();
|
|
38
|
+
}, [fetchRewards]);
|
|
39
|
+
|
|
40
|
+
const redeem = useCallback(
|
|
41
|
+
async (rewardId: string): Promise<RedemptionResult> => {
|
|
42
|
+
if (!playerId) {
|
|
43
|
+
return { success: false, message: 'No player ID set' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setRedeeming(true);
|
|
47
|
+
try {
|
|
48
|
+
const result = await client.redeemReward(playerId, rewardId);
|
|
49
|
+
// Refresh rewards after redemption
|
|
50
|
+
await fetchRewards();
|
|
51
|
+
return result;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
message: err instanceof Error ? err.message : 'Redemption failed',
|
|
56
|
+
};
|
|
57
|
+
} finally {
|
|
58
|
+
setRedeeming(false);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[client, playerId, fetchRewards],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
rewards,
|
|
66
|
+
loading,
|
|
67
|
+
error,
|
|
68
|
+
refresh: fetchRewards,
|
|
69
|
+
redeem,
|
|
70
|
+
redeeming,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default useRewards;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playbasis SDK for QwikCard College Rewards
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { PlaybasisProvider, usePlaybasis } from './PlaybasisProvider';
|
|
8
|
+
export { PlaybasisClient } from './api/client';
|
|
9
|
+
export { useQuests } from './hooks/useQuests';
|
|
10
|
+
export { useRewards } from './hooks/useRewards';
|
|
11
|
+
export { useBadges } from './hooks/useBadges';
|
|
12
|
+
export { usePoints } from './hooks/usePoints';
|
|
13
|
+
export { QuestProgress } from './components/QuestProgress';
|
|
14
|
+
export { RewardCard } from './components/RewardCard';
|
|
15
|
+
export { BadgeIcon } from './components/BadgeIcon';
|
|
16
|
+
export { PointsBalance } from './components/PointsBalance';
|
|
17
|
+
|
|
18
|
+
// Theme
|
|
19
|
+
export * from './theme/context';
|
|
20
|
+
export * from './theme/tokens';
|
|
21
|
+
|
|
22
|
+
// New Components
|
|
23
|
+
export * from './components/GradientCard';
|
|
24
|
+
export * from './components/Badge'; // Exports StatBadge
|
|
25
|
+
export * from './components/Button';
|
|
26
|
+
export * from './components/ProgressBar';
|
|
27
|
+
export * from './components/RadialGauge';
|
|
28
|
+
export * from './components/RulesModal';
|
|
29
|
+
|
|
30
|
+
// App
|
|
31
|
+
export { QwikCardApp } from './QwikCardApp';
|
|
32
|
+
export { useQwikApp } from './hooks/useQwikApp';
|
|
33
|
+
|
|
34
|
+
export * from './types';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React, { createContext, useContext, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { QwikTheme, qwikTheme } from './tokens';
|
|
4
|
+
|
|
5
|
+
const ThemeContext = createContext<QwikTheme>(qwikTheme);
|
|
6
|
+
|
|
7
|
+
export const ThemeProvider = ({
|
|
8
|
+
children,
|
|
9
|
+
theme = qwikTheme,
|
|
10
|
+
}: {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
theme?: QwikTheme;
|
|
13
|
+
}) => {
|
|
14
|
+
return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const useTheme = () => useContext(ThemeContext);
|