Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/common/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function versionToText(version: string): string {
return 'Next (unversioned)';
} else if (version === 'latest') {
return `${formatSdkVersion(LATEST_VERSION)} (latest)`;
} else if (BETA_VERSION && version === BETA_VERSION.toString()) {
} else if (version === BETA_VERSION?.toString()) {
return `${formatSdkVersion(BETA_VERSION.toString())} (beta)`;
}

Expand Down
4 changes: 2 additions & 2 deletions docs/components/plugins/APISection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ const isComponent = (entry: GeneratedData) => {
} else if (signatures?.length) {
const mainSignature = signatures[0];
if (
(mainSignature.parameters && mainSignature.parameters[0].name === 'props') ||
(mainSignature.parameters && mainSignature.parameters[0].name === '__namedParameters')
mainSignature.parameters?.[0].name === 'props' ||
mainSignature.parameters?.[0].name === '__namedParameters'
) {
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/components/plugins/api/APISectionCompoundNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const getComponentPropertyChildren = (entry: GeneratedData): PropData[] =

const seen = new Set<string>();
return candidates.filter(child => {
if (!child || child.kind !== TypeDocKind.Property) {
if (child?.kind !== TypeDocKind.Property) {
return false;
}
const id = `${child.name ?? ''}-${child.kind ?? ''}`;
Expand Down
2 changes: 1 addition & 1 deletion docs/components/plugins/api/APISectionEnums.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type APISectionEnumsProps = {
};

const sortByValue = (a: EnumValueData, b: EnumValueData) => {
if (a.type && a.type.value !== undefined && b.type && b.type.value !== undefined) {
if (a.type?.value !== undefined && b.type?.value !== undefined) {
if (typeof a.type.value === 'string' && typeof b.type.value === 'string') {
return a.type.value.localeCompare(b.type.value);
} else if (typeof a.type.value === 'number' && typeof b.type.value === 'number') {
Expand Down
2 changes: 1 addition & 1 deletion docs/components/plugins/api/APISectionUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export const resolveTypeName = (
return 'See description for available values.';
}
return renderUnion(types, { sdkVersion });
} else if (elementType && elementType.type === 'union' && elementType?.types?.length) {
} else if (elementType?.type === 'union' && elementType?.types?.length) {
const unionTypes = elementType?.types ?? [];
return (
<>
Expand Down
2 changes: 1 addition & 1 deletion docs/components/plugins/api/components/APIDataType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const APIDataType = ({ typeDefinition, sdkVersion, inline = true }: APIDa
const isIntersectionWithObject =
type === 'intersection' && types?.filter(typeDefinitionContainsObject).length;
const isUnionWithObject =
(type === 'union' || (elementType && elementType.type === 'union')) &&
(type === 'union' || elementType?.type === 'union') &&
(types?.filter(typeDefinitionContainsObject)?.length ?? 0) > 0;
const isObjectWrapped =
type === 'reference' &&
Expand Down
3 changes: 3 additions & 0 deletions docs/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const jestConfig = {
'^nanoid/index.browser.js$': '<rootDir>/node_modules/nanoid/index.browser.cjs',
'^nanoid$': '<rootDir>/node_modules/nanoid/index.cjs',
'^nanoid/non-secure$': '<rootDir>/node_modules/nanoid/non-secure/index.cjs',
// c15t (used by our cookie consent) bundles CSS modules that jsdom cannot parse
'^@expo/styleguide-cookie-consent$':
'<rootDir>/node_modules/@expo/styleguide-cookie-consent/mock.js',
},
transform: {},
extensionsToTreatAsEsm: ['.ts', '.tsx'],
Expand Down
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"@expo/styleguide": "^9.3.1",
"@expo/styleguide-base": "^2.0.5",
"@expo/styleguide-cookie-consent": "^0.1.11",
"@expo/styleguide-icons": "^2.3.4",
"@expo/styleguide-search-ui": "^3.3.3",
"@kapaai/react-sdk": "^0.9.0",
Expand Down
12 changes: 7 additions & 5 deletions docs/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ThemeProvider } from '@expo/styleguide';
import { CookieConsentProvider } from '@expo/styleguide-cookie-consent';
import { KapaProvider } from '@kapaai/react-sdk';
import { MDXProvider } from '@mdx-js/react';
import * as Sentry from '@sentry/react';
Expand All @@ -9,7 +10,7 @@ import { Inter, JetBrains_Mono } from 'next/font/google';
import { preprocessSentryError } from '~/common/sentry-utilities';
import { useNProgress } from '~/common/useNProgress';
import { DocumentationPageWrapper } from '~/components/DocumentationPageWrapper';
import { AnalyticsProvider } from '~/providers/Analytics';
import { useAnalyticsPageTracking } from '~/providers/Analytics';
import { CodeBlockSettingsProvider } from '~/providers/CodeBlockSettingsProvider';
import { TutorialChapterCompletionProvider } from '~/providers/TutorialChapterCompletionProvider';
import { markdownComponents } from '~/ui/components/Markdown';
Expand Down Expand Up @@ -60,6 +61,7 @@ export { reportWebVitals } from '~/providers/Analytics';

export default function App({ Component, pageProps }: AppProps) {
useNProgress();
useAnalyticsPageTracking();
return (
<>
{/* eslint-disable-next-line react/no-unknown-property */}
Expand All @@ -81,8 +83,8 @@ export default function App({ Component, pageProps }: AppProps) {
}
`}</style>
<MotionConfig reducedMotion="user">
<AnalyticsProvider>
<ThemeProvider>
<ThemeProvider>
<CookieConsentProvider ga4Id="G-YKNPYCMLWY">
<TutorialChapterCompletionProvider>
<CodeBlockSettingsProvider>
<MDXProvider components={rootMarkdownComponents}>
Expand All @@ -94,8 +96,8 @@ export default function App({ Component, pageProps }: AppProps) {
</MDXProvider>
</CodeBlockSettingsProvider>
</TutorialChapterCompletionProvider>
</ThemeProvider>
</AnalyticsProvider>
</CookieConsentProvider>
</ThemeProvider>
</MotionConfig>
</>
);
Expand Down
158 changes: 112 additions & 46 deletions docs/providers/Analytics.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import {
isAnalyticsConsented,
enqueueAnalyticsEvent,
setAnalyticsReplayFn,
getAnalyticsConsentStatus,
} from '@expo/styleguide-cookie-consent';
import type { QueuedEvent } from '@expo/styleguide-cookie-consent';
import { NextWebVitalsMetric } from 'next/app';
import { useRouter } from 'next/compat/router';
import Script from 'next/script';
import React, { PropsWithChildren, useEffect } from 'react';
import { useEffect } from 'react';

/** The global analytics measurement ID */
const MEASUREMENT_ID = 'G-YKNPYCMLWY';

type AnalyticsProps = PropsWithChildren<object>;

/**
* @see https://nextjs.org/docs/messages/next-script-for-ga
* @see https://nextjs.org/docs/basic-features/script#lazyonload
* Hook that subscribes to Next.js router events and reports page views.
* Call this from the App component in _app.tsx.
*/
export function AnalyticsProvider(props: AnalyticsProps) {
export function useAnalyticsPageTracking() {
const router = useRouter();

useEffect(function didMount() {
Expand All @@ -21,67 +25,129 @@ export function AnalyticsProvider(props: AnalyticsProps) {
router?.events.off('routeChangeComplete', reportPageView);
};
}, []);

return (
<>
<Script
id="gtm-script"
strategy="lazyOnload"
src={`https://www.googletagmanager.com/gtag/js?id=${MEASUREMENT_ID}`}
/>
<Script id="gtm-init" strategy="lazyOnload">{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${MEASUREMENT_ID}', { 'transport_type': 'beacon', 'anonymize_ip': true });
`}</Script>
{props.children}
</>
);
}

export function reportPageView(url: string) {
window?.gtag?.('config', MEASUREMENT_ID, {
page_path: url,
transport_type: 'beacon',
anonymize_ip: true,
});
if (isAnalyticsConsented()) {
window?.gtag?.('config', MEASUREMENT_ID, {
page_path: url,
transport_type: 'beacon',
anonymize_ip: true,
});
return;
}

if (getAnalyticsConsentStatus() === 'pending') {
enqueueAnalyticsEvent({
type: 'track',
args: ['pageview', url],
timestamp: Date.now(),
});
}
}

export function reportWebVitals({ id, name, label, value }: NextWebVitalsMetric) {
window?.gtag?.('event', name, {
const payload = {
event_category: label === 'web-vital' ? 'Web Vitals' : 'Next.js custom metric',
// Google Analytics metrics must be integers, so the value is rounded.
// For CLS the value is first multiplied by 1000 for greater precision
// (note: increase the multiplier for greater precision if needed).
value: Math.round(name === 'CLS' ? value * 1000 : value),
// The `id` value will be unique to the current page load. When sending
// multiple values from the same page (e.g. for CLS), Google Analytics can
// compute a total by grouping on this ID (note: requires `eventLabel` to
// be a dimension in your report).
event_label: id,
// Use a non-interaction event to avoid affecting bounce rate.
non_interaction: true,
anonymize_ip: true,
});
};

if (isAnalyticsConsented()) {
window?.gtag?.('event', name, payload);
return;
}

if (getAnalyticsConsentStatus() === 'pending') {
enqueueAnalyticsEvent({
type: 'track',
args: ['webvitals', { name, ...payload }],
timestamp: Date.now(),
});
}
}

export function reportPageVote({ status }: { status: boolean }) {
window?.gtag?.('event', status ? 'page_vote_up' : 'page_vote_down', {
const eventName = status ? 'page_vote_up' : 'page_vote_down';
const payload = {
event_category: 'Page vote',
value: window?.location.pathname,
// Use a non-interaction event to avoid affecting bounce rate.
value: typeof window !== 'undefined' ? window.location.pathname : '',
non_interaction: true,
anonymize_ip: true,
});
};

if (isAnalyticsConsented()) {
window?.gtag?.('event', eventName, payload);
return;
}

if (getAnalyticsConsentStatus() === 'pending') {
enqueueAnalyticsEvent({
type: 'track',
args: ['pagevote', { status, eventName, ...payload }],
timestamp: Date.now(),
});
}
}

export function reportEasTutorialCompleted() {
window?.gtag?.('event', 'eas_tutorial', {
const payload = {
event_category: 'EAS Tutorial Completed',
event_label: 'All chapters in EAS Tutorial completed',
// Use a non-interaction event to avoid affecting bounce rate.
non_interaction: true,
anonymize_ip: true,
});
};

if (isAnalyticsConsented()) {
window?.gtag?.('event', 'eas_tutorial', payload);
return;
}

if (getAnalyticsConsentStatus() === 'pending') {
enqueueAnalyticsEvent({
type: 'track',
args: ['eas_tutorial', payload],
timestamp: Date.now(),
});
}
}

// Register the replay function so queued events are dispatched when consent is granted.
setAnalyticsReplayFn((events: QueuedEvent[]) => {
for (const event of events) {
if (event.type !== 'track') {
continue;
}

const [eventType, data] = event.args as [string, unknown];

switch (eventType) {
case 'pageview': {
const url = data as string;
window?.gtag?.('config', MEASUREMENT_ID, {
page_path: url,
transport_type: 'beacon',
anonymize_ip: true,
});
break;
}
case 'webvitals': {
const { name, ...payload } = data as Record<string, unknown>;
window?.gtag?.('event', name as string, payload);
break;
}
case 'pagevote': {
const { eventName, ...payload } = data as Record<string, unknown>;
window?.gtag?.('event', eventName as string, payload);
break;
}
case 'eas_tutorial': {
const payload = data as Record<string, unknown>;
window?.gtag?.('event', 'eas_tutorial', payload);
break;
}
}
}
});
1 change: 1 addition & 0 deletions docs/tailwind.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
'./scenes/**/*.{js,ts,jsx,tsx}',
'./node_modules/@expo/styleguide/dist/**/*.{js,ts,jsx,tsx}',
'./node_modules/@expo/styleguide-search-ui/dist/**/*.{js,ts,jsx,tsx}',
'./node_modules/@expo/styleguide-cookie-consent/dist/**/*.{js,ts,jsx,tsx}',
],
...getExpoTheme({
backgroundColor: {
Expand Down
2 changes: 2 additions & 0 deletions docs/ui/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LinkBase, mergeClasses } from '@expo/styleguide';
import { PrivacyChoicesButton } from '@expo/styleguide-cookie-consent';
import { ArrowLeftIcon } from '@expo/styleguide-icons/outline/ArrowLeftIcon';
import { ArrowRightIcon } from '@expo/styleguide-icons/outline/ArrowRightIcon';
import { useRouter } from 'next/compat/router';
Expand Down Expand Up @@ -131,6 +132,7 @@ export const Footer = ({
</div>
<NewsletterSignUp />
</div>
<PrivacyChoicesButton />
</footer>
);
};
4 changes: 4 additions & 0 deletions docs/ui/components/Footer/NewsletterSignUp.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Button, mergeClasses } from '@expo/styleguide';
import { isMarketingConsented } from '@expo/styleguide-cookie-consent';
import { Mail01Icon } from '@expo/styleguide-icons/outline/Mail01Icon';
import { useState } from 'react';

Expand All @@ -9,6 +10,9 @@ const PORTAL_ID = '22007177';
const FORM_GUID = '6a213eb9-5e86-4a8e-8607-33f9ac1e07d6';

function getHutk() {
if (!isMarketingConsented()) {
return '';
}
return (
document.cookie
.split('; ')
Expand Down
2 changes: 1 addition & 1 deletion docs/ui/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const InnerTabs = ({
setIndex,
}: Props & { index: number; setIndex: (index: number) => void }) => {
const tabPanels = useMemo(() => collectTabPanels(children), [children]);
const tabTitles = tabs && tabs.length === tabPanels.length ? tabs : generateTabLabels(tabPanels);
const tabTitles = tabs?.length === tabPanels.length ? tabs : generateTabLabels(tabPanels);

const layoutId = useMemo(
() => tabTitles.reduce((acc, tab) => acc + tab, `${Math.random().toString(36).slice(5)}-`),
Expand Down
Loading
Loading