feat(api): Add payments with Polar.sh (#264)

## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new subscription API endpoint for managing subscriptions
and products.
- Enhanced subscription management with new entities and
functionalities.
- Added functionality to retrieve current timestamps in both local and
UTC formats.
- Added Polar.sh integration with customer portal and checkout session
creation APIs.

- **Refactor**
- Redesigned team details to now present members and subscription
information instead of a plan type.
  - Enhanced member management by incorporating role assignments.
- Streamlined user data handling and removed legacy subscription event
logic.
  - Simplified error handling in actor functions for better clarity.
  - Updated plan types and UI labels to reflect new subscription tiers.
  - Improved database indexing for Steam user data.

- **Chores**
- Updated the database schema with new tables and fields to support
subscription, team, and member enhancements.
  - Extended identifier prefixes to broaden system integration.
- Added new secrets related to pricing plans in infrastructure
configuration.
  - Configured API and auth routing with new domain and routing rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Wanjohi
2025-04-18 14:24:19 +03:00
committed by GitHub
parent 76d27e4708
commit 47e61599bb
40 changed files with 3304 additions and 425 deletions

View File

@@ -8,8 +8,8 @@ import { useNavigate } from "@solidjs/router";
import { useOpenAuth } from "@openauthjs/solid";
import { utility } from "@nestri/www/ui/utility";
import { useAccount } from "../providers/account";
import { Container, Screen as FullScreen } from "@nestri/www/ui/layout";
import { FormField, Input, Select } from "@nestri/www/ui/form";
import { Container, Screen as FullScreen } from "@nestri/www/ui/layout";
import { createForm, getValue, setError, valiForm } from "@modular-forms/solid";
const nameRegex = /^[a-z0-9\-]+$/
@@ -32,8 +32,9 @@ const Hr = styled("hr", {
})
const Plan = {
Pro: 'BYOG',
Basic: 'Hosted',
Free: 'free',
Pro: 'pro',
Family: 'family',
} as const;
const schema = v.object({
@@ -110,6 +111,13 @@ const UrlTitle = styled("span", {
}
})
/**
* Renders a form for creating a new team with validated fields for team name, slug, and plan type.
*
* Submits the form data to the API to create the team, displays validation errors, and navigates to the new team's page upon success.
*
* @remark If the chosen team slug is already taken, an error message is shown for the slug field.
*/
export function CreateTeamComponent() {
const [form, { Form, Field }] = createForm({
validate: valiForm(schema),
@@ -215,12 +223,14 @@ export function CreateTeamComponent() {
required
value={field.value}
badges={[
{ label: "BYOG", color: "purple" },
{ label: "Hosted", color: "blue" },
{ label: "Free", color: "gray" },
{ label: "Pro", color: "blue" },
{ label: "Family", color: "purple" },
]}
options={[
{ label: "I'll be playing on my machine", value: 'BYOG' },
{ label: "I'll be playing on the cloud", value: 'Hosted' },
{ label: "I'll be playing by myself", value: 'free' },
{ label: "I'll be playing with 3 friends", value: 'pro' },
{ label: "I'll be playing with 5 family members", value: 'family' },
]}
/>
</FormField>

View File

@@ -201,15 +201,12 @@ const Nav = styled("nav", {
})
/**
* Renders the application's header, featuring navigation, branding, and team details.
* Displays the application's fixed top navigation bar with branding, team information, and navigation links.
*
* This component displays a navigation bar that includes the logo, team avatar, team name, a badge
* reflecting the team's plan type, and navigation links. It adjusts its styling based on the scroll
* position by toggling visual effects on the navigation wrapper. A scroll event listener is added
* on mount to update the header's appearance when the user scrolls and is removed on unmount.
* The header includes the app logo, team avatar and name, a badge indicating the team's plan type, and navigation links related to the team. The header's appearance updates dynamically based on the user's scroll position.
*
* @param props.children - Optional child elements rendered below the header component.
* @returns The header component element.
* @param props.children - Optional elements rendered below the header.
* @returns The rendered header component.
*/
export function Header(props: ParentProps) {
// const team = useContext(TeamContext)
@@ -218,7 +215,7 @@ export function Header(props: ParentProps) {
id: "tea_01JPACSPYWTTJ66F32X3AWWFWE",
slug: "wanjohiryan",
name: "Wanjohi",
planType: "BYOG"
planType: "Pro"
})
createEffect(() => {
@@ -231,8 +228,9 @@ export function Header(props: ParentProps) {
});
})
// const account = useAccount()
return (
<PageWrapper>
<NavWrapper scrolled={hasScrolled()}>
@@ -294,14 +292,14 @@ export function Header(props: ParentProps) {
/>
<TeamLabel style={{ color: theme.color.d1000.gray }}>{team!().name}</TeamLabel>
<Switch>
<Match when={team!().planType === "BYOG"}>
<Match when={team!().planType === "Family"}>
<Badge style={{ "background-color": theme.color.purple.d700 }}>
<span style={{ "line-height": 0 }} >BYOG</span>
<span style={{ "line-height": 0 }} >Family</span>
</Badge>
</Match>
<Match when={team!().planType === "Hosted"}>
<Match when={team!().planType === "Pro"}>
<Badge style={{ "background-color": theme.color.blue.d700 }}>
<span style={{ "line-height": 0 }}>Hosted</span>
<span style={{ "line-height": 0 }}>Pro</span>
</Badge>
</Match>
</Switch>

View File

@@ -44,7 +44,7 @@ export const TeamRoute = (
return (
<Switch>
<Match when={!team()}>
TODO: Add a public page for (other) teams
{/* TODO: Add a public page for (other) teams */}
<NotAllowed header />
</Match>
<Match when={team()}>

View File

@@ -157,6 +157,7 @@ export const InputRadio = styled("input", {
const Label = styled("p", {
base: {
fontWeight: 500,
textAlign: "left",
letterSpacing: -0.1,
fontSize: theme.font.size.mono_sm,
textTransform: "capitalize",
@@ -213,6 +214,7 @@ const Hint = styled("p", {
fontSize: theme.font.size.xs,
lineHeight: theme.font.lineHeight,
color: theme.color.gray.d800,
textAlign: "left"
},
variants: {
color: {