Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve secret selection #708

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
221 changes: 221 additions & 0 deletions src/components/ga4/EventBuilder/MPSecret/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { PAB, SAB } from "@/components/Buttons"
import ExternalLink from "@/components/ExternalLink"
import Spinner from "@/components/Spinner"
import Warning from "@/components/Warning"
import WithHelpText from "@/components/WithHelpText"
import { StorageKey, Url } from "@/constants"
import useFormStyles from "@/hooks/useFormStyles"
import { Dispatch, RequestStatus, successful } from "@/types"
import {
Dialog,
DialogTitle,
makeStyles,
TextField,
Typography,
} from "@material-ui/core"
import { Autocomplete } from "@material-ui/lab"
import * as React from "react"
import StreamPicker, { RenderOption } from "../../StreamPicker"
import useAccountPropertyStream from "../../StreamPicker/useAccountPropertyStream"
import { QueryParam } from "../types"
import useInputs, { CreationStatus } from "./useInputs"
import useMPSecretsRequest, {
MPSecret as MPSecretT,
} from "./useMPSecretsRequest"

const useStyles = makeStyles(theme => ({
mpSecret: {
"&> :not(:first-child)": {
marginTop: theme.spacing(1),
},
},
secret: {
display: "flex",
alignItems: "center",
"&> :not(:first-child)": {
marginLeft: theme.spacing(1),
},
},
createSecretDialog: {
padding: theme.spacing(1),
"&> :not(:first-child)": {
marginTop: theme.spacing(1),
},
},
}))

interface Props {
setSecret: Dispatch<MPSecretT | undefined>
secret: MPSecretT | undefined
useFirebase: boolean
}

const api_secret_reference = (
<ExternalLink href={Url.ga4MPAPISecretReference}>api_secret</ExternalLink>
)

const MPSecret: React.FC<Props> = ({ secret, setSecret, useFirebase }) => {
const formClasses = useFormStyles()
const classes = useStyles()

const aps = useAccountPropertyStream(
StorageKey.eventBuilderAPS,
QueryParam,
{
androidStreams: useFirebase,
iosStreams: useFirebase,
webStreams: !useFirebase,
},
true
)

const secretsRequest = useMPSecretsRequest({
aps,
})

React.useEffect(() => {
if (successful(secretsRequest)) {
const secrets = successful(secretsRequest)!.secrets
console.log("setting to first secret")
setSecret(secrets?.[0])
}
}, [secretsRequest])

const [creationError, setCreationError] = React.useState<any>()

const {
displayName,
setDisplayName,
creationStatus,
setCreationStatus,
} = useInputs()

return (
<section className={classes.mpSecret}>
<Typography>Choose an account, property, and stream.</Typography>
<StreamPicker
streams
{...aps}
noStreamsText={
useFirebase
? "There are no iOS or Android streams for the selected property."
: "There are no web streams for the selected property."
}
/>
<Typography>
Select an existing api_secret or create a new secret.
</Typography>
<WithHelpText
helpText={
<>
The API secret for the property to send the event to. See{" "}
{api_secret_reference} on devsite
</>
}
>
<section className={classes.secret}>
<Autocomplete<MPSecretT, false, false, true>
className={formClasses.grow}
loading={secretsRequest.status !== RequestStatus.Successful}
options={successful(secretsRequest)?.secrets || []}
noOptionsText="There are no secrets for the selected stream."
loadingText={
aps.stream === undefined
? "Choose an account, property, and stream to see existing secrets."
: "Loading..."
}
value={secret || null}
getOptionLabel={secret => secret.secretValue}
getOptionSelected={(a, b) => a.name === b.name}
onChange={(_event, value) => {
if (value === null) {
setSecret(undefined)
return
}
if (typeof value === "string") {
setSecret({ secretValue: value })
return
}
setSecret(value)
}}
renderOption={secret => (
<RenderOption
first={
(typeof secret === "string"
? "manually entered secret"
: secret.displayName) || ""
}
second={secret.secretValue}
/>
)}
renderInput={params => (
<TextField
{...params}
label="api_secret"
size="small"
variant="outlined"
/>
)}
/>
<div>
<SAB
title="Create a new secret under the current stream."
disabled={!successful(secretsRequest)}
onClick={() => {
setCreationStatus(CreationStatus.ShowDialog)
}}
>
new secret
</SAB>
</div>

<Dialog
open={
creationStatus === CreationStatus.ShowDialog ||
creationStatus === CreationStatus.Creating
}
onClose={() => setCreationStatus(CreationStatus.NotStarted)}
>
<DialogTitle>Create new secret</DialogTitle>
<section className={classes.createSecretDialog}>
{creationStatus === CreationStatus.ShowDialog ? (
<TextField
label="secret name"
variant="outlined"
size="small"
value={displayName}
onChange={e => setDisplayName(e.target.value)}
/>
) : (
<Spinner ellipses>creating new secret</Spinner>
)}
<div>
<PAB
add
onClick={async () => {
setCreationStatus(CreationStatus.Creating)
try {
const nuSecret = await successful(
secretsRequest
)!.createMPSecret(displayName)
setCreationStatus(CreationStatus.Done)
setSecret(nuSecret)
} catch (e) {
setCreationError(e)
setCreationStatus(CreationStatus.Failed)
}
}}
>
Create
</PAB>
</div>
</section>
</Dialog>
</section>
{creationError && <Warning>{creationError?.message}</Warning>}
</WithHelpText>
</section>
)
}

