npm package: @lokalise/harmony
Description: Archived: Harmony is deprecated and no longer maintained.
Author: Lokalise
Latest version: 4.7.3
Deprecated: Harmony has been archived; migrate to the lokalise platform-specific implementations.

> **Archived:** Harmony is deprecated and no longer maintained. The repository is read-only and receives no further updates.

# Harmony

A temporary shared library designed to house reusable components, such as molecules and organisms, exclusively for the Expert and Flow platforms during the migration period. This library will be retired upon the completion of the migration and the establishment of the unified next-gen platform.

## Deprecation Status

- Final release: `4.7.3` (documentation-only).
- The npm package is deprecated and will warn on install.
- Existing consumers should migrate to the platform-specific implementations that now live in the Expert, Flow, or next-gen repositories.

## Storybook
https://lokalise.github.io/harmony/?path=/story/app-shell-navigationpanel--default&args=sticky:!true

## Features

### Authentication

This provides a wretch addon for authentication. It is used to add the authentication token to the request headers, and will handle automatic token refreshes.

To compliment this, there are a number of convenience hooks and functions to help with authentication in both a frontend and backend context.

#### Basic Frontend Usage

```typescript
import { HeaderBuilder, JwtAuthHeaderBuilderMiddleware, generateTokenFromClassicSession, refreshExpiredToken, getJwtTokenFromCookie } from '@lokalise/harmony'
import wretch from 'wretch'

// Create a client pointing at the Lokalise Auth API
const authHttpClient = wretch('path/to/auth/provider')

// This is the client that you would use to make requests to your app's API
//  - It assumes that your API is compatible with the shared authentication strategy
export const appHttpClient = wretch('path/to/you/api')
    .errorType('json') // More config as you see fit

const getTeamId = async () => 000 /* Get the team ID from somewhere*/

export const authenticationHeaderBuilder = HeaderBuilder.create()
    .with(
        JwtAuthHeaderBuilderMiddleware({
            // Provide a way to refresh the token - there are utility functions to help with this in the frontend
            refreshToken: refreshExpiredToken(authHttpClient, getTeamId),

            // Setup a source for the current token - there are utility functions to help with this in the frontend
            getCurrentToken: getJwtTokenFromCookie,

            // Additionally, you can promote a classic session to an authenticated session
            // when configuring a client in the context of a classic PHP CSRF session.
            // this `promoteClassicSession` method will automatically promote the session to JWT
            generateNewToken: generateTokenFromClassicSession(authHttpClient, getCsrfTokenFromCookie, getTeamId),

            // Optionally, you can provide a callback to be notified when a new token is issued
            onNewTokenIssued: (token) => {
                // Do something with the new token
            }
        })
    )

const result = await sendByGetRoute(appHttpClient, getTeamUser, {
    headers: await authenticationHeaderBuilder.resolve(),
    pathParams: { teamId, userId }
})
```

### Public API

This provides a set of type safe request routes for the public API. They are designed to be used with the `sendByRoute` style functions that accept a wretch client and a route object.
Additionally, there are a number of convenience hooks and functions to help with public API requests.

#### Basic Frontend Usage

```typescript
import { sendByGetRoute } from '@lokalise/frontend-http-client'
import { type TeamUserResponse, getTeamUser } from '@lokalise/harmony';
import { appAuthenticatedHttpClient } from '../path/to/appAuthenticatedHttpClient';

const result: TeamUserResponse = await sendByGetRoute(appAuthenticatedHttpClient, getTeamUser, { pathParams: { teamId, userId } });
```

#### Basic Backend Usage

```typescript
import { sendByGetRoute } from '@lokalise/backend-http-client'
import { type TeamUserResponse, getTeamUser } from '@lokalise/harmony';
import { Client } from 'undici'

const result: TeamUserResponse = async (client: Client) => {
    return sendByGetRoute(client, getTeamUser, {
        pathParams: { teamId, userId },
        headers: { 'authorization': `Bearer token` }
    });
}
```

### Tests

#### Query Hooks testing
Query hooks tests functionality has been developed due to inconsistencies in data structure from API responses and `zod` schemas. `zod.parse` will error if data is invalid and although this solution is not ideal (until we have data layer that is driving schemas for all apps), we need to do it this way.

It is important not to generate fake data but to use some actual payloads from API, as this iss the source of the problem (like date formatting for example).

If your hook is relatively simple, you may not need to add the hook tests as such, but simply add payload parsing test.

