@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.
Files changed (106) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +267 -0
  4. package/SDK_HANDOVER_GUIDE.md +129 -0
  5. package/dist/PlaybasisProvider.d.ts +19 -0
  6. package/dist/PlaybasisProvider.d.ts.map +1 -0
  7. package/dist/PlaybasisProvider.js +24 -0
  8. package/dist/QwikCardApp.d.ts +9 -0
  9. package/dist/QwikCardApp.d.ts.map +1 -0
  10. package/dist/QwikCardApp.js +210 -0
  11. package/dist/api/client.d.ts +66 -0
  12. package/dist/api/client.d.ts.map +1 -0
  13. package/dist/api/client.js +196 -0
  14. package/dist/components/Badge.d.ts +8 -0
  15. package/dist/components/Badge.d.ts.map +1 -0
  16. package/dist/components/Badge.js +34 -0
  17. package/dist/components/BadgeIcon.d.ts +10 -0
  18. package/dist/components/BadgeIcon.d.ts.map +1 -0
  19. package/dist/components/BadgeIcon.js +51 -0
  20. package/dist/components/Button.d.ts +10 -0
  21. package/dist/components/Button.d.ts.map +1 -0
  22. package/dist/components/Button.js +40 -0
  23. package/dist/components/GradientCard.d.ts +9 -0
  24. package/dist/components/GradientCard.d.ts.map +1 -0
  25. package/dist/components/GradientCard.js +28 -0
  26. package/dist/components/PointsBalance.d.ts +8 -0
  27. package/dist/components/PointsBalance.d.ts.map +1 -0
  28. package/dist/components/PointsBalance.js +85 -0
  29. package/dist/components/ProgressBar.d.ts +8 -0
  30. package/dist/components/ProgressBar.d.ts.map +1 -0
  31. package/dist/components/ProgressBar.js +41 -0
  32. package/dist/components/QuestProgress.d.ts +10 -0
  33. package/dist/components/QuestProgress.d.ts.map +1 -0
  34. package/dist/components/QuestProgress.js +94 -0
  35. package/dist/components/RadialGauge.d.ts +8 -0
  36. package/dist/components/RadialGauge.d.ts.map +1 -0
  37. package/dist/components/RadialGauge.js +53 -0
  38. package/dist/components/RewardCard.d.ts +10 -0
  39. package/dist/components/RewardCard.d.ts.map +1 -0
  40. package/dist/components/RewardCard.js +64 -0
  41. package/dist/components/RulesModal.d.ts +7 -0
  42. package/dist/components/RulesModal.d.ts.map +1 -0
  43. package/dist/components/RulesModal.js +106 -0
  44. package/dist/hooks/useBadges.d.ts +12 -0
  45. package/dist/hooks/useBadges.d.ts.map +1 -0
  46. package/dist/hooks/useBadges.js +42 -0
  47. package/dist/hooks/usePoints.d.ts +13 -0
  48. package/dist/hooks/usePoints.d.ts.map +1 -0
  49. package/dist/hooks/usePoints.js +70 -0
  50. package/dist/hooks/useQuests.d.ts +12 -0
  51. package/dist/hooks/useQuests.d.ts.map +1 -0
  52. package/dist/hooks/useQuests.js +41 -0
  53. package/dist/hooks/useQwikApp.d.ts +16 -0
  54. package/dist/hooks/useQwikApp.d.ts.map +1 -0
  55. package/dist/hooks/useQwikApp.js +42 -0
  56. package/dist/hooks/useRewards.d.ts +12 -0
  57. package/dist/hooks/useRewards.d.ts.map +1 -0
  58. package/dist/hooks/useRewards.js +56 -0
  59. package/dist/index.d.ts +27 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +29 -0
  62. package/dist/theme/context.d.ts +8 -0
  63. package/dist/theme/context.d.ts.map +1 -0
  64. package/dist/theme/context.js +8 -0
  65. package/dist/theme/tokens.d.ts +35 -0
  66. package/dist/theme/tokens.d.ts.map +1 -0
  67. package/dist/theme/tokens.js +33 -0
  68. package/dist/types/index.d.ts +119 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/dist/types/index.js +4 -0
  71. package/dist/web/widgetAssets.d.ts +4 -0
  72. package/dist/web/widgetAssets.d.ts.map +1 -0
  73. package/dist/web/widgetAssets.js +5 -0
  74. package/dist/web/widgetHtml.d.ts +2 -0
  75. package/dist/web/widgetHtml.d.ts.map +1 -0
  76. package/dist/web/widgetHtml.js +299 -0
  77. package/dist/web/widgetTypes.d.ts +128 -0
  78. package/dist/web/widgetTypes.d.ts.map +1 -0
  79. package/dist/web/widgetTypes.js +1 -0
  80. package/package.json +86 -0
  81. package/src/PlaybasisProvider.tsx +72 -0
  82. package/src/QwikCardApp.tsx +302 -0
  83. package/src/api/client.ts +307 -0
  84. package/src/components/Badge.tsx +51 -0
  85. package/src/components/BadgeIcon.tsx +97 -0
  86. package/src/components/Button.tsx +70 -0
  87. package/src/components/GradientCard.tsx +49 -0
  88. package/src/components/PointsBalance.tsx +122 -0
  89. package/src/components/ProgressBar.tsx +65 -0
  90. package/src/components/QuestProgress.tsx +153 -0
  91. package/src/components/RadialGauge.tsx +101 -0
  92. package/src/components/RewardCard.tsx +123 -0
  93. package/src/components/RulesModal.tsx +171 -0
  94. package/src/hooks/useBadges.ts +59 -0
  95. package/src/hooks/usePoints.ts +91 -0
  96. package/src/hooks/useQuests.ts +60 -0
  97. package/src/hooks/useQwikApp.ts +49 -0
  98. package/src/hooks/useRewards.ts +74 -0
  99. package/src/index.ts +34 -0
  100. package/src/theme/context.tsx +17 -0
  101. package/src/theme/tokens.ts +68 -0
  102. package/src/types/index.ts +176 -0
  103. package/src/web/widgetAssets.d.ts +3 -0
  104. package/src/web/widgetAssets.ts +6 -0
  105. package/src/web/widgetHtml.ts +302 -0
  106. 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);