Skip to content

Commit

Permalink
Implement prime settings (commaai#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
incognitojam authored Aug 7, 2024
1 parent 8f48d97 commit a11309a
Show file tree
Hide file tree
Showing 16 changed files with 578 additions and 15 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:

- name: Checkout ci-artifacts
uses: actions/checkout@v4
with:
with:
repository: commaai/ci-artifacts
ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }}
path: ${{ github.workspace }}/ci-artifacts
Expand Down Expand Up @@ -116,7 +116,10 @@ jobs:
<tr>
<td><img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/Login-mobile.playwright.png"></td>
<td><img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/RouteActivity-mobile.playwright.png"></td>
<tr/>
<tr>
<td><img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/RouteList-mobile.playwright.png"></td>
<td><img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/SettingsActivity-mobile.playwright.png"></td>
</tr>
</table>
Expand All @@ -125,13 +128,16 @@ jobs:
<tr>
<img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/Login-desktop.playwright.png">
<img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/RouteActivity-desktop.playwright.png">
</tr>
<tr>
<img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/RouteList-desktop.playwright.png">
<img src="https://raw.githubusercontent.com/commaai/ci-artifacts/new-connect/pr-${{ steps.pr.outputs.result }}/SettingsActivity-desktop.playwright.png">
</tr>
</table>
comment_tag: run_id
pr_number: ${{ steps.pr.outputs.result }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

update_pr_check:
needs: preview
if: always() && github.event.workflow_run.event == 'pull_request'
Expand All @@ -153,4 +159,4 @@ jobs:
},
owner: 'commaai',
repo: '${{ github.event.repository.name }}',
})
})
1 change: 1 addition & 0 deletions src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const ATHENA_URL = 'https://athena.comma.ai'
export const BASE_URL = 'https://api.comma.ai'
export const BILLING_URL = 'https://billing.comma.ai'
4 changes: 2 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const populateFetchedAt = <T>(item: T): T => {
}
}

