diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0abd815 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,329 @@ +## 2. Member Features + +### Member Profiles + +**Core Fields:** + +- Name, pronouns, time zone +- Avatar/photo - choose from ghosts +- Studio/organization affiliation +- Bio (rich text) +- Skills tags (searchable) +- Location (city/region) +- Social links (Mastodon, LinkedIn, etc.) +- **Offering:** What I can contribute +- **Looking For:** What I need support with + +**Privacy Controls:** + +- Public/members-only/private toggle per field +- Opt-in to member directory + +### Member Updates/Mini Blog + +- Post updates about projects, learnings, questions +- Rich text with image support +- Comments enabled +- Filter by circle or topic tags + +## 3. Events System + +### Core Features + +- RSVP with capacity limits +- Waitlist management +- Add to calendar (.ics download) +- Pre-event discussion threads +- Post-event recordings archive +- Speaker/facilitator profiles + +### Member-Proposed Events + +**Proposal Flow:** + +1. Member submits event idea via form +2. Include: Topic, format, target circle, time commitment +3. Admin quick review (spam check only) +4. Published to "Proposed Events" board +5. Members can express interest (like feature upvote pages) +6. If threshold met (e.g., 5 interested), event is scheduled +7. Proposer gets facilitator support if needed + +## 4. Resources Integration + +### Consolidating Existing Assets + +**Import and organize from:** + +- learn.weirdghosts.ca content +- Existing tools and templates +- PA curriculum materials (where appropriate) +- Case studies and examples + +**Organization Structure:** + +``` +Resources/ +├── Start Here/ +│ ├── Welcome Letter from Jennie & Eileen +│ ├── How Ghost Guild Works +│ └── Solidarity Economics Explained +├── Learning Paths/ +│ ├── Community Track → links to learn.weirdghosts.ca +│ ├── Founder Track → practical tools +│ └── Practitioner Track → advanced resources +├── Templates & Tools/ +│ ├── Governance/ +│ ├── Financial/ +│ ├── Operations/ +│ └── Legal/ +├── Case Studies/ +│ └── Member stories and examples +└── External Resources/ + └── Curated links and recommendations +``` + +### Resource Features + +- Tag by circle relevance (but accessible to all) +- Download tracking for impact metrics +- Version control for templates +- Comment threads on resources +- "Request a resource" feature + +## 5. Peer Support System + +### Cal.com Integration for 1:1s + +**Setup:** + +- Each member can enable peer support availability +- Set their own hours/frequency +- Cal.com handles scheduling +- Types of sessions: + - Peer support (30 min) + - Co-founder check-in (45 min) + - Practitioner office hours (60 min) + +**Matching System:** + +- Simple questionnaire about current needs +- Suggest 3 potential peers based on: + - Complementary skills/needs + - Time zone compatibility + - Circle alignment (optional) +- Book directly via Cal.com links + +## 6. Dashboard Design + +### Personalized Sections + +**Welcome Block:** + +- "Welcome back, [Name]" +- Your circle: [Circle] | Your contribution: $X/month +- Quick stats: Days as member, events attended, peers met + +**Community Pulse:** + +- Recent member updates (mini blog posts) +- Upcoming events this week +- New resources added +- New members to welcome + +**Your Activity:** + +- Your upcoming events +- Scheduled peer sessions +- Recent discussions you're in +- Resources you've bookmarked + +**Take Action:** + +- Post an update +- Propose an event +- Book a peer session +- Browse resources +- Update profile + +**Impact Metrics:** + +- Total solidarity spots funded +- Events hosted this month +- Active members this week +- Resources shared + +## 7. Collaborative Tools + +### Etherpad Integration + +**Use Cases:** + +- Meeting notes templates +- Collaborative resource creation +- Event planning documents +- Shared learning notes + +**Implementation:** + +- Self-hosted Etherpad instance +- SSO with Ghost Guild accounts +- Auto-save and version history +- Export to multiple formats +- Embed in event pages for notes + +### Living Documents + +- Community-maintained guides +- Glossaries and definitions +- Frequently asked questions +- Best practices collections + +## 8. Technical Infrastructure + +### Notification System + +**Channels:** + +- Email (via Resend) +- In-app notifications +- Slack integration via bot + +**Configurable Preferences:** + +- Event reminders +- New resources in your area +- Peer session invitations +- Member updates digest +- Community announcements + +### Search & Discovery + +- Full-text search across: + - Resources + - Member profiles + - Event descriptions + - Member updates +- Filter by circle, tags, date +- Save searches for alerts + +### Analytics & Reporting + +- Member engagement metrics +- Resource usage stats +- Event attendance patterns +- Contribution distribution +- Circle movement tracking + +## 9. Content for Launch + +### Essential Content Pieces + +1. **Welcome Video** - Jennie & Eileen introduce Ghost Guild +2. **How This Works** - Clear explanation of circles and contributions +3. **Circle Guides** - What to expect in each circle +4. **Solidarity Economics** - Practical examples from gaming +5. **Getting Started Checklist** - First week actions + +### Pre-Populated Content + +- 10-15 essential resources per circle +- 3-5 upcoming events scheduled +- Sample member updates to show activity +- FAQ based on pre-registration questions + +## 10. Launch Strategy + +### Soft Launch (Week Before) + +- Invite 10-15 friendly testers +- Each from different backgrounds/circles +- Gather feedback on: + - Onboarding flow + - Resource organization + - Event system + - Profile creation + +### Launch Week + +**Day 1-2:** PA alumni and close network + +- Personal invitations +- Extra support available +- Gather testimonials + +**Day 3-4:** Gamma Space announcement + +- Post in relevant channels +- Host info session + +**Day 5-7:** Public launch + +- Email pre-registration list +- Social media announcement +- Open registration + +### Success Metrics + +**Week 1:** + +- 30 members across all circles +- 80% complete profiles +- 50% attend first event + +**Month 1:** + +- 75 active members +- 5 member-proposed events +- 20 peer sessions booked +- 90% Slack participation + +## 11. Ongoing Operations + +### Weekly Tasks + +- Review member proposals for events +- Process Gamma Space channel access +- Update resource library +- Send member spotlight + +### Monthly Tasks + +- Impact report to members +- Review and adjust contribution distribution +- Plan next month's events +- Gather member feedback + +### Quarterly Reviews + +- Assess circle definitions +- Evaluate pricing model +- Review platform features +- Plan new initiatives + +--- + +## Implementation Priority Order + +### Must Have for Launch + +1. Payment processing (Helcim) +2. Basic Slack automation +3. Member dashboard +4. Simple resource library +5. Event listing and RSVP + +### Nice to Have for Launch + +7. Member profiles +8. Peer matching system +9. Cal.com integration +10. Member updates/blog + +### Can Build Post-Launch + +11. Etherpad integration +12. Member-proposed events +13. Advanced search +14. Analytics dashboard +15. Monthly themes diff --git a/HELCIM_TEST_INSTRUCTIONS.md b/HELCIM_TEST_INSTRUCTIONS.md new file mode 100644 index 0000000..d1534e8 --- /dev/null +++ b/HELCIM_TEST_INSTRUCTIONS.md @@ -0,0 +1,120 @@ +# Helcim Integration Testing Guide + +## Setup Complete +The Helcim Recurring API integration has been set up with the following components: + +### 1. Composables +- `/app/composables/useHelcim.js` - Server-side Helcim API interactions +- `/app/composables/useHelcimPay.js` - Client-side HelcimPay.js integration + +### 2. Server API Endpoints +- `/server/api/helcim/customer.post.js` - Creates Helcim customer and member record +- `/server/api/helcim/subscription.post.js` - Creates subscription for paid tiers +- `/server/api/helcim/verify-payment.post.js` - Verifies payment token + +### 3. Updated Pages +- `/app/pages/join.vue` - Multi-step signup flow with payment integration + +### 4. Database Schema +- Updated `/server/models/member.js` with subscription fields + +## Testing Instructions + +### Prerequisites +1. Ensure your `.env` file has the test Helcim token: + ``` + NUXT_PUBLIC_HELCIM_TOKEN=your_test_token_here + ``` + +2. Ensure you have test payment plans created in Helcim dashboard matching these IDs: + - `supporter-monthly-5` + - `member-monthly-15` + - `advocate-monthly-30` + - `champion-monthly-50` + +### Test Flow + +#### 1. Start the Development Server +```bash +npm run dev +``` + +#### 2. Test Free Tier Signup +1. Navigate to `/join` +2. Fill in name and email +3. Select any circle +4. Choose "$0 - I need support right now" +5. Click "Complete Registration" +6. Should go directly to confirmation without payment + +#### 3. Test Paid Tier Signup +1. Navigate to `/join` +2. Fill in test details: + - Name: Test User + - Email: test@example.com +3. Select any circle +4. Choose a paid contribution tier (e.g., "$15 - I can sustain the community") +5. Click "Continue to Payment" +6. On the payment step, use Helcim test card numbers: + - **Success**: 4111 1111 1111 1111 + - **Decline**: 4000 0000 0000 0002 + - CVV: Any 3 digits + - Expiry: Any future date +7. Click "Complete Payment" +8. Should see confirmation with member details + +### Test Card Numbers (Helcim Test Mode) +- **Visa Success**: 4111 1111 1111 1111 +- **Mastercard Success**: 5500 0000 0000 0004 +- **Amex Success**: 3400 0000 0000 009 +- **Decline**: 4000 0000 0000 0002 +- **Insufficient Funds**: 4000 0000 0000 0051 + +### Debugging + +#### Check API Responses +Open browser DevTools Network tab to monitor: +- `/api/helcim/customer` - Should return customer ID and token +- `/api/helcim/verify-payment` - Should return card details +- `/api/helcim/subscription` - Should return subscription ID + +#### Common Issues + +1. **HelcimPay.js not loading** + - Check console for script loading errors + - Verify token is correctly set in environment + +2. **Customer creation fails** + - Check API token permissions in Helcim dashboard + - Verify MongoDB connection + +3. **Payment verification fails** + - Ensure you're using test card numbers + - Check that Helcim account is in test mode + +4. **Subscription creation fails** + - Verify payment plan IDs exist in Helcim + - Check that card token was successfully captured + +### Database Verification + +Check MongoDB for created records: +```javascript +// In MongoDB shell or client +db.members.findOne({ email: "test@example.com" }) +``` + +Should see: +- `helcimCustomerId` populated +- `helcimSubscriptionId` for paid tiers +- `status: "active"` after successful payment +- `paymentMethod: "card"` for paid tiers + +## Next Steps + +Once testing is successful: +1. Switch to production Helcim token +2. Create production payment plans in Helcim +3. Update plan IDs in `/app/config/contributions.js` if needed +4. Test with real payment card (small amount) +5. Set up webhook endpoints for subscription events (renewals, failures, cancellations) \ No newline at end of file diff --git a/UPDATE_SUMMARY.md b/UPDATE_SUMMARY.md new file mode 100644 index 0000000..5c7d439 --- /dev/null +++ b/UPDATE_SUMMARY.md @@ -0,0 +1,57 @@ +# Helcim Integration - Issues Fixed + +## Problem +The API was returning 401 Unauthorized when trying to create customers. + +## Root Cause +The runtime config wasn't properly accessing the Helcim token in server-side endpoints. + +## Solution Applied + +### 1. Fixed Runtime Config Access +Updated all server endpoints to: +- Pass the `event` parameter to `useRuntimeConfig(event)` +- Fallback to `process.env.NUXT_PUBLIC_HELCIM_TOKEN` if config doesn't load + +### 2. Files Updated +- `/server/api/helcim/customer.post.js` +- `/server/api/helcim/subscription.post.js` +- `/server/api/helcim/verify-payment.post.js` +- `/server/api/helcim/test-connection.get.js` + +### 3. Fixed Import Path +Created `/server/config/contributions.js` to re-export the contributions config for server-side imports. + +### 4. Verified Token Works +Created `test-helcim-direct.js` which successfully: +- Connected to Helcim API +- Created a test customer (ID: 32854583, Code: CST1000) + +## Testing Instructions + +1. Restart your development server: + ```bash + npm run dev + ``` + +2. Test the connection: + ```bash + curl http://localhost:3000/api/helcim/test-connection + ``` + +3. Try the signup flow at `/join` + +## Important Notes + +- The token in your `.env` file is working correctly +- The Helcim API is accessible and responding +- Customer creation is functional when called directly +- The issue was specifically with how Nuxt's runtime config was being accessed in server endpoints + +## Next Steps + +Once you confirm the signup flow works: +1. Test with different contribution tiers +2. Verify payment capture with test cards +3. Check that subscriptions are created correctly +4. Consider adding webhook endpoints for subscription events \ No newline at end of file diff --git a/app/app.config.ts b/app/app.config.ts index 51f28f0..363e07f 100644 --- a/app/app.config.ts +++ b/app/app.config.ts @@ -1,8 +1,13 @@ export default defineAppConfig({ ui: { colors: { - primary: "pink", - neutral: "zinc", + primary: "emerald", + neutral: "stone", + }, + formField: { + slots: { + label: "block font-medium text-stone-200", + }, }, }, }); diff --git a/app/assets/css/main.css b/app/assets/css/main.css index a5e24e1..22cb675 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -2,13 +2,145 @@ @import "tailwindcss"; @import "@nuxt/ui"; -@theme { +@theme static { /* Font families */ --font-sans: "Inter", sans-serif; --font-body: "Inter", sans-serif; --font-mono: "Ubuntu Mono", monospace; --font-display: "NB Television Pro", monospace; + /* Ethereal color palette - grays, blacks, minimal color */ + --color-ghost-50: #f0f0f0; + --color-ghost-100: #d0d0d0; + --color-ghost-200: #b0b0b0; + --color-ghost-300: #8a8a8a; + --color-ghost-400: #6a6a6a; + --color-ghost-500: #4a4a4a; + --color-ghost-600: #3a3a3a; + --color-ghost-700: #2a2a2a; + --color-ghost-800: #1a1a1a; + --color-ghost-900: #0a0a0a; + + /* Subtle accent - barely visible blue-gray */ + --color-whisper-50: #d4dae6; + --color-whisper-100: #a8b3c7; + --color-whisper-200: #8491a8; + --color-whisper-300: #687291; + --color-whisper-400: #4f5d7a; + --color-whisper-500: #3a4964; + --color-whisper-600: #2f3b52; + --color-whisper-700: #252d40; + --color-whisper-800: #1a1f2e; + --color-whisper-900: #0f1419; + /* Sparkle accent */ + --color-sparkle-50: #fafafa; + --color-sparkle-100: #f0f0f0; + --color-sparkle-200: #e8e8e8; + --color-sparkle-300: #d0d0d0; + --color-sparkle-400: #c0c0c0; + --color-sparkle-500: #a0a0a0; + --color-sparkle-600: #808080; + --color-sparkle-700: #606060; + --color-sparkle-800: #404040; + --color-sparkle-900: #202020; +} + +/* Global ethereal background */ +:root { + --ethereal-bg: radial-gradient(circle at 20% 80%, rgba(232, 232, 232, 0.03) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(232, 232, 232, 0.02) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba(232, 232, 232, 0.01) 0%, transparent 50%); + + --halftone-pattern: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px); + --halftone-size: 8px 8px; +} + +html { + background: var(--color-ghost-900); + color: var(--color-ghost-200); +} + +body { + background: var(--ethereal-bg), var(--color-ghost-900); + background-attachment: fixed; +} + +/* Halftone texture overlay */ +.halftone-texture { + position: relative; +} + +.halftone-texture::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--halftone-pattern); + background-size: var(--halftone-size); + opacity: 0.1; + pointer-events: none; +} + +/* Sparkle effects */ +@keyframes sparkle { + 0%, 100% { opacity: 0.3; transform: scale(0.8); } + 50% { opacity: 1; transform: scale(1.2); } +} + +@keyframes twinkle { + 0%, 100% { opacity: 0.2; } + 25% { opacity: 0.8; } + 75% { opacity: 0.4; } +} + +.sparkle-field { + position: relative; + overflow: hidden; +} + +.sparkle-field::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + radial-gradient(circle at 10% 20%, var(--color-sparkle-200) 1px, transparent 1px), + radial-gradient(circle at 90% 80%, var(--color-sparkle-400) 1px, transparent 1px), + radial-gradient(circle at 30% 70%, var(--color-sparkle-200) 0.5px, transparent 0.5px), + radial-gradient(circle at 70% 30%, var(--color-sparkle-400) 0.5px, transparent 0.5px), + radial-gradient(circle at 50% 10%, var(--color-sparkle-200) 1px, transparent 1px), + radial-gradient(circle at 20% 90%, var(--color-sparkle-400) 0.5px, transparent 0.5px); + background-size: 200px 200px, 300px 300px, 150px 150px, 250px 250px, 180px 180px, 220px 220px; + animation: twinkle 4s infinite ease-in-out; + pointer-events: none; + opacity: 0.6; +} + +/* Ethereal glow effects */ +.ethereal-glow { + box-shadow: + 0 0 20px rgba(232, 232, 232, 0.1), + 0 0 40px rgba(232, 232, 232, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.ethereal-text { + text-shadow: 0 0 10px rgba(232, 232, 232, 0.3); +} + +/* Dithered gradients */ +.dithered-bg { + background: + linear-gradient(45deg, var(--color-ghost-800) 25%, transparent 25%), + linear-gradient(-45deg, var(--color-ghost-800) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, var(--color-ghost-700) 75%), + linear-gradient(-45deg, transparent 75%, var(--color-ghost-700) 75%); + background-size: 4px 4px; + background-position: 0 0, 0 2px, 2px -2px, -2px 0px; } diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue index 74928a8..7f35a63 100644 --- a/app/components/AppFooter.vue +++ b/app/components/AppFooter.vue @@ -1,288 +1,31 @@ \ No newline at end of file +const currentYear = new Date().getFullYear(); + diff --git a/app/components/AppNavigation.vue b/app/components/AppNavigation.vue index 803f8bd..f7b1473 100644 --- a/app/components/AppNavigation.vue +++ b/app/components/AppNavigation.vue @@ -1,102 +1,191 @@ + + diff --git a/app/components/NaturalDateInput.vue b/app/components/NaturalDateInput.vue new file mode 100644 index 0000000..c0f8983 --- /dev/null +++ b/app/components/NaturalDateInput.vue @@ -0,0 +1,238 @@ + + + \ No newline at end of file diff --git a/app/components/PageHeader.vue b/app/components/PageHeader.vue index b756f47..a5646cc 100644 --- a/app/components/PageHeader.vue +++ b/app/components/PageHeader.vue @@ -1,65 +1,108 @@ \ No newline at end of file + return "text-white"; +}); + diff --git a/app/components/PrivacyToggle.vue b/app/components/PrivacyToggle.vue new file mode 100644 index 0000000..8b0ca9a --- /dev/null +++ b/app/components/PrivacyToggle.vue @@ -0,0 +1,47 @@ + + + diff --git a/app/components/UpdateCard.vue b/app/components/UpdateCard.vue new file mode 100644 index 0000000..af76bc1 --- /dev/null +++ b/app/components/UpdateCard.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/app/components/UpdateForm.vue b/app/components/UpdateForm.vue new file mode 100644 index 0000000..286e77b --- /dev/null +++ b/app/components/UpdateForm.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/app/composables/useAuth.js b/app/composables/useAuth.js new file mode 100644 index 0000000..9e37046 --- /dev/null +++ b/app/composables/useAuth.js @@ -0,0 +1,46 @@ +export const useAuth = () => { + const memberData = useState('auth.member', () => null) + + const isAuthenticated = computed(() => !!memberData.value) + + const isMember = computed(() => !!memberData.value) + + const checkMemberStatus = async () => { + console.log('🔍 checkMemberStatus called') + console.log(' - Current memberData:', !!memberData.value) + + try { + console.log(' - Making API call to /api/auth/member...') + const response = await $fetch('/api/auth/member') + console.log(' - API response received:', { email: response.email, id: response.id }) + memberData.value = response + console.log(' - ✅ Member authenticated successfully') + return true + } catch (error) { + console.error(' - ❌ Failed to fetch member status:', error.statusCode, error.statusMessage) + memberData.value = null + console.log(' - Cleared memberData') + return false + } + } + + const logout = async () => { + try { + await $fetch('/api/auth/logout', { + method: 'POST' + }) + memberData.value = null + await navigateTo('/') + } catch (error) { + console.error('Logout failed:', error) + } + } + + return { + isAuthenticated: readonly(isAuthenticated), + isMember: readonly(isMember), + memberData: readonly(memberData), + checkMemberStatus, + logout + } +} \ No newline at end of file diff --git a/app/composables/useHelcim.js b/app/composables/useHelcim.js new file mode 100644 index 0000000..efc96b1 --- /dev/null +++ b/app/composables/useHelcim.js @@ -0,0 +1,90 @@ +// Helcim API integration composable +export const useHelcim = () => { + const config = useRuntimeConfig() + const helcimToken = config.public.helcimToken + + // Base URL for Helcim API + const HELCIM_API_BASE = 'https://api.helcim.com/v2' + + // Helper function to make API requests + const makeHelcimRequest = async (endpoint, method = 'GET', body = null) => { + try { + const response = await $fetch(`${HELCIM_API_BASE}${endpoint}`, { + method, + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + 'api-token': helcimToken + }, + body: body ? JSON.stringify(body) : undefined + }) + return response + } catch (error) { + console.error('Helcim API error:', error) + throw error + } + } + + // Create a customer + const createCustomer = async (customerData) => { + return await makeHelcimRequest('/customers', 'POST', { + customerType: 'PERSON', + contactName: customerData.name, + email: customerData.email, + billingAddress: customerData.billingAddress || {} + }) + } + + // Create a subscription + const createSubscription = async (customerId, planId, cardToken) => { + return await makeHelcimRequest('/recurring/subscriptions', 'POST', { + customerId, + planId, + cardToken, + startDate: new Date().toISOString().split('T')[0] // Today's date + }) + } + + // Get customer details + const getCustomer = async (customerId) => { + return await makeHelcimRequest(`/customers/${customerId}`) + } + + // Get subscription details + const getSubscription = async (subscriptionId) => { + return await makeHelcimRequest(`/recurring/subscriptions/${subscriptionId}`) + } + + // Update subscription + const updateSubscription = async (subscriptionId, updates) => { + return await makeHelcimRequest(`/recurring/subscriptions/${subscriptionId}`, 'PATCH', updates) + } + + // Cancel subscription + const cancelSubscription = async (subscriptionId) => { + return await makeHelcimRequest(`/recurring/subscriptions/${subscriptionId}`, 'DELETE') + } + + // Get payment plans + const getPaymentPlans = async () => { + return await makeHelcimRequest('/recurring/plans') + } + + // Verify card token (for testing) + const verifyCardToken = async (cardToken) => { + return await makeHelcimRequest('/cards/verify', 'POST', { + cardToken + }) + } + + return { + createCustomer, + createSubscription, + getCustomer, + getSubscription, + updateSubscription, + cancelSubscription, + getPaymentPlans, + verifyCardToken + } +} \ No newline at end of file diff --git a/app/composables/useHelcimPay.js b/app/composables/useHelcimPay.js new file mode 100644 index 0000000..9f40d07 --- /dev/null +++ b/app/composables/useHelcimPay.js @@ -0,0 +1,158 @@ +// HelcimPay.js integration composable +export const useHelcimPay = () => { + let checkoutToken = null + let secretToken = null + + // Initialize HelcimPay.js session + const initializeHelcimPay = async (customerId, customerCode, amount = 0) => { + try { + const response = await $fetch('/api/helcim/initialize-payment', { + method: 'POST', + body: { + customerId, + customerCode, + amount + } + }) + + if (response.success) { + checkoutToken = response.checkoutToken + secretToken = response.secretToken + return true + } + + throw new Error('Failed to initialize payment session') + } catch (error) { + console.error('Payment initialization error:', error) + throw error + } + } + + // Show payment modal + const showPaymentModal = () => { + return new Promise((resolve, reject) => { + if (!checkoutToken) { + reject(new Error('Payment not initialized. Call initializeHelcimPay first.')) + return + } + + // Load HelcimPay.js modal script + if (!window.appendHelcimPayIframe) { + console.log('HelcimPay script not loaded, loading now...') + const script = document.createElement('script') + script.src = 'https://secure.helcim.app/helcim-pay/services/start.js' + script.async = true + script.onload = () => { + console.log('HelcimPay script loaded successfully!') + console.log('Available functions:', Object.keys(window).filter(key => key.includes('Helcim') || key.includes('helcim'))) + console.log('appendHelcimPayIframe available:', typeof window.appendHelcimPayIframe) + openModal(resolve, reject) + } + script.onerror = () => { + reject(new Error('Failed to load HelcimPay.js')) + } + document.head.appendChild(script) + } else { + console.log('HelcimPay script already loaded, calling openModal') + openModal(resolve, reject) + } + }) + } + + // Open the payment modal + const openModal = (resolve, reject) => { + try { + console.log('Trying to open modal with checkoutToken:', checkoutToken) + + if (typeof window.appendHelcimPayIframe === 'function') { + // Set up event listener for HelcimPay.js responses + const helcimPayJsIdentifierKey = 'helcim-pay-js-' + checkoutToken + + const handleHelcimPayEvent = (event) => { + console.log('Received window message:', event.data) + + if (event.data.eventName === helcimPayJsIdentifierKey) { + console.log('HelcimPay event received:', event.data) + + // Remove event listener to prevent multiple responses + window.removeEventListener('message', handleHelcimPayEvent) + + if (event.data.eventStatus === 'SUCCESS') { + console.log('Payment success:', event.data.eventMessage) + + // Parse the JSON string eventMessage + let paymentData + try { + paymentData = JSON.parse(event.data.eventMessage) + console.log('Parsed payment data:', paymentData) + } catch (parseError) { + console.error('Failed to parse eventMessage:', parseError) + reject(new Error('Invalid payment response format')) + return + } + + // Extract transaction details from nested data structure + const transactionData = paymentData.data?.data || {} + console.log('Transaction data:', transactionData) + + resolve({ + success: true, + transactionId: transactionData.transactionId, + cardToken: transactionData.cardToken, + cardLast4: transactionData.cardNumber ? transactionData.cardNumber.slice(-4) : undefined, + cardType: transactionData.cardType || 'unknown' + }) + } else if (event.data.eventStatus === 'ABORTED') { + console.log('Payment aborted:', event.data.eventMessage) + reject(new Error(event.data.eventMessage || 'Payment failed')) + } else if (event.data.eventStatus === 'HIDE') { + console.log('Modal closed without completion') + reject(new Error('Payment cancelled by user')) + } + } + } + + // Add event listener + window.addEventListener('message', handleHelcimPayEvent) + + // Open the HelcimPay iframe modal + console.log('Calling appendHelcimPayIframe with token:', checkoutToken) + window.appendHelcimPayIframe(checkoutToken, true) + console.log('appendHelcimPayIframe called, waiting for window messages...') + + // Add timeout to clean up if no response + setTimeout(() => { + console.log('60 seconds passed, cleaning up event listener...') + window.removeEventListener('message', handleHelcimPayEvent) + reject(new Error('Payment timeout - no response received')) + }, 60000) + } else { + reject(new Error('appendHelcimPayIframe function not available')) + } + } catch (error) { + console.error('Error opening modal:', error) + reject(error) + } + } + + // Process payment verification + const verifyPayment = async () => { + try { + return await showPaymentModal() + } catch (error) { + throw error + } + } + + // Cleanup tokens + const cleanup = () => { + checkoutToken = null + secretToken = null + } + + return { + initializeHelcimPay, + verifyPayment, + cleanup + } +} \ No newline at end of file diff --git a/app/layouts/admin.vue b/app/layouts/admin.vue index a9194f7..fda4eef 100644 --- a/app/layouts/admin.vue +++ b/app/layouts/admin.vue @@ -57,18 +57,18 @@ - + - Analytics + Series @@ -159,15 +159,15 @@ - Analytics + Series diff --git a/app/layouts/default.vue b/app/layouts/default.vue index befc6f9..07411f7 100644 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -1,7 +1,35 @@ \ No newline at end of file + diff --git a/app/middleware/auth.js b/app/middleware/auth.js new file mode 100644 index 0000000..a29566b --- /dev/null +++ b/app/middleware/auth.js @@ -0,0 +1,27 @@ +export default defineNuxtRouteMiddleware(async (to, from) => { + // Skip on server-side rendering + if (process.server) { + console.log('🛡️ Auth middleware - skipping on server') + return + } + + const { memberData, checkMemberStatus } = useAuth() + + console.log('🛡️ Auth middleware (CLIENT) - route:', to.path) + console.log(' - memberData exists:', !!memberData.value) + console.log(' - Running on:', process.server ? 'SERVER' : 'CLIENT') + + // If no member data, try to check authentication + if (!memberData.value) { + console.log(' - No member data, checking authentication...') + const isAuthenticated = await checkMemberStatus() + console.log(' - Authentication result:', isAuthenticated) + + if (!isAuthenticated) { + console.log(' - ❌ Authentication failed, redirecting to login') + return navigateTo('/login') + } + } + + console.log(' - ✅ Authentication successful for:', memberData.value?.email) +}) \ No newline at end of file diff --git a/app/pages/about.vue b/app/pages/about.vue index 44ddd0e..cbc7469 100644 --- a/app/pages/about.vue +++ b/app/pages/about.vue @@ -1,306 +1,278 @@