Skip to content

Commit 29fdac8

Browse files
authored
Merge pull request #206 from develop
chore: sync develop → main
2 parents ecaf7ce + 9e35997 commit 29fdac8

33 files changed

Lines changed: 890 additions & 99 deletions

.github/workflows/deploy-cloudflare.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
runs-on: ubuntu-latest
1717
if: >-
1818
github.event.workflow_run.conclusion == 'success' &&
19+
github.event.workflow_run.head_repository.full_name == github.repository &&
1920
(github.event.workflow_run.head_branch == 'main' ||
2021
github.event.workflow_run.head_branch == 'develop')
2122
permissions:

.github/workflows/deploy-supabase-functions.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ jobs:
5151
supabase functions deploy platform-access
5252
supabase functions deploy delete-account
5353
supabase functions deploy waitlist-welcome --no-verify-jwt
54+
supabase functions deploy sync-contacts

.github/workflows/ensure-pr-source-develop.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,15 @@ jobs:
2222

2323
- name: Read inputs
2424
id: check
25+
env:
26+
ALLOWED: ${{ secrets.ALLOWED_MERGERS }}
27+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
28+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
29+
TARGET_BRANCH: ${{ github.event.pull_request.base.ref }}
2530
run: |
2631
# Allowed mergers come from a repo secret, comma-separated.
2732
# Set this secret in your repo settings: Settings -> Secrets -> Actions -> New repository secret
2833
# Example value: ayoub,my-org-release-bot
29-
ALLOWED="${{ secrets.ALLOWED_MERGERS }}"
30-
31-
# PR metadata from github context
32-
HEAD_REF="${{ github.event.pull_request.head.ref }}"
33-
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
34-
TARGET_BRANCH="${{ github.event.pull_request.base.ref }}"
3534
3635
echo "HEAD_REF=$HEAD_REF"
3736
echo "PR_AUTHOR=$PR_AUTHOR"
@@ -68,10 +67,11 @@ jobs:
6867
6968
- name: Fail if not allowed
7069
if: steps.check.outputs.allowed != 'true'
70+
env:
71+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
72+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
7173
run: |
72-
head_ref="${{ github.event.pull_request.head.ref }}"
73-
author="${{ github.event.pull_request.user.login }}"
74-
echo "ERROR: PR targeting 'main' is not allowed. Head branch is '$head_ref' and PR author is '$author'."
74+
echo "ERROR: PR targeting 'main' is not allowed. Head branch is '$HEAD_REF' and PR author is '$PR_AUTHOR'."
7575
echo "Only PRs whose head branch is 'develop' or PRs authored by an allowed merger (repo secret ALLOWED_MERGERS) may target 'main'."
7676
exit 1
7777

.github/workflows/keep-supabase-alive.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ on:
55
- cron: "0 0 */3 * *"
66
workflow_dispatch:
77

8+
permissions:
9+
contents: read
10+
811
jobs:
912
ping:
1013
runs-on: ubuntu-latest
1114
steps:
1215
- name: Ping Supabase REST API
16+
env:
17+
SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
18+
SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
1319
run: |
14-
curl -sf "${{ secrets.VITE_SUPABASE_URL }}/rest/v1/keepalive?select=id&limit=1" \
15-
-H "apikey: ${{ secrets.VITE_SUPABASE_ANON_KEY }}" \
16-
-H "Authorization: Bearer ${{ secrets.VITE_SUPABASE_ANON_KEY }}"
20+
curl -sf "${SUPABASE_URL}/rest/v1/keepalive?select=id&limit=1" \
21+
-H "apikey: ${SUPABASE_ANON_KEY}" \
22+
-H "Authorization: Bearer ${SUPABASE_ANON_KEY}"

AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ See reference doc for full checklists (JS, Python, pseudocode, sound, insight, t
103103
- **OAuth UX** — Google Identity Services (PKCE popup on `/auth/google/callback`); web uses `signInWithIdToken`, not `signInWithOAuth`; `googleIdentity.js` manages GIS script loading, nonce creation, and popup flow
104104
- **Service layer**`src/services/authService.js`, `profileService.js`, `entitlementService.js`, `accessService.js`, `googleTokenExchange.js`; components use `AuthContext` / `useAuth`, never import Supabase directly
105105
- **Platform access** — signed-in users pass through `checkPlatformAccess()` → Supabase Edge Function `platform-access` (account ban gate); **fail-open** on transport/invoke errors, **fail-closed** only when `allowed: false` + `reason: account_banned`; signup path uses `before-signup` / `post-signup` hooks (ban logic fail-closed)
106-
- **Postgres-portable schema**`profiles` keyed to `auth.users`; RLS on public tables; client-writable columns: `display_name`, `avatar_preference` only; `avatar_url` is OAuth/trigger-populated (not client-writable); `plan` and future `referral_*` / `pro_*` columns are service role / webhook only
106+
- **Postgres-portable schema**`profiles` keyed to `auth.users`; RLS on public tables; client-writable columns: `display_name`, `avatar_preference` only; `avatar_url` is OAuth/trigger-populated (not client-writable); `plan` is service role only. Through 0.5.0: **no** LemonSqueezy, subscriptions, referrals, or `user_sessions` tables in tree
107+
- **Pro through 0.5.0** — waitlist demand only (`/pro` + `waitlist`); no checkout or Pro entitlements beyond waitlist capture
108+
- **Analytics (PostHog)**`src/services/analytics.js` + `analyticsEvents.js`; SPA pageviews via `capture_pageview: 'history_change'`; `disable_surveys: true`; session replay sampled (`sampleRate: 0.2`). Growth events: `waitlist_joined`, `upgrade_limit_hit` (plus existing `sign_in_completed`). Anonymous viz limit stays hardcoded at `12` (no feature-flag A/B)
109+
- **Email (Resend)**`waitlist-welcome` (one styled transactional email per waitlist join); `sync-contacts` on `SIGNED_IN` upserts the contact **and** sends a one-time Free-account welcome (`profiles.welcome_email_sent_at`). No broadcasts, digests, referral, or Pro-nudge sends in 0.5.0. Stay under Resend free daily cap (100/day)
107110
- **Profile settings** — private route `/settings/profile` (`RequireAuth`); `updateProfile()` in `profileService.js`; security boundary = RLS row scope + `REVOKE UPDATE` + `GRANT UPDATE (display_name, avatar_preference)`; tabbed UI with profile/notifications/connections tabs; DiceBear notionists avatar fallback
108111
- **Session**`getSession()`, `onAuthStateChange()`; `AuthProvider` in `src/main.jsx`; request-dedup via `requestRef`
109112
- **Tiered access model** — Anonymous (no account) gets limited access to drive sign-in conversion; Free account (Google sign-in) unlocks the full platform

docs/AGENTS_REFERENCE.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@
9999
- `supabase/functions/post-signup/` — post-signup side effects
100100
- `supabase/functions/platform-access/` — signed-in ban check (`accessService.checkPlatformAccess()`; fail-open on transport)
101101
- `supabase/functions/waitlist-welcome/` — Pro waitlist confirmation email via Resend (fail-open; invoked after client insert)
102+
- `supabase/functions/sync-contacts/` — JWT-authenticated Resend contact upsert + one-time Free welcome email on sign-in (fail-open; identity from JWT only; atomic claim on `profiles.welcome_email_sent_at`; Resend segment via `RESEND_SEGMENT_ID` or legacy `RESEND_AUDIENCE_ID`)
102103
- `supabase/functions/delete-account/` — self-service account deletion
104+
- Shared email HTML: `supabase/functions/_shared/transactionalEmails.ts`
105+
- **Not in 0.5.0:** LemonSqueezy webhook, subscriptions / usage_events / referrals / user_sessions tables (dropped via `20260720180000_drop_premature_saas_scaffolding.sql`); no viz-limit email, weekly digest, Pro nudge, or referral invite sends (Pro nudge + referral templates may exist in Resend as parked drafts for later)
106+
- Analytics: `src/services/analytics.js`, `src/services/analyticsEvents.js` — PostHog SPA pageviews + growth events `waitlist_joined`, `upgrade_limit_hit`; surveys disabled in SDK
103107
- Context: `src/contexts/AuthProvider.jsx`, `src/hooks/useAuth.js`
104108
- Avatar resolution: `src/utils/resolveUserAvatar.js` (`resolveUserAvatar`, `resolveDisplayName`, DiceBear notionists style)
105109
- Components: `src/components/UserMenu.jsx`, `src/components/UserAvatar.jsx`, `src/components/RequireAuth.jsx`
@@ -117,8 +121,9 @@
117121
| `display_name` | yes | Editable on profile settings page |
118122
| `avatar_url` | no | OAuth / trigger-populated HTTPS URL |
119123
| `avatar_preference` | yes | `google` (default) \| `generated` |
124+
| `welcome_email_sent_at` | no | Set atomically by `sync-contacts` when claiming the one-time welcome send |
120125

121-
Future (v0.6.0, not shipped): `username` (unique, set-once RLS), public `/u/:username` route; referral/billing columns (`referral_code`, `referred_by`, `referral_count`, `pro_months_earned`, `pro_expires_at`) — service role only.
126+
Future (post-0.5.0, not shipped): `username` (unique, set-once RLS), public `/u/:username` route; any referral/billing columns — new migrations then, not restore of dropped scaffolding. Through 0.5.0 Pro remains waitlist-only.
122127

123128
### Personal learning tables
124129

index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,13 @@
211211
}
212212
})();
213213
</script>
214+
215+
<!-- Cloudflare Turnstile — invisible CAPTCHA for bot signup prevention -->
216+
<script
217+
src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
218+
async
219+
defer
220+
></script>
214221
</head>
215222
<body>
216223
<div id="root"></div>