export async function fetcher<T>(endpoint: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${endpoint}`, {
export async function fetcher<T>(endpoint: string, init?: RequestInit, apiUrl: string = BASE_URL): Promise<T> {
const res = await fetch(`${apiUrl}${endpoint}`, {
...init,
headers: {
...init?.headers,
Expand Down
96 changes: 96 additions & 0 deletions src/api/prime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { BILLING_URL } from './config'

import { fetcher } from '.'

export interface SubscriptionStatus {
amount: number
cancel_at: number | null
is_prime_sim: boolean
next_charge_at: number
plan: string
requires_migration: boolean
sim_id: string | null
subscribed_at: number
// trial_claim_end: number | null
// trial_claimable: boolean
trial_end: number
user_id: string
}

export const getSubscriptionStatus = async (dongleId: string) => {
const params = new URLSearchParams()
params.append('dongle_id', dongleId)
return fetcher<SubscriptionStatus>(`/v1/prime/subscription?${params.toString()}`, undefined, BILLING_URL)
}

export interface SubscribeInfo {
data_connected: boolean | null
device_online: boolean
has_prime: boolean
is_prime_sim: boolean
sim_id: string | null
sim_type: string | null
sim_usable: boolean | null
trial_end_data: number | null,
trial_end_nodata: number | null,
}

export const getSubscribeInfo = async (dongleId: string) => {
const params = new URLSearchParams()
params.append('dongle_id', dongleId)
return fetcher<SubscribeInfo>(`/v1/prime/subscribe_info?${params.toString()}`, undefined, BILLING_URL)
}

interface ActivateSubscriptionRequest {
dongle_id: string
sim_id: string | null
stripe_token: string
}

const getBilling = <T>(endpoint: string, init?: RequestInit): Promise<T> =>
fetcher<T>(endpoint, init, BILLING_URL)

const postBilling = <T>(endpoint: string, body: unknown, init?: RequestInit): Promise<T> => {
return fetcher(endpoint, {
...init,
method: 'POST',
headers: {
...init?.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}, BILLING_URL)
}

export const activateSubscription = async (body: ActivateSubscriptionRequest) =>
postBilling<{ success: 1 }>('/v1/prime/pay', body)

export const cancelSubscription = async (dongleId: string) =>
postBilling<{ success: 1 }>('/v1/prime/cancel', { dongle_id: dongleId })

interface PaymentSource {
brand: string
country: string
exp_month: number
exp_year: number
last4: string
tokenization_method: string | null
}

export const getPaymentSource = async () => getBilling<PaymentSource>('/v1/prime/payment_source')

export const setPaymentSource = async (stripeToken: string) =>
postBilling<PaymentSource>('/v1/prime/payment_source', { stripe_token: stripeToken })

export const getStripeCheckout = async (dongleId: string, simId: string, plan: string) =>
postBilling<{ url: string }>('/v1/prime/stripe_checkout', {
dongle_id: dongleId,
sim_id: simId,
plan,
})

export const getStripePortal = async (dongleId: string) =>
getBilling<{ url: string }>(`/v1/prime/stripe_portal?dongle_id=${dongleId}`)

export const getStripeSession = async (dongleId: string, sessionId: string) =>
getBilling<{ payment_status: 'no_payment_required' | 'paid' | 'unpaid' }>(`/v1/prime/stripe_session?dongle_id=${dongleId}&session_id=${sessionId}`)
1 change: 1 addition & 0 deletions src/ci/screenshots.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const endpoints = {
Login: 'login',
RouteList: '1d3dc3e03047b0c7',
RouteActivity: '1d3dc3e03047b0c7/000000dd--455f14369d',
SettingsActivity: '1d3dc3e03047b0c7/000000dd--455f14369d/settings',
};

async function takeScreenshots(deviceType, context) {
Expand Down
15 changes: 12 additions & 3 deletions src/components/material/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { JSXElement, ParentComponent } from 'solid-js'
import { splitProps } from 'solid-js'
import { Show, splitProps } from 'solid-js'
import clsx from 'clsx'

import ButtonBase, { ButtonBaseProps } from './ButtonBase'
import CircularProgress from './CircularProgress'

type ButtonProps = ButtonBaseProps & {
color?: 'primary' | 'secondary' | 'tertiary' | 'error'
disabled?: boolean
loading?: boolean
leading?: JSXElement
trailing?: JSXElement
}
Expand All @@ -26,22 +28,29 @@ const Button: ParentComponent<ButtonProps> = (props) => {
'trailing',
'class',
'children',
'disabled',
'loading',
])
const disabled = () => props.disabled || props.loading

return (
<ButtonBase
class={clsx(
'state-layer hover:elevation-1 inline-flex h-10 items-center justify-center gap-2 rounded-full py-1 contrast-100 transition',
colorClasses(),
props.disabled && 'cursor-not-allowed opacity-50',
disabled() && 'cursor-not-allowed opacity-50',
props.leading ? 'pl-4' : 'pl-6',
props.trailing ? 'pr-4' : 'pr-6',
props.class,
)}
{...rest}
disabled={disabled()}
>
{props.leading}
<span class="text-label-lg">{props.children}</span>
<span class={clsx('text-label-lg', props.loading && 'invisible')}>{props.children}</span>
<Show when={props.loading}>
<CircularProgress class="absolute left-1/2 top-1/2 ml-[-10px] mt-[-10px]" color="inherit" size={20} />
</Show>
{props.trailing}
</ButtonBase>
)
Expand Down
2 changes: 1 addition & 1 deletion src/components/material/ButtonBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Component, JSX } from 'solid-js'
import { A } from '@solidjs/router'
import clsx from 'clsx'

export type ButtonBaseProps = JSX.HTMLAttributes<HTMLButtonElement> & {
export type ButtonBaseProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & {
class?: string
onClick?: (e: MouseEvent) => void
href?: string
Expand Down
3 changes: 2 additions & 1 deletion src/components/material/CircularProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import clsx from 'clsx'
type CircularProgressProps = {
class?: string
progress?: number
color?: 'primary' | 'secondary' | 'tertiary' | 'error'
color?: 'primary' | 'secondary' | 'tertiary' | 'error' | 'inherit'
size?: number
thickness?: number
}
Expand All @@ -18,6 +18,7 @@ const CircularProgress: VoidComponent<CircularProgressProps> = (props) => {
secondary: 'text-secondary',
tertiary: 'text-tertiary',
error: 'text-error',
inherit: '',
}[props.color || 'primary'])

const size = () => `${props.size || 40}px`
Expand Down
2 changes: 1 addition & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@

@layer components {
.state-layer {
@apply relative before:absolute before:inset-0 before:transition before:opacity-0 before:overflow-hidden sm:before:hover:opacity-[.08] sm:before:focus:opacity-[.12] before:active:opacity-[.12];
@apply relative before:absolute before:inset-0 before:transition before:opacity-0 before:overflow-hidden sm:before:hover:opacity-[.08] sm:before:focus:opacity-[.12] before:active:opacity-[.12] disabled:before:opacity-0 disabled:before:hover:opacity-0;
}

.skeleton-loader {
Expand Down
4 changes: 4 additions & 0 deletions src/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import TopAppBar from '~/components/material/TopAppBar'
import DeviceList from './components/DeviceList'
import DeviceActivity from './activities/DeviceActivity'
import RouteActivity from './activities/RouteActivity'
import SettingsActivity from './activities/SettingsActivity'
import storage from '~/utils/storage'

const PairActivity = lazy(() => import('./activities/PairActivity'))
Expand Down Expand Up @@ -114,6 +115,9 @@ const DashboardLayout: Component<RouteSectionProps> = () => {
<Match when={dongleId() === 'pair' || pairToken()}>
<PairActivity />
</Match>
<Match when={dateStr() === 'settings' || dateStr() === 'prime'}>
<SettingsActivity dongleId={dongleId()} />
</Match>
<Match when={dateStr()} keyed>
<RouteActivity dongleId={dongleId()} dateStr={dateStr()} />
</Match>
Expand Down
9 changes: 6 additions & 3 deletions src/pages/dashboard/activities/DeviceActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {

return (
<>
<TopAppBar leading={<IconButton onClick={toggleDrawer}>menu</IconButton>}>
<TopAppBar
leading={<IconButton onClick={toggleDrawer}>menu</IconButton>}
trailing={<IconButton href={`/${props.dongleId}/settings`}>settings</IconButton>}
>
{deviceName()}
</TopAppBar>
<div class="flex flex-col gap-4 px-4 pb-4">
Expand All @@ -117,7 +120,7 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
</Suspense>
</div>
<div class="flex p-4">
<IconButton onClick={() => void takeSnapshot() }>camera</IconButton>
<IconButton onClick={() => void takeSnapshot()}>camera</IconButton>
</div>
</div>
</div>
Expand All @@ -126,7 +129,7 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
{(image, index) => (
<div class="flex-1 overflow-hidden rounded-lg bg-surface-container-low">
<div class="relative p-4">
<img src={`data:image/jpeg;base64,${image}`} alt={`Device Snapshot ${index() + 1}`}/>
<img src={`data:image/jpeg;base64,${image}`} alt={`Device Snapshot ${index() + 1}`} />
<div class="absolute right-4 top-4 p-4">
<IconButton onClick={() => downloadSnapshot(image, index())} class="text-white">download</IconButton>
<IconButton onClick={() => clearImage(index())} class="text-white">clear</IconButton>
Expand Down
Loading

0 comments on commit a11309a

Please sign in to comment.