In order to test query hooks, utilise `@tests/utils/apiHelpers`. Setup is already extended with `msw` server setup, so there's no need to add any logic. Simply use normal `it` function, within witch you can use `mockGetResponse` (POST yet to be implemented) like in example below:
```ts
import {
    mockGetResponse,
    renderQueryHook
} from "@tests/utils/apiHelpers";

// Inside tests
// Create expected response
const response = {
    user: {
        id: 1,
        name: 'Test User'
    }
}

// Mock the response
mockGetResponse('/me', responseData)

// Use helper method renderQueryHook to utilise custom client and header builder
const { result } = renderQueryHook(({ client, builder}) => useListTeamsQuery(client, builder, {
    queryKey: ['me']
}))

// Create necessary assertions
await waitFor(() => {
    const { data ,  isSuccess} = result.current
    expect(isSuccess).toBeTruthy();
    expect(data).toEqual(responseData);
});
```

## Permissions Feature

The permissions feature provides a flexible and type-safe way to control access to features and actions throughout the application. It leverages context providers, guard components, and hooks to make permission checks declarative and easy to use in your React components.

### Usage Examples

#### Providing Permissions Context

```tsx
import {
  FeatureFlagActionResolverContextProvider,
  TeamActionResolverContextProvider,
  ProjectActionResolverContextProvider
} from "@lokalise/harmony";

{/* NB :: as these are contexts and will cause everything below to re-render any time the value updates... */}
{/*       we should make sure that the information is provided once and changed as little as possible. */}
{/*       Ideally this is a request when the page loads and then doesn't change until absolutly needed. */}

{/* Provide the FeatureFlag once at the top level of your app */}
<FeatureFlagActionResolverContextProvider enabledFeatureFlags={["homeMarketingBeta"]}>
    
    {/* Team level context should be provided as soon as team context becomes available */}
    {/* NOTE: This might be lower in the DOM tree than feature-flags context*/}
    {/*       the reason they are separate is to allower for checks outside the scope of a team  */}
    <TeamActionResolverContextProvider teamRole="admin">

        {/* Project level context should be provided when projcet information available */}
        {/* NOTE: This is likely going to be lower in the DOM tree than the team context */}
        <ProjectActionResolverContextProvider projectPermissions={["tasks", "upload"]} projectType="marketing">
            {/* ...your app... */}
        </ProjectActionResolverContextProvider>
    </TeamActionResolverContextProvider>
</FeatureFlagActionResolverContextProvider>
```

#### Guarding UI with Permission Checks

```tsx
import { CanPerformActionGuard, CanPerformAllActionsGuard, CanPerformAnyActionGuard } from "@lokalise/harmony";

<CanPerformActionGuard action="accessMarketingProjects">
  <MarketingDashboard />
</CanPerformActionGuard>

<CanPerformAllActionsGuard actions={["accessMarketingProjects", "deleteProjects"]}>
  <DangerZone />
</CanPerformAllActionsGuard>

<CanPerformAnyActionGuard actions={["accessMarketingProjects", "deleteProjects"]}>
  <ProjectDetails />
</CanPerformAnyActionGuard>
```

#### Checking Permissions in Logic

```tsx
import { useCanPerformAction, useCanPerformAllActions, useCanPerformAnyAction } from "@lokalise/harmony";

const canAccess = useCanPerformAction("accessMarketingProjects");
const canDoAll = useCanPerformAllActions("accessMarketingProjects", "deleteProjects");
const canDoAny = useCanPerformAnyAction("accessMarketingProjects", "deleteProjects");

if (canAccess) {
  // show marketing dashboard
}
if (canDoAll) {
  // show danger zone
}
if (canDoAny) {
  // show details or enable actions
}
```

### Contributing: Creating a New Permission Action

To add a new permission action to the system, follow these steps:

1. **Create the Action Resolver**
   - Implement a new resolver function in `src/features/permissions/action-resolvers/`.
   - The resolver should accept an `ActionResolverPayload` and return a boolean indicating if the action is permitted.
   - Example:
     ```ts
     // src/features/permissions/action-resolvers/canManageWidgets.ts
     import type { ActionResolver } from '../types';
     import { has } from '../utils/has'
     
     export const canManageWidgets: ActionResolver = ({ teamRole, projectPermissions }) => {
       return has(teamRole, 'admin') && has(projectPermissions, 'settings');
     };
     ```

2. **Register the Action**
   - Add your new resolver to the `actionResolvers` object in `src/features/permissions/action.ts`:
     ```ts
     import { canManageWidgets } from './action-resolvers/canManageWidgets';
     
     export const actionResolvers = {
       // ...existing actions...
       canManageWidgets,
     };
     ```

3. **Success!**
   - Your new action is now available for use throughout the application via the guards, hooks, and context providers.
   - You can now use it like:
     ```tsx
     <CanPerformActionGuard action="canManageWidgets">
       <WidgetAdminPanel />
     </CanPerformActionGuard>
     ```

For more details, see the documentation in the `src/features/permissions` directory.