export default MPSecret
42 changes: 42 additions & 0 deletions src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Stream } from "@/types/ga4/StreamPicker"
import { useCallback } from "react"
import { useSelector } from "react-redux"

const necessaryScopes = ["https://www.googleapis.com/auth/analytics.edit"]

const useCreateMPSecret = (stream: Stream | undefined) => {
const gapi = useSelector((a: AppState) => a.gapi)
const user = useSelector((a: AppState) => a.user)
return useCallback(
async (displayName: string) => {
if (gapi === undefined || stream === undefined || user === undefined) {
return
}
try {
if (!user.hasGrantedScopes(necessaryScopes.join(","))) {
await user.grant({
scope: necessaryScopes.join(","),
})
}
// TODO - Update this once this is available in the client libraries.
const response = await gapi.client.request({
path: `https://content-analyticsadmin.googleapis.com/v1alpha/${stream.value.name}/measurementProtocolSecrets`,
method: "POST",
body: JSON.stringify({
display_name: displayName,
}),
})
return response.result
} catch (e) {
if (e?.result?.error?.message !== undefined) {
throw new Error(e.result.error.message)
} else {
throw e
}
}
},
[gapi, stream, user]
)
}

export default useCreateMPSecret
38 changes: 38 additions & 0 deletions src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Stream } from "@/types/ga4/StreamPicker"
import { useCallback, useMemo } from "react"
import { useSelector } from "react-redux"
import { MPSecret } from "./useMPSecretsRequest"

const useGetMPSecrets = (stream: Stream | undefined) => {
const gapi = useSelector((a: AppState) => a.gapi)

const requestReady = useMemo(() => {
if (gapi === undefined || stream === undefined) {
return false
}
return true
}, [gapi, stream])

const getMPSecrets = useCallback(async () => {
if (gapi === undefined || stream === undefined) {
throw new Error("Invalid invariant - gapi & stream must be defined here.")
}
try {
const response = await gapi.client.request({
path: `https://content-analyticsadmin.googleapis.com/v1alpha/${stream.value.name}/measurementProtocolSecrets`,
})
console.log({ response })
return (response.result.measurementProtocolSecrets || []) as MPSecret[]
} catch (e) {
console.error(
"There was an error getting the measurement protocol secrets.",
e
)
throw e
}
}, [gapi, stream])

return { requestReady, getMPSecrets }
}

export default useGetMPSecrets
27 changes: 27 additions & 0 deletions src/components/ga4/EventBuilder/MPSecret/useInputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useState } from "react"

import { MPSecret } from "./useMPSecretsRequest"

export enum CreationStatus {
NotStarted = "not-started",
ShowDialog = "show-dialog",
Creating = "creating",
Done = "done",
Failed = "failed",
}

const useInputs = () => {
const [displayName, setDisplayName] = useState("")
const [creationStatus, setCreationStatus] = useState<CreationStatus>(
CreationStatus.NotStarted
)

return {
displayName,
setDisplayName,
creationStatus,
setCreationStatus,
}
}

export default useInputs
Loading