Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
bb7d759
feat: poc app folder
ilasw Mar 18, 2025
9f62e27
Merge branch 'main' of https://github.com/dailydotdev/apps into feat-…
ilasw Mar 18, 2025
30bc2c1
Merge branch 'main' of https://github.com/dailydotdev/apps into feat-…
ilasw Mar 21, 2025
1072de0
feat: add app folder boot fetch and jotai query
ilasw Mar 26, 2025
138e866
feat: make get current user working on both ssr and client
ilasw Mar 26, 2025
1110abc
feat: add funnel basic types
ilasw Mar 27, 2025
c337435
feat: add funnel id/version support and sub folder
ilasw Mar 27, 2025
647c00c
refactor: clean log and remove tsx extension
ilasw Mar 27, 2025
70ccbcc
fix: build error
ilasw Mar 27, 2025
11fbe24
Merge branch 'main' of https://github.com/dailydotdev/apps into feat-…
ilasw Mar 27, 2025
7b20e1f
fix: build errors and linter
ilasw Mar 27, 2025
8b10637
feat: add client hook for testing ssr hydration
ilasw Mar 31, 2025
a2bef0e
fix: client not aligned with server
ilasw Apr 1, 2025
2fb8e7c
Merge branch 'main' into feat-app-directory-helloworld
ilasw Apr 1, 2025
9402d34
feat: update auth actions
ilasw Apr 1, 2025
1a27388
fix: mock next/navigation for testing
ilasw Apr 1, 2025
aced153
Merge branch 'main' into feat-app-directory-helloworld
ilasw Apr 1, 2025
e3dd2dc
fix: mock next/navigation for testing
ilasw Apr 1, 2025
b36b0eb
refactor: moved useAppAuth hook
ilasw Apr 1, 2025
da69daf
refactor: remove fragment
ilasw Apr 1, 2025
1e43ab9
refactor: remove unnecessary 'use client' directive from useRefreshToken
ilasw Apr 1, 2025
27bbb3f
Merge remote-tracking branch 'origin/feat-app-directory-helloworld' i…
ilasw Apr 1, 2025
389405a
fix: lint prettier
ilasw Apr 1, 2025
c279170
feat: update context providers and refresh sub
ilasw Apr 1, 2025
24c5e6e
feat: add return type
ilasw Apr 1, 2025
8c94256
fix: update boot data fetching method to use fetchQuery
ilasw Apr 2, 2025
d28d6eb
refactor: remove unused dependencies and improve boot data handling
ilasw Apr 2, 2025
3a20140
fix: update useAppAuth to utilize initial data in boot data query
ilasw Apr 2, 2025
23e435b
refactor: update FunnelStepType enum values to camelCase
ilasw Apr 2, 2025
28e4af2
refactor: add meta to app router layout
ilasw Apr 2, 2025
9358e22
refactor: remove jotai and jotai-tanstack-query dependencies
ilasw Apr 3, 2025
7c91c62
refactor: optimize useRouterQuery
ilasw Apr 3, 2025
8d83023
refactor: use useState for queryClient initialization
ilasw Apr 3, 2025
5c3a2ac
refactor: remove boot data error handling from app-boot.ts
ilasw Apr 3, 2025
1a324d8
refactor: add TODO to improve metadata handling in RootLayout
ilasw Apr 3, 2025
9a5ebf9
refactor: remove jotai and jotai-tanstack-query from pnpm-lock.yaml
ilasw Apr 3, 2025
2736ad4
fix: removed jotai atom from boot
ilasw Apr 3, 2025
7a7b77d
refactor: rename app-boot.ts to appBoot.ts and update import paths
ilasw Apr 3, 2025
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
12 changes: 12 additions & 0 deletions packages/extension/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,16 @@ Object.defineProperty(global, 'BroadcastChannel', {
})),
});

jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
refresh: jest.fn(),
back: jest.fn(),
forward: jest.fn(),
}),
usePathname: () => '',
useSearchParams: () => new URLSearchParams(),
}));

structuredCloneJsonPolyfill();
12 changes: 12 additions & 0 deletions packages/shared/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ jest.mock('next/router', () => ({
),
}));

jest.mock('next/navigation', () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
refresh: jest.fn(),
back: jest.fn(),
forward: jest.fn(),
}),
usePathname: () => '',
useSearchParams: () => new URLSearchParams(),
}));

