diff --git a/apps/console/app/components/IconPicker/index.tsx b/apps/console/app/components/IconPicker/index.tsx
index f5e36fc7f8..d69a319bb7 100644
--- a/apps/console/app/components/IconPicker/index.tsx
+++ b/apps/console/app/components/IconPicker/index.tsx
@@ -195,6 +195,7 @@ export default function IconPicker({
Upload
+ {iconURL && }
{
)}
+ {errors?.upsertAppContactAddress && (
+
+ {errors.upsertAppContactAddress}
+
+ )}
+
This will be used for notifications about your application
diff --git a/apps/console/app/routes/apps/delete.tsx b/apps/console/app/routes/apps/delete.tsx
index d033083b46..afd904b276 100644
--- a/apps/console/app/routes/apps/delete.tsx
+++ b/apps/console/app/routes/apps/delete.tsx
@@ -9,7 +9,11 @@ import {
getErrorCause,
getRollupReqFunctionErrorWrapper,
} from '@proofzero/utils/errors'
-import { BadRequestError, InternalServerError } from '@proofzero/errors'
+import {
+ BadRequestError,
+ InternalServerError,
+ UnauthorizedError,
+} from '@proofzero/errors'
export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
async ({ request, context }) => {
@@ -33,6 +37,8 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
const traceparent = context.traceSpan.getTraceParent()
if (cause instanceof BadRequestError) {
throw cause
+ } else if (cause instanceof UnauthorizedError) {
+ throw error
} else {
console.error(error)
throw JsonError(
diff --git a/apps/console/app/routes/onboarding.tsx b/apps/console/app/routes/onboarding.tsx
index 54b1906a83..b4312e260d 100644
--- a/apps/console/app/routes/onboarding.tsx
+++ b/apps/console/app/routes/onboarding.tsx
@@ -63,7 +63,7 @@ export const loader: LoaderFunction = getRollupReqFunctionErrorWrapper(
: undefined
return json({
- url: request.url,
+ currentPageURL: request.url,
profile,
connectedEmails,
PASSPORT_URL: context.env.PASSPORT_URL,
@@ -87,12 +87,12 @@ export const shouldRevalidate = ({
}
export default function Onboarding() {
- const { connectedEmails, PASSPORT_URL, profile, url, targetIG } =
+ const { connectedEmails, PASSPORT_URL, profile, currentPageURL, targetIG } =
useLoaderData<{
connectedEmails: DropdownSelectListItem[]
PASSPORT_URL: string
profile: Profile
- url: string
+ currentPageURL: string
targetIG:
| {
name: string
@@ -102,7 +102,9 @@ export default function Onboarding() {
}>()
const currentPage =
- new URL(url).searchParams.get('rollup_result') || targetIG ? 1 : 0
+ new URL(currentPageURL).searchParams.get('rollup_result') || targetIG
+ ? 1
+ : 0
useConnectResult()
@@ -119,7 +121,13 @@ export default function Onboarding() {
}
>
diff --git a/apps/console/app/routes/onboarding/index.tsx b/apps/console/app/routes/onboarding/index.tsx
index 6861b6c82b..2e5839d078 100644
--- a/apps/console/app/routes/onboarding/index.tsx
+++ b/apps/console/app/routes/onboarding/index.tsx
@@ -10,7 +10,11 @@ import {
} from '@proofzero/design-system/src/atoms/dropdown/DropdownSelectList'
import { useFetcher, useNavigate, useOutletContext } from '@remix-run/react'
import { getEmailIcon } from '@proofzero/utils/getNormalisedConnectedAccounts'
-import { redirectToPassport } from '~/utils'
+import {
+ OnboardTypeValues,
+ RedirectQueryParamKeys,
+ redirectToPassport,
+} from '~/utils'
import { HiOutlineArrowLeft, HiOutlineMail } from 'react-icons/hi'
import { Input } from '@proofzero/design-system/src/atoms/form/Input'
import { DocumentationBadge } from '~/components/DocumentationBadge'
@@ -125,8 +129,8 @@ const Option = ({
description: string
selected?: boolean
disabled?: boolean
- setSelectedType: (value: 'solo' | 'team') => void
- type: 'solo' | 'team'
+ setSelectedType: (value: OnboardTypeValues) => void
+ type: OnboardTypeValues
}) => {
return (
void
page: number
- setOrgType: (value: 'solo' | 'team') => void
- orgType: 'solo' | 'team'
+ setOrgType: (value: OnboardTypeValues) => void
+ orgType: OnboardTypeValues
}) => {
return (
setOrgType('solo')}
- type="solo"
+ selected={orgType === OnboardTypeValues.Solo}
+ setSelectedType={() => setOrgType(OnboardTypeValues.Solo)}
+ type={OnboardTypeValues.Solo}
/>
diff --git a/apps/console/app/services/billing/stripe.ts b/apps/console/app/services/billing/stripe.ts
index f18d70eb58..79e76d41b8 100644
--- a/apps/console/app/services/billing/stripe.ts
+++ b/apps/console/app/services/billing/stripe.ts
@@ -1,5 +1,6 @@
import { InternalServerError } from '@proofzero/errors'
import { type CoreClientType } from '@proofzero/platform-clients/core'
+import { IDENTITY_GROUP_OPTIONS } from '@proofzero/platform/identity/src/constants'
import { type ReconcileAppsSubscriptionsOutput } from '@proofzero/platform/starbase/src/jsonrpc/methods/reconcileAppSubscriptions'
import { ServicePlanType } from '@proofzero/types/billing'
import {
@@ -354,25 +355,26 @@ export const reconcileSubscriptions = async (
if (seatQuantities) {
const { quantity: stripeSeatQuantity } = seatQuantities
- const groupSeats = await coreClient.billing.getIdentityGroupSeats.query(
- {
+ const usedSeats =
+ await coreClient.billing.getUsedIdentityGroupSeats.query({
URN: URN as IdentityGroupURN,
- }
- )
+ })
// If the group has more seats than the subscription, set payment failed
// because this flag is responsible for displaying the "Payment failed"
// in the UI
if (
!paidInvoice ||
- (groupSeats && groupSeats.quantity > stripeSeatQuantity!)
+ usedSeats >
+ stripeSeatQuantity! + IDENTITY_GROUP_OPTIONS.maxFreeMembers
) {
await coreClient.billing.setPaymentFailed.mutate({
URN: URN as IdentityGroupURN,
})
} else if (
paidInvoice &&
- (!groupSeats || groupSeats.quantity <= stripeSeatQuantity!)
+ usedSeats <=
+ stripeSeatQuantity! + IDENTITY_GROUP_OPTIONS.maxFreeMembers
) {
await coreClient.billing.setPaymentFailed.mutate({
URN: URN as IdentityGroupURN,
diff --git a/apps/console/app/types.ts b/apps/console/app/types.ts
index 95924052a5..eca67c3c7e 100644
--- a/apps/console/app/types.ts
+++ b/apps/console/app/types.ts
@@ -40,7 +40,7 @@ export type errorsAuthProps = {
}
export type errorsTeamProps = {
- upserteAppContactAddress?: string
+ upsertAppContactAddress?: string
}
export type AuthorizedProfile = AuthorizedUser
diff --git a/apps/console/app/utils.ts b/apps/console/app/utils.ts
index 23866e1ae5..9f5c91b4ba 100644
--- a/apps/console/app/utils.ts
+++ b/apps/console/app/utils.ts
@@ -107,29 +107,66 @@ export const setPurchaseToastNotification = ({
}
}
+export enum RedirectQueryParamKeys {
+ OnboardType = 'onboard_type',
+}
+
+export enum OnboardTypeValues {
+ Team = 'team',
+ Solo = 'solo',
+}
+
+type RedirectQueryParams = {
+ onboard_type?: OnboardTypeValues
+}
+
+const validExtraParams: Record
= {
+ [RedirectQueryParamKeys.OnboardType]: [
+ OnboardTypeValues.Team,
+ OnboardTypeValues.Solo,
+ ],
+}
+
export const redirectToPassport = ({
PASSPORT_URL,
login_hint,
scope = '',
state = 'skip',
rollup_action,
+ redirectQueryParams,
}: {
PASSPORT_URL: string
login_hint: string
scope?: string
state?: string
rollup_action?: string
+ redirectQueryParams?: RedirectQueryParams
}) => {
const currentURL = new URL(window.location.href)
currentURL.search = ''
+ if (redirectQueryParams) {
+ for (const [key, value] of Object.entries(redirectQueryParams)) {
+ const enumKey = key as RedirectQueryParamKeys
+ if (
+ enumKey in validExtraParams &&
+ validExtraParams[enumKey].includes(value)
+ ) {
+ currentURL.searchParams.append(key, value)
+ }
+ }
+ }
+
const qp = new URLSearchParams()
qp.append('scope', scope)
qp.append('state', state)
qp.append('client_id', 'console')
-
qp.append('redirect_uri', currentURL.toString())
- if (rollup_action) qp.append('rollup_action', rollup_action)
+
+ if (rollup_action) {
+ qp.append('rollup_action', rollup_action)
+ }
+
qp.append('login_hint', login_hint)
window.location.href = `${PASSPORT_URL}/authorize?${qp.toString()}`
diff --git a/apps/passport/app/routes/authenticate/$clientId/index.tsx b/apps/passport/app/routes/authenticate/$clientId/index.tsx
index 74c6348cd0..43abcc70ab 100644
--- a/apps/passport/app/routes/authenticate/$clientId/index.tsx
+++ b/apps/passport/app/routes/authenticate/$clientId/index.tsx
@@ -231,7 +231,7 @@ const InnerComponent = ({
size="sm"
>
- {!rollup_action?.startsWith('groupconnect') && (
+ {!rollup_action?.startsWith('group') && (
-
-
+
+
"{invitationData.inviterAlias}"
has invited you to join group
-
+
"{invitationData.groupName}"
-
+
To accept please authenticate with your
=> {
const { identityGroupURN, accountURN } = input
- const caller = router.createCaller(ctx)
+ await groupAdminValidatorByIdentityGroupURN(ctx, identityGroupURN)
- const { edges: membershipEdges } = await caller.edges.getEdges({
- query: {
- src: {
- baseUrn: ctx.identityURN,
- },
- tag: EDGE_MEMBER_OF_IDENTITY_GROUP,
- dst: {
- baseUrn: identityGroupURN,
- },
- },
- })
- if (membershipEdges.length === 0) {
- throw new UnauthorizedError({
- message: 'Caller is not a member of the identity group',
- })
- }
+ const caller = router.createCaller(ctx)
const { edges } = await caller.edges.getEdges({
query: {
diff --git a/platform/account/src/jsonrpc/router.ts b/platform/account/src/jsonrpc/router.ts
index ac9b0ea2a8..1e45cee027 100644
--- a/platform/account/src/jsonrpc/router.ts
+++ b/platform/account/src/jsonrpc/router.ts
@@ -128,6 +128,10 @@ import {
ConnectIdentityGroupEmailOutputSchema,
connectIdentityGroupEmail,
} from './methods/identity-groups/connectIdentityGroupEmail'
+import {
+ AuthorizationTokenFromHeader,
+ ValidateJWT,
+} from '@proofzero/platform-middleware/jwt'
const t = initTRPC.context().create({ errorFormatter })
@@ -350,6 +354,8 @@ export const appRouter = t.router({
connectIdentityGroupEmail: t.procedure
.use(LogUsage)
.use(Analytics)
+ .use(AuthorizationTokenFromHeader)
+ .use(ValidateJWT)
.use(parse3RN)
.use(setAccountNodeClient)
.use(initAccountNode)
diff --git a/platform/billing/src/jsonrpc/methods/getUsedIdentityGroupSeats.ts b/platform/billing/src/jsonrpc/methods/getUsedIdentityGroupSeats.ts
new file mode 100644
index 0000000000..7deac2c9d4
--- /dev/null
+++ b/platform/billing/src/jsonrpc/methods/getUsedIdentityGroupSeats.ts
@@ -0,0 +1,34 @@
+import { z } from 'zod'
+import { Context } from '../../context'
+import { IdentityGroupURNValidator } from '@proofzero/platform-middleware/inputValidators'
+import { initIdentityGroupNodeByName } from '@proofzero/platform.identity/src/nodes'
+
+export const GetUsedIdentityGroupSeatsInputSchema = z.object({
+ URN: IdentityGroupURNValidator,
+})
+export type GetUsedIdentityGroupSeatsInput = z.infer<
+ typeof GetUsedIdentityGroupSeatsInputSchema
+>
+
+export const GetUsedIdentityGroupSeatsOutputSchema = z.number()
+export type GetUsedIdentityGroupSeatsOutput = z.infer<
+ typeof GetUsedIdentityGroupSeatsOutputSchema
+>
+
+export const getUsedIdentityGroupSeats = async ({
+ input,
+ ctx,
+}: {
+ input: GetUsedIdentityGroupSeatsInput
+ ctx: Context
+}): Promise => {
+ const ownerNode = initIdentityGroupNodeByName(
+ input.URN,
+ ctx.env.IdentityGroup
+ )
+
+ const orderedMembers = await ownerNode.class.getOrderedMembers()
+ const invitations = await ownerNode.class.getInvitations()
+
+ return orderedMembers.length + invitations.length
+}
diff --git a/platform/billing/src/jsonrpc/router.ts b/platform/billing/src/jsonrpc/router.ts
index 17b6fa03da..c99e2ceeb7 100644
--- a/platform/billing/src/jsonrpc/router.ts
+++ b/platform/billing/src/jsonrpc/router.ts
@@ -38,6 +38,11 @@ import {
SetPaymentFailedInput,
setPaymentFailed,
} from './methods/setPaymentFailed'
+import {
+ GetUsedIdentityGroupSeatsInputSchema,
+ GetUsedIdentityGroupSeatsOutputSchema,
+ getUsedIdentityGroupSeats,
+} from './methods/getUsedIdentityGroupSeats'
const t = initTRPC.context().create({ errorFormatter })
@@ -69,6 +74,12 @@ export const appRouter = t.router({
.use(Analytics)
.input(CancelServicePlansInput)
.mutation(cancelServicePlans),
+ getUsedIdentityGroupSeats: t.procedure
+ .use(LogUsage)
+ .use(Analytics)
+ .input(GetUsedIdentityGroupSeatsInputSchema)
+ .output(GetUsedIdentityGroupSeatsOutputSchema)
+ .query(getUsedIdentityGroupSeats),
getIdentityGroupSeats: t.procedure
.use(LogUsage)
.use(Analytics)