src/contexts/AuthProvider.jsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,24 @@ export function AuthProvider({ children }) {
186186
if (event === 'SIGNED_IN' && nextSession?.user != null) {
187187
resetAllSessionCounters();
188188
resetUser();
189+
190+
const fallbackDisplayName =
191+
nextSession.user.user_metadata?.full_name ||
192+
nextSession.user.user_metadata?.name;
193+
189194
identifyUser(nextSession.user, {
190195
email: nextSession.user.email,
191-
displayName:
192-
nextSession.user.user_metadata?.full_name ||
193-
nextSession.user.user_metadata?.name,
196+
displayName: fallbackDisplayName,
194197
plan: null,
195198
});
196199
trackSignInCompleted();
200+
setTimeout(() => {
201+
authService
202+
.syncContactToResend({
203+
displayName: fallbackDisplayName,
204+
})
205+
.catch(() => {});
206+
}, 0);
197207
}
198208

199209
if (event === 'SIGNED_OUT') {

src/contexts/AuthProvider.test.jsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
resetSupabaseMocks,
88
supabaseAuthMock,
99
supabaseFromMock,
10+
supabaseFunctionsInvokeMock,
1011
authStateChangeCallbackRef,
1112
} from '../test/supabaseMock.js';
1213

@@ -339,6 +340,17 @@ describe('AuthProvider', () => {
339340
});
340341
expect(screen.getByTestId('display-name')).toHaveTextContent('New User');
341342
expect(resetAllSessionCounters).toHaveBeenCalledTimes(1);
343+
await waitFor(() => {
344+
expect(supabaseFunctionsInvokeMock).toHaveBeenCalledWith(
345+
'sync-contacts',
346+
expect.objectContaining({
347+
method: 'POST',
348+
body: expect.objectContaining({
349+
displayName: 'New User',
350+
}),
351+
})
352+
);
353+
});
342354
});
343355

344356
it('does not reset session counters on INITIAL_SESSION hydrate', async () => {

src/i18n/locales/ar/translation.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@
5252
"dangerZoneTitle": "منطقة الخطر",
5353
"dangerZoneDescription": "يحذف حسابك نهائياً مع جميع المفضلات وملاحظات الدراسة. لا يمكن التراجع عن هذا الإجراء.",
5454
"deleteAccount": "حذف الحساب",
55-
"deleteAccountConfirmLabel": "اكتب DELETE للتأكيد",
56-
"deleteAccountConfirmPlaceholder": "DELETE",
57-
"deleteAccountConfirmWord": "DELETE",
55+
"deleteAccountConfirmLabel": "اكتب بريدك الإلكتروني للتأكيد",
5856
"deleteAccountInProgress": "جارٍ حذف الحساب…",
5957
"deleteAccountSuccess": "تم حذف حسابك.",
6058
"deleteAccountError": "تعذّر حذف حسابك. حاول مرة أخرى أو راسل contact@bayanflow.com."

0 commit comments

Comments
 (0)