beforeEach(() => {
clear();
storage.clear();
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/components/buttons/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import type { HTMLAttributes, ReactElement, ReactNode, Ref } from 'react';
import React, { forwardRef, useState } from 'react';
import classNames from 'classnames';
Expand Down
9 changes: 5 additions & 4 deletions packages/shared/src/components/fields/ImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { IconSize } from '../Icon';
import { useToastNotification } from '../../hooks/useToastNotification';
import { ButtonSize, ButtonVariant } from '../buttons/common';
import CloseButton from '../CloseButton';
import {
MEGABYTE,
acceptedTypesList,
ACCEPTED_TYPES,
} from '../../graphql/posts';

type Size = 'medium' | 'large' | 'cover';

Expand All @@ -34,8 +39,6 @@ interface ImageInputProps {
fileSizeLimitMB?: number;
}

export const MEGABYTE = 1024 * 1024;

const componentSize: Record<Size, string> = {
medium: 'w-24 h-24 rounded-26',
large: 'w-40 h-40 rounded-40',
Expand All @@ -46,8 +49,6 @@ const sizeToIconSize: Record<Size, IconSize> = {
large: IconSize.Medium,
cover: IconSize.Small,
};
export const ACCEPTED_TYPES = 'image/png,image/jpeg';
export const acceptedTypesList = ACCEPTED_TYPES.split(',');
Comment on lines -49 to -50
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since they are used on server side code we should not mix with client-side one.


function ImageInput({
initialValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import type {
UseMarkdownInputProps,
} from '../../../hooks/input';
import { useMarkdownInput } from '../../../hooks/input';
import { ACCEPTED_TYPES } from '../ImageInput';
import { MarkdownUploadLabel } from './MarkdownUploadLabel';
import { markdownGuide } from '../../../lib/constants';
import useSidebarRendered from '../../../hooks/useSidebarRendered';
Expand All @@ -43,6 +42,7 @@ import { Divider } from '../../utilities';
import { usePopupSelector } from '../../../hooks/usePopupSelector';
import { focusInput } from '../../../lib/textarea';
import CloseButton from '../../CloseButton';
import { ACCEPTED_TYPES } from '../../../graphql/posts';

interface ClassName {
container?: string;
Expand Down
10 changes: 5 additions & 5 deletions packages/shared/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use client';

import type { ReactElement, ReactNode } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import type { QueryObserverResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useRouter } from 'next/navigation';
import type { AnonymousUser, LoggedUser } from '../lib/user';
import {
deleteAccount,
Expand Down Expand Up @@ -77,14 +79,12 @@ export const getQueryParams = (): Record<string, string> => {
}

const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());

return params;
return Object.fromEntries(urlSearchParams.entries());
};

export const REGISTRATION_PATH = '/register';

const logout = async (reason: string): Promise<void> => {
export const logout = async (reason: string): Promise<void> => {
await dispatchLogout(reason);
const params = getQueryParams();
if (params.redirect_uri) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import type { ReactElement, ReactNode } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { requestIdleCallback } from 'next/dist/client/request-idle-callback';
Expand Down
44 changes: 38 additions & 6 deletions packages/shared/src/contexts/SubscriptionContext.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
'use client';

import type { ReactElement, ReactNode } from 'react';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import type { Client } from 'graphql-ws';
import { requestIdleCallback } from 'next/dist/client/request-idle-callback';
import { useQuery } from '@tanstack/react-query';
import ProgressiveEnhancementContext from './ProgressiveEnhancementContext';
import AuthContext from './AuthContext';
import { appBootDataQuery } from '../lib/boot';

export interface SubscriptionContextData {
subscriptionClient: Client;
Expand All @@ -20,12 +24,14 @@ export type SubscriptionContextProviderProps = {
children?: ReactNode;
};

export const SubscriptionContextProvider = ({
children,
}: SubscriptionContextProviderProps): ReactElement => {
const useSubscriptionContext = ({
tokenRefreshed,
accessToken,
}: {
tokenRefreshed: boolean;
accessToken: { token: string };
}): SubscriptionContextData => {
const { windowLoaded } = useContext(ProgressiveEnhancementContext);
const { tokenRefreshed, accessToken } = useContext(AuthContext);

const [connected, setConnected] = useState(false);
const [subscriptionClient, setSubscriptionClient] = useState<Client>(null);

Expand All @@ -43,14 +49,40 @@ export const SubscriptionContextProvider = ({
}
}, [windowLoaded, tokenRefreshed, accessToken, subscriptionClient]);

const contextData = useMemo<SubscriptionContextData>(
return useMemo<SubscriptionContextData>(
() => ({
connected,
subscriptionClient,
}),
[connected, subscriptionClient],
);
};

export const AppSubscriptionContextProvider = ({
children,
}: SubscriptionContextProviderProps): ReactElement => {
const { data: boot, dataUpdatedAt } = useQuery(appBootDataQuery);
const tokenRefreshed = dataUpdatedAt > 0;
const accessToken = boot?.accessToken;
const contextData = useSubscriptionContext({
tokenRefreshed,
accessToken,
});
return (
<SubscriptionContext.Provider value={contextData}>
{children}
</SubscriptionContext.Provider>
);
};

export const SubscriptionContextProvider = ({
children,
}: SubscriptionContextProviderProps): ReactElement => {
const { tokenRefreshed, accessToken } = useContext(AuthContext);
const contextData = useSubscriptionContext({
tokenRefreshed,
accessToken,
});
return (
<SubscriptionContext.Provider value={contextData}>
{children}
Expand Down
59 changes: 59 additions & 0 deletions packages/shared/src/features/common/components/AppHeadMetas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ReactElement } from 'react';
import React from 'react';
import { fromCDN } from '../../../lib';

export function AppHeadMetas(): ReactElement {
return (
<>
<meta
name="viewport"
content="initial-scale=1.0, width=device-width, viewport-fit=cover"
/>

<meta name="application-name" content="daily.dev" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="daily.dev" />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="slack-app-id" content="A07AM7XC529" />
<meta name="apple-itunes-app" content="app-id=6740634400" />
Comment on lines +8 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But non blocking since I see you reused it so we don't have to update both places, we just need to make sure we move at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I can add a todo for this?


<link
rel="apple-touch-icon"
sizes="180x180"
href={fromCDN('/apple-touch-icon.png')}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href={fromCDN('/favicon-32x32.png')}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href={fromCDN('/favicon-16x16.png')}
/>
<link rel="manifest" href="/manifest.json" />
<link
rel="sitemap"
type="text/plain"
title="Sitemap"
href="/sitemap.txt"
/>

<script
dangerouslySetInnerHTML={{
__html: `window.addEventListener('load', () => { window.windowLoaded = true; }, {
once: true,
});`,
}}
/>

<link rel="preconnect" href="https://api.daily.dev" />
<link rel="preconnect" href="https://sso.daily.dev" />
<link rel="preconnect" href="https://media.daily.dev" />
</>
);
}
61 changes: 61 additions & 0 deletions packages/shared/src/features/common/hooks/useAppAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { Boot } from '../../../lib/boot';
import type { LoggedUser, AnonymousUser } from '../../../lib/user';
import { appBootDataQuery } from '../../../lib/boot';
import { logout } from '../../../contexts/AuthContext';
import { generateQueryKey, RequestKey } from '../../../lib/query';
import { useRefreshToken } from '../../../hooks/useRefreshToken';

export enum AppAuthActionsKeys {
LOGOUT = 'logout',
REFRESH = 'refresh',
UPDATE_USER = 'updateUser',
}

export type AppAuthActions = Record<
AppAuthActionsKeys,
(data?: unknown) => void
>;

interface UseAppAuthReturn {
boot: Boot;
dispatch: <Data>(action: { type: AppAuthActionsKeys; data?: Data }) => void;
isLoggedIn: boolean;
user: LoggedUser | null;
}

function checkIfUserIsLoggedIn(user: Boot['user']): user is LoggedUser {
return 'username' in user;
}

export const useAppAuth = (): UseAppAuthReturn => {
const queryClient = useQueryClient();
const initialData = queryClient.getQueryData(appBootDataQuery.queryKey);
const { data: boot, refetch } = useQuery({
...appBootDataQuery,
initialData,
});
const user = boot && checkIfUserIsLoggedIn(boot.user) ? boot.user : null;
const isLoggedIn = !!user;

useRefreshToken(boot?.accessToken, refetch);

const actions: Readonly<AppAuthActions> = {
[AppAuthActionsKeys.LOGOUT]: logout,
[AppAuthActionsKeys.REFRESH]: refetch,
[AppAuthActionsKeys.UPDATE_USER]: async (
newUser: LoggedUser | AnonymousUser,
) => {
await queryClient.invalidateQueries({
queryKey: generateQueryKey(RequestKey.Profile, newUser),
});
},
};

return {
boot,
dispatch: ({ type, data }) => actions[type](data),
isLoggedIn,
user,
};
};
14 changes: 14 additions & 0 deletions packages/shared/src/features/common/hooks/useRouterQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';

export const useRouterQuery = (): {
query: Record<string, string>;
} => {
const params = useSearchParams();
return useMemo(
() => ({
query: Object.fromEntries(params.entries()),
}),
[params],
);
};
4 changes: 4 additions & 0 deletions packages/shared/src/features/common/types/page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface AppPageProps {
params: Promise<Record<string, string>>;
searchParams: Promise<Record<string, string>>;
}
Loading