@canva/cli 0.0.1-beta.3 → 0.0.1-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +218 -104
  2. package/cli.js +661 -394
  3. package/lib/cjs/index.cjs +5 -0
  4. package/lib/esm/index.mjs +5 -0
  5. package/package.json +19 -4
  6. package/templates/base/eslint.config.mjs +2 -27
  7. package/templates/base/package.json +31 -25
  8. package/templates/base/scripts/copy_env.ts +10 -0
  9. package/templates/base/scripts/start/app_runner.ts +39 -2
  10. package/templates/base/scripts/start/context.ts +12 -6
  11. package/templates/base/scripts/start/start.ts +11 -0
  12. package/templates/base/scripts/start/tests/start.tests.ts +61 -0
  13. package/templates/base/{webpack.config.cjs → webpack.config.ts} +46 -46
  14. package/templates/common/.cursor/mcp.json +8 -0
  15. package/templates/common/.gitignore.template +5 -6
  16. package/templates/common/.nvmrc +1 -0
  17. package/templates/common/.prettierrc +21 -0
  18. package/templates/common/.vscode/extensions.json +6 -0
  19. package/templates/common/.vscode/mcp.json +9 -0
  20. package/templates/common/README.md +4 -7
  21. package/templates/common/jest.config.mjs +29 -2
  22. package/templates/common/jest.setup.ts +20 -0
  23. package/templates/common/utils/backend/base_backend/create.ts +104 -0
  24. package/templates/common/utils/backend/jwt_middleware/index.ts +1 -0
  25. package/templates/common/utils/backend/jwt_middleware/jwt_middleware.ts +229 -0
  26. package/templates/common/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +630 -0
  27. package/templates/common/utils/table_wrapper.ts +477 -0
  28. package/templates/common/utils/tests/table_wrapper.tests.ts +252 -0
  29. package/templates/common/utils/use_add_element.ts +48 -0
  30. package/templates/common/utils/use_feature_support.ts +28 -0
  31. package/templates/common/utils/use_overlay_hook.ts +74 -0
  32. package/templates/common/utils/use_selection_hook.ts +37 -0
  33. package/templates/dam/backend/routers/dam.ts +23 -19
  34. package/templates/dam/eslint.config.mjs +2 -28
  35. package/templates/dam/package.json +43 -39
  36. package/templates/dam/scripts/copy_env.ts +10 -0
  37. package/templates/dam/scripts/start/app_runner.ts +39 -2
  38. package/templates/dam/scripts/start/context.ts +12 -6
  39. package/templates/dam/scripts/start/start.ts +11 -0
  40. package/templates/dam/scripts/start/tests/start.tests.ts +61 -0
  41. package/templates/dam/src/app.tsx +24 -3
  42. package/templates/dam/src/config.ts +212 -87
  43. package/templates/{hello_world/webpack.config.cjs → dam/webpack.config.ts} +46 -46
  44. package/templates/data_connector/README.md +84 -0
  45. package/templates/data_connector/declarations/declarations.d.ts +29 -0
  46. package/templates/data_connector/eslint.config.mjs +14 -0
  47. package/templates/data_connector/package.json +91 -0
  48. package/templates/data_connector/scripts/copy_env.ts +10 -0
  49. package/templates/data_connector/scripts/ssl/ssl.ts +131 -0
  50. package/templates/data_connector/scripts/start/app_runner.ts +201 -0
  51. package/templates/data_connector/scripts/start/context.ts +171 -0
  52. package/templates/data_connector/scripts/start/start.ts +46 -0
  53. package/templates/data_connector/scripts/start/tests/start.tests.ts +61 -0
  54. package/templates/data_connector/src/api/connect_client.ts +6 -0
  55. package/templates/data_connector/src/api/data_source.ts +96 -0
  56. package/templates/data_connector/src/api/data_sources/designs.tsx +296 -0
  57. package/templates/data_connector/src/api/data_sources/index.ts +4 -0
  58. package/templates/data_connector/src/api/data_sources/templates.tsx +329 -0
  59. package/templates/data_connector/src/api/fetch_data_table.ts +55 -0
  60. package/templates/data_connector/src/api/index.ts +4 -0
  61. package/templates/data_connector/src/api/oauth.ts +8 -0
  62. package/templates/data_connector/src/api/tests/data_source.test.tsx +99 -0
  63. package/templates/data_connector/src/app.tsx +20 -0
  64. package/templates/data_connector/src/components/app_error.tsx +15 -0
  65. package/templates/data_connector/src/components/footer.tsx +26 -0
  66. package/templates/data_connector/src/components/header.tsx +40 -0
  67. package/templates/data_connector/src/components/index.ts +3 -0
  68. package/templates/data_connector/src/components/inputs/messages.tsx +99 -0
  69. package/templates/data_connector/src/components/inputs/search_filter.tsx +108 -0
  70. package/templates/data_connector/src/components/inputs/select_field.tsx +26 -0
  71. package/templates/data_connector/src/context/app_context.tsx +124 -0
  72. package/templates/data_connector/src/context/index.ts +2 -0
  73. package/templates/data_connector/src/context/use_app_context.ts +17 -0
  74. package/templates/data_connector/src/entrypoint.tsx +70 -0
  75. package/templates/data_connector/src/home.tsx +21 -0
  76. package/templates/data_connector/src/index.tsx +69 -0
  77. package/templates/data_connector/src/pages/data_source_config.tsx +9 -0
  78. package/templates/data_connector/src/pages/error.tsx +37 -0
  79. package/templates/data_connector/src/pages/index.ts +4 -0
  80. package/templates/data_connector/src/pages/login.tsx +145 -0
  81. package/templates/data_connector/src/pages/select_source.tsx +24 -0
  82. package/templates/data_connector/src/routes/index.ts +2 -0
  83. package/templates/data_connector/src/routes/protected_route.tsx +25 -0
  84. package/templates/data_connector/src/routes/routes.tsx +46 -0
  85. package/templates/data_connector/src/utils/data_params.ts +17 -0
  86. package/templates/data_connector/src/utils/data_table.ts +115 -0
  87. package/templates/data_connector/src/utils/fetch_result.ts +36 -0
  88. package/templates/data_connector/src/utils/index.ts +2 -0
  89. package/templates/data_connector/src/utils/tests/data_table.test.ts +133 -0
  90. package/templates/data_connector/styles/components.css +38 -0
  91. package/templates/data_connector/tsconfig.json +54 -0
  92. package/templates/{gen_ai/webpack.config.cjs → data_connector/webpack.config.ts} +46 -46
  93. package/templates/gen_ai/README.md +1 -40
  94. package/templates/gen_ai/backend/routers/image.ts +11 -11
  95. package/templates/gen_ai/backend/server.ts +0 -7
  96. package/templates/gen_ai/eslint.config.mjs +2 -27
  97. package/templates/gen_ai/package.json +48 -42
  98. package/templates/gen_ai/scripts/copy_env.ts +10 -0
  99. package/templates/gen_ai/scripts/start/app_runner.ts +39 -2
  100. package/templates/gen_ai/scripts/start/context.ts +12 -6
  101. package/templates/gen_ai/scripts/start/start.ts +11 -0
  102. package/templates/gen_ai/scripts/start/tests/start.tests.ts +61 -0
  103. package/templates/gen_ai/src/api/api.ts +24 -79
  104. package/templates/gen_ai/src/app.tsx +16 -10
  105. package/templates/gen_ai/src/components/footer.messages.ts +0 -5
  106. package/templates/gen_ai/src/components/footer.tsx +7 -25
  107. package/templates/gen_ai/src/components/index.ts +0 -1
  108. package/templates/gen_ai/src/components/loading_results.tsx +4 -8
  109. package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +43 -0
  110. package/templates/gen_ai/src/context/app_context.tsx +5 -33
  111. package/templates/gen_ai/src/context/context.messages.ts +1 -12
  112. package/templates/gen_ai/src/home.tsx +13 -0
  113. package/templates/gen_ai/src/index.tsx +2 -18
  114. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  115. package/templates/{dam/webpack.config.cjs → gen_ai/webpack.config.ts} +46 -46
  116. package/templates/hello_world/eslint.config.mjs +2 -27
  117. package/templates/hello_world/package.json +47 -34
  118. package/templates/hello_world/scripts/copy_env.ts +10 -0
  119. package/templates/hello_world/scripts/start/app_runner.ts +39 -2
  120. package/templates/hello_world/scripts/start/context.ts +12 -6
  121. package/templates/hello_world/scripts/start/start.ts +11 -0
  122. package/templates/hello_world/scripts/start/tests/start.tests.ts +61 -0
  123. package/templates/hello_world/src/app.tsx +20 -0
  124. package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
  125. package/templates/hello_world/src/tests/app.tests.tsx +86 -0
  126. package/templates/hello_world/webpack.config.ts +270 -0
  127. package/templates/common/conf/eslint-general.mjs +0 -277
  128. package/templates/common/conf/eslint-i18n.mjs +0 -23
  129. package/templates/gen_ai/backend/routers/oauth.ts +0 -393
  130. package/templates/gen_ai/src/components/logged_in_status.tsx +0 -44
  131. package/templates/gen_ai/src/services/auth.tsx +0 -26
  132. package/templates/gen_ai/src/services/index.ts +0 -1
@@ -0,0 +1,61 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import type { ChildProcess } from "child_process";
4
+ import { spawn } from "child_process";
5
+ import * as treeKill from "tree-kill";
6
+
7
+ describe("start script", () => {
8
+ let serverProcess: ChildProcess;
9
+
10
+ afterEach(() => {
11
+ if (serverProcess?.pid && !serverProcess.exitCode) {
12
+ treeKill(serverProcess.pid);
13
+ }
14
+ });
15
+
16
+ it("should execute 'npm run start' and start a dev server", async () => {
17
+ const testServerPort = 8089;
18
+ serverProcess = await spawn(
19
+ `npm run start -- -p ${testServerPort} --no-preview`,
20
+ {
21
+ shell: true,
22
+ },
23
+ );
24
+
25
+ if (!serverProcess.pid) {
26
+ throw new Error("Unable to start server");
27
+ }
28
+
29
+ // wait for the server to start and collect output until it reports the running URL
30
+ let output = "";
31
+ await new Promise<void>((resolve, reject) => {
32
+ serverProcess.stdout?.on("data", (data) => {
33
+ output += data.toString();
34
+ if (output.includes("Development URL")) {
35
+ resolve();
36
+ }
37
+ });
38
+
39
+ serverProcess.stderr?.on("data", (data) => {
40
+ console.error("Server process error: ", data.toString());
41
+ reject();
42
+ });
43
+
44
+ // timeout and fail test if the server hasn't correctly started in 10 seconds
45
+ setTimeout(() => {
46
+ if (serverProcess?.pid && !serverProcess.exitCode) {
47
+ treeKill(serverProcess.pid);
48
+ }
49
+ reject(new Error("Test timed out"));
50
+ }, 10000);
51
+ });
52
+
53
+ // check that the server is running and accessible
54
+ const expectedServerURL = `http://localhost:${testServerPort}`;
55
+ expect(output).toContain(expectedServerURL);
56
+ expect(serverProcess.exitCode).toBeNull();
57
+
58
+ // clean up
59
+ treeKill(serverProcess.pid);
60
+ }, 15000); // 15 seconds timeout to allow for the server to start
61
+ });
@@ -1,4 +1,3 @@
1
- import type { Oauth } from "@canva/user";
2
1
  import { POLLING_INTERVAL_IN_SECONDS } from "src/config";
3
2
 
4
3
  /**
@@ -41,22 +40,12 @@ interface RemainingCreditsResult {
41
40
  credits: number;
42
41
  }
43
42
 
44
- /**
45
- * Represents the result of retrieving the logged in status of the user.
46
- */
47
- export type LoggedInState =
48
- | "authenticated"
49
- | "not_authenticated"
50
- | "checking"
51
- | "error";
52
-
53
43
  const endpoints = {
54
44
  queueImageGeneration: "/api/queue-image-generation",
55
45
  getImageGenerationJobStatus: "/api/job-status",
56
46
  cancelImageGenerationJob: "/api/job-status/cancel",
57
47
  getRemainingCredits: "/api/credits",
58
48
  purchaseCredits: "/api/purchase-credits",
59
- getLoggedInStatus: "/api/authentication/status",
60
49
  };
61
50
 
62
51
  /**
@@ -66,21 +55,18 @@ const endpoints = {
66
55
  * @param {number} options.numberOfImages - The number of images to generate.
67
56
  * @returns {Promise<QueueImageGenerationResponse>} A promise that resolves to the created job ID.
68
57
  */
69
- export const queueImageGeneration = async (
70
- {
71
- prompt,
72
- numberOfImages,
73
- }: {
74
- prompt: string;
75
- numberOfImages: number;
76
- },
77
- oauth: Oauth,
78
- ): Promise<QueueImageGenerationResponse> => {
58
+ export const queueImageGeneration = async ({
59
+ prompt,
60
+ numberOfImages,
61
+ }: {
62
+ prompt: string;
63
+ numberOfImages: number;
64
+ }): Promise<QueueImageGenerationResponse> => {
79
65
  const url = new URL(endpoints.queueImageGeneration, BACKEND_HOST);
80
66
  url.searchParams.append("count", numberOfImages.toString());
81
67
  url.searchParams.append("prompt", prompt);
82
68
 
83
- const result: QueueImageGenerationResponse = await sendRequest(url, oauth);
69
+ const result: QueueImageGenerationResponse = await sendRequest(url);
84
70
 
85
71
  return result;
86
72
  };
@@ -90,14 +76,11 @@ export const queueImageGeneration = async (
90
76
  * @param {string} jobId - The ID of the job to retrieve status for.
91
77
  * @returns {Promise<ImageGenerationResult>} A promise that resolves to the image generation result.
92
78
  */
93
- export const getImageGenerationJobStatus = async (
94
- {
95
- jobId,
96
- }: {
97
- jobId: string;
98
- },
99
- oauth: Oauth,
100
- ): Promise<ImageGenerationResult> => {
79
+ export const getImageGenerationJobStatus = async ({
80
+ jobId,
81
+ }: {
82
+ jobId: string;
83
+ }): Promise<ImageGenerationResult> => {
101
84
  const url = new URL(endpoints.getImageGenerationJobStatus, BACKEND_HOST);
102
85
  url.searchParams.append("jobId", jobId);
103
86
 
@@ -109,7 +92,6 @@ export const getImageGenerationJobStatus = async (
109
92
  try {
110
93
  const response = (await sendRequest(
111
94
  url,
112
- oauth,
113
95
  )) as ImageGenerationJobStatusResponse;
114
96
  if (response.status === "completed") {
115
97
  return { images: response.images, credits: response.credits };
@@ -138,13 +120,12 @@ export const getImageGenerationJobStatus = async (
138
120
  */
139
121
  export const cancelImageGenerationJob = async (
140
122
  jobId: string,
141
- oauth: Oauth,
142
123
  ): Promise<void> => {
143
124
  const url = new URL(endpoints.cancelImageGenerationJob, BACKEND_HOST);
144
125
  url.searchParams.append("jobId", jobId);
145
126
 
146
127
  try {
147
- await sendRequest(url, oauth, {
128
+ await sendRequest(url, {
148
129
  method: "POST",
149
130
  });
150
131
  } catch {
@@ -156,26 +137,23 @@ export const cancelImageGenerationJob = async (
156
137
  * Retrieves the remaining credits.
157
138
  * @returns {Promise<RemainingCreditsResult>} - A promise that resolves to the remaining credits.
158
139
  */
159
- export const getRemainingCredits = async (
160
- oauth: Oauth,
161
- ): Promise<RemainingCreditsResult> => {
162
- const url = new URL(endpoints.getRemainingCredits, BACKEND_HOST);
140
+ export const getRemainingCredits =
141
+ async (): Promise<RemainingCreditsResult> => {
142
+ const url = new URL(endpoints.getRemainingCredits, BACKEND_HOST);
163
143
 
164
- const result: RemainingCreditsResult = await sendRequest(url, oauth);
144
+ const result: RemainingCreditsResult = await sendRequest(url);
165
145
 
166
- return result;
167
- };
146
+ return result;
147
+ };
168
148
 
169
149
  /**
170
150
  * Purchases credits for the user.
171
151
  * @returns {Promise<RemainingCreditsResult>} - A promise that resolves to the remaining credits after purchasing.
172
152
  */
173
- export const purchaseCredits = async (
174
- oauth: Oauth,
175
- ): Promise<RemainingCreditsResult> => {
153
+ export const purchaseCredits = async (): Promise<RemainingCreditsResult> => {
176
154
  const url = new URL(endpoints.purchaseCredits, BACKEND_HOST);
177
155
 
178
- const result: RemainingCreditsResult = await sendRequest(url, oauth, {
156
+ const result: RemainingCreditsResult = await sendRequest(url, {
179
157
  method: "POST",
180
158
  });
181
159
 
@@ -183,48 +161,15 @@ export const purchaseCredits = async (
183
161
  };
184
162
 
185
163
  /**
186
- * Send a request to an endpoint that checks if the user is authenticated.
187
- * This is example code, intended to convey the basic idea. When implementing this in your app, you might want more advanced checks.
188
- *
189
- * Note: You must register the provided endpoint via the Developer Portal.
190
- */
191
- export const checkAuthenticationStatus = async (
192
- oauth: Oauth,
193
- ): Promise<LoggedInState> => {
194
- try {
195
- const url = new URL(endpoints.getLoggedInStatus, BACKEND_HOST);
196
- const result: { isAuthenticated: string } = await sendRequest(url, oauth, {
197
- method: "POST",
198
- });
199
-
200
- if (result?.isAuthenticated) {
201
- return "authenticated";
202
- } else {
203
- return "not_authenticated";
204
- }
205
- } catch (error) {
206
- // eslint-disable-next-line no-console
207
- console.error(error);
208
- return "error";
209
- }
210
- };
211
-
212
- /**
213
- * Sends a request to the specified URL with authorization headers.
164
+ * Sends a request to the specified URL.
214
165
  * @param {URL} url - The URL to send the request to.
215
166
  * @param {RequestInit} [options] - Optional fetch options to be passed to the fetch function.
216
167
  * @returns {Promise<Object>} - A promise that resolves to the response body.
217
168
  */
218
- const sendRequest = async <T>(
219
- url: URL,
220
- oauth: Oauth,
221
- options?: RequestInit,
222
- ): Promise<T> => {
223
- const token = (await oauth.getAccessToken())?.token;
169
+ const sendRequest = async <T>(url: URL, options?: RequestInit): Promise<T> => {
224
170
  const res = await fetch(url, {
225
171
  headers: {
226
172
  ...options?.headers,
227
- Authorization: `Bearer ${token}`,
228
173
  },
229
174
  ...options,
230
175
  });
@@ -1,13 +1,19 @@
1
- import { Outlet } from "react-router-dom";
2
- import { Rows } from "@canva/app-ui-kit";
3
- import { Footer } from "./components";
4
- import * as styles from "styles/components.css";
1
+ import { createHashRouter, RouterProvider } from "react-router-dom";
2
+ import { ContextProvider } from "./context";
3
+ import { routes } from "./routes";
4
+ import { AppI18nProvider } from "@canva/app-i18n-kit";
5
+ import { AppUiProvider } from "@canva/app-ui-kit";
6
+ import { ErrorBoundary } from "react-error-boundary";
7
+ import { ErrorPage } from "./pages";
5
8
 
6
9
  export const App = () => (
7
- <div className={styles.scrollContainer}>
8
- <Rows spacing="3u">
9
- <Outlet />
10
- <Footer />
11
- </Rows>
12
- </div>
10
+ <AppI18nProvider>
11
+ <AppUiProvider>
12
+ <ErrorBoundary fallback={<ErrorPage />}>
13
+ <ContextProvider>
14
+ <RouterProvider router={createHashRouter(routes)} />
15
+ </ContextProvider>
16
+ </ErrorBoundary>
17
+ </AppUiProvider>
18
+ </AppI18nProvider>
13
19
  );
@@ -11,11 +11,6 @@ export const FooterMessages = defineMessages({
11
11
  defaultMessage: "Generate image",
12
12
  description: "A button label to generate an image from a prompt",
13
13
  },
14
- signUpOrLogin: {
15
- defaultMessage: "Sign up or log in to generate",
16
- description:
17
- "A button label to sign up or log in, before the user is able to generate an image",
18
- },
19
14
  purchaseMoreCredits: {
20
15
  defaultMessage: "Purchase more credits",
21
16
  description:
@@ -1,11 +1,10 @@
1
1
  import { useNavigate, useLocation } from "react-router-dom";
2
2
  import { Rows, Button } from "@canva/app-ui-kit";
3
3
  import { queueImageGeneration, purchaseCredits } from "src/api";
4
- import { LoggedInStatus, RemainingCredits } from "src/components";
4
+ import { RemainingCredits } from "src/components";
5
5
  import { NUMBER_OF_IMAGES_TO_GENERATE } from "src/config";
6
6
  import { useAppContext } from "src/context";
7
7
  import { Paths } from "src/routes";
8
- import { useAuth } from "src/services";
9
8
  import { getObsceneWords } from "src/utils";
10
9
  import { useIntl } from "react-intl";
11
10
  import { FooterMessages as Messages } from "./footer.messages";
@@ -13,10 +12,8 @@ import { FooterMessages as Messages } from "./footer.messages";
13
12
  export const Footer = () => {
14
13
  const navigate = useNavigate();
15
14
  const { pathname } = useLocation();
16
- const { requestAuthentication } = useAuth();
17
15
  const isRootRoute = pathname === Paths.HOME;
18
16
  const {
19
- loggedInState,
20
17
  setAppError,
21
18
  promptInput,
22
19
  setPromptInput,
@@ -27,7 +24,6 @@ export const Footer = () => {
27
24
  setIsLoadingImages,
28
25
  remainingCredits,
29
26
  setRemainingCredits,
30
- oauth,
31
27
  } = useAppContext();
32
28
  const intl = useIntl();
33
29
 
@@ -75,13 +71,10 @@ export const Footer = () => {
75
71
 
76
72
  setIsLoadingImages(true);
77
73
  try {
78
- const { jobId } = await queueImageGeneration(
79
- {
80
- prompt: promptInput,
81
- numberOfImages: NUMBER_OF_IMAGES_TO_GENERATE,
82
- },
83
- oauth,
84
- );
74
+ const { jobId } = await queueImageGeneration({
75
+ prompt: promptInput,
76
+ numberOfImages: NUMBER_OF_IMAGES_TO_GENERATE,
77
+ });
85
78
 
86
79
  setJobId(jobId);
87
80
  } catch {
@@ -90,12 +83,8 @@ export const Footer = () => {
90
83
  navigate(Paths.RESULTS);
91
84
  };
92
85
 
93
- const onSignUpOrLogInClick = async () => {
94
- await requestAuthentication();
95
- };
96
-
97
86
  const onPurchaseMoreCredits = async () => {
98
- const { credits } = await purchaseCredits(oauth);
87
+ const { credits } = await purchaseCredits();
99
88
 
100
89
  setRemainingCredits(credits);
101
90
  };
@@ -114,17 +103,11 @@ export const Footer = () => {
114
103
  : intl.formatMessage(Messages.generateAgain),
115
104
  visible: hasRemainingCredits,
116
105
  },
117
- {
118
- variant: "primary" as const,
119
- onClick: onSignUpOrLogInClick,
120
- value: intl.formatMessage(Messages.signUpOrLogin),
121
- visible: loggedInState === "not_authenticated" && !hasRemainingCredits,
122
- },
123
106
  {
124
107
  variant: "primary" as const,
125
108
  onClick: onPurchaseMoreCredits,
126
109
  value: intl.formatMessage(Messages.purchaseMoreCredits),
127
- visible: loggedInState === "authenticated" && !hasRemainingCredits,
110
+ visible: !hasRemainingCredits,
128
111
  },
129
112
  {
130
113
  variant: "secondary" as const,
@@ -155,7 +138,6 @@ export const Footer = () => {
155
138
  ),
156
139
  )}
157
140
  <RemainingCredits />
158
- <LoggedInStatus />
159
141
  </Rows>
160
142
  );
161
143
  };
@@ -2,7 +2,6 @@ export * from "./app_error";
2
2
  export * from "./footer";
3
3
  export * from "./image_grid";
4
4
  export * from "./loading_results";
5
- export * from "./logged_in_status";
6
5
  export * from "./prompt_input";
7
6
  export * from "./report_box";
8
7
  export * from "./remaining_credits";
@@ -70,7 +70,6 @@ export const LoadingResults = ({
70
70
  promptInput,
71
71
  setGeneratedImages,
72
72
  setRemainingCredits,
73
- oauth,
74
73
  } = useAppContext();
75
74
 
76
75
  useEffect(() => {
@@ -83,12 +82,9 @@ export const LoadingResults = ({
83
82
  const pollJobStatus = async () => {
84
83
  if (jobId) {
85
84
  try {
86
- const { images, credits } = await getImageGenerationJobStatus(
87
- {
88
- jobId,
89
- },
90
- oauth,
91
- );
85
+ const { images, credits } = await getImageGenerationJobStatus({
86
+ jobId,
87
+ });
92
88
  setGeneratedImages(images);
93
89
  setRemainingCredits(credits);
94
90
  // Clear the jobId after fetching images
@@ -121,7 +117,7 @@ export const LoadingResults = ({
121
117
  ]);
122
118
 
123
119
  const onCancelClick = async () => {
124
- await cancelImageGenerationJob(jobId, oauth);
120
+ await cancelImageGenerationJob(jobId);
125
121
  setIsLoadingImages(false);
126
122
  navigate(Paths.HOME);
127
123
  };
@@ -0,0 +1,43 @@
1
+ /* eslint-disable formatjs/no-literal-string-in-jsx */
2
+ import { TestAppUiProvider } from "@canva/app-ui-kit";
3
+ import { TestAppI18nProvider } from "@canva/app-i18n-kit";
4
+ import type { RenderResult } from "@testing-library/react";
5
+ import { fireEvent, render } from "@testing-library/react";
6
+ import React from "react";
7
+ import { RemainingCredits } from "../remaining_credits";
8
+ import { requestOpenExternalUrl } from "@canva/platform";
9
+
10
+ function renderInTestProvider(node: React.ReactNode): RenderResult {
11
+ return render(
12
+ // In a test environment, you should wrap your apps in `TestAppI18nProvider` and `TestAppUiProvider`, rather than `AppI18nProvider` and `AppUiProvider`
13
+ <TestAppI18nProvider>
14
+ <TestAppUiProvider>{node}</TestAppUiProvider>,
15
+ </TestAppI18nProvider>,
16
+ );
17
+ }
18
+
19
+ // This test demonstrates how to test code that uses functions from the Canva Apps SDK
20
+ // For more information on testing with the Canva Apps SDK, see https://www.canva.dev/docs/apps/testing/
21
+ describe("Remaining Credit Tests", () => {
22
+ const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);
23
+
24
+ beforeEach(() => {
25
+ jest.resetAllMocks();
26
+ });
27
+
28
+ it("should call requestOpenExternalUrl when the link is clicked", () => {
29
+ // assert that the mock is in the expected clean state
30
+ expect(mockRequestOpenExternalUrl).not.toHaveBeenCalled();
31
+
32
+ const result = renderInTestProvider(<RemainingCredits />);
33
+
34
+ // get a reference to the link to purchase more credits
35
+ const purchaseMoreLink = result.getByRole("button");
36
+
37
+ // programmatically simulate clicking the button
38
+ fireEvent.click(purchaseMoreLink);
39
+
40
+ // we expect that requestOpenExternalUrl has been called
41
+ expect(mockRequestOpenExternalUrl).toHaveBeenCalled();
42
+ });
43
+ });
@@ -1,14 +1,10 @@
1
1
  import { createContext, useEffect, useState } from "react";
2
2
  import { useIntl } from "react-intl";
3
- import type { ImageType, LoggedInState } from "src/api";
4
- import { checkAuthenticationStatus, getRemainingCredits } from "src/api";
3
+ import type { ImageType } from "src/api";
4
+ import { getRemainingCredits } from "src/api";
5
5
  import { ContextMessages as Messages } from "./context.messages";
6
- import type { Oauth } from "@canva/user";
7
- import { auth } from "@canva/user";
8
6
 
9
7
  export interface AppContextType {
10
- loggedInState: LoggedInState;
11
- setLoggedInState: (value: LoggedInState) => void;
12
8
  appError: string;
13
9
  setAppError: (value: string) => void;
14
10
  creditsError: string;
@@ -27,12 +23,9 @@ export interface AppContextType {
27
23
  setPromptInputError: (value: string) => void;
28
24
  generatedImages: ImageType[];
29
25
  setGeneratedImages: (value: ImageType[]) => void;
30
- oauth: Oauth;
31
26
  }
32
27
 
33
28
  export const AppContext = createContext<AppContextType>({
34
- loggedInState: "not_authenticated",
35
- setLoggedInState: () => {},
36
29
  appError: "",
37
30
  setAppError: () => {},
38
31
  creditsError: "",
@@ -51,7 +44,6 @@ export const AppContext = createContext<AppContextType>({
51
44
  setPromptInputError: () => {},
52
45
  generatedImages: [] as ImageType[],
53
46
  setGeneratedImages: () => {},
54
- oauth: {} as Oauth,
55
47
  });
56
48
 
57
49
  /**
@@ -69,8 +61,6 @@ export const ContextProvider = ({
69
61
  }: {
70
62
  children: React.ReactNode;
71
63
  }): JSX.Element => {
72
- const [loggedInState, setLoggedInState] =
73
- useState<LoggedInState>("not_authenticated");
74
64
  const [appError, setAppError] = useState<string>("");
75
65
  const [loadingApp, setLoadingApp] = useState<boolean>(true); // set to true to prevent ui flash on load
76
66
  const [isLoadingImages, setIsLoadingImages] = useState<boolean>(false);
@@ -81,7 +71,6 @@ export const ContextProvider = ({
81
71
  const [generatedImages, setGeneratedImages] = useState<ImageType[]>([]);
82
72
  const [creditsError, setCreditsError] = useState<string>("");
83
73
  const intl = useIntl();
84
- const oauth = auth.initOauth();
85
74
 
86
75
  // Fetches initial data on component mount
87
76
  useEffect(() => {
@@ -91,7 +80,7 @@ export const ContextProvider = ({
91
80
 
92
81
  // Fetch remaining credits
93
82
  try {
94
- const { credits } = await getRemainingCredits(oauth);
83
+ const { credits } = await getRemainingCredits();
95
84
  setRemainingCredits(credits);
96
85
  } catch (error) {
97
86
  setAppError(
@@ -100,17 +89,6 @@ export const ContextProvider = ({
100
89
  // eslint-disable-next-line no-console
101
90
  console.error("Error fetching remaining credits:", error);
102
91
  }
103
-
104
- // Fetch login status
105
- try {
106
- checkAuthenticationStatus(oauth);
107
- } catch (error) {
108
- setAppError(
109
- intl.formatMessage(Messages.appErrorGetLoggedInStatusFailed),
110
- );
111
- // eslint-disable-next-line no-console
112
- console.error("Error fetching login status:", error);
113
- }
114
92
  } catch (error) {
115
93
  setAppError(intl.formatMessage(Messages.appErrorGeneral));
116
94
  // eslint-disable-next-line no-console
@@ -130,13 +108,10 @@ export const ContextProvider = ({
130
108
  return;
131
109
  }
132
110
 
133
- const errorMessage =
134
- loggedInState === "authenticated"
135
- ? intl.formatMessage(Messages.alertNotEnoughCreditsLoggedIn)
136
- : intl.formatMessage(Messages.alertNotEnoughCreditsLoggedOut);
111
+ const errorMessage = intl.formatMessage(Messages.alertNotEnoughCredits);
137
112
 
138
113
  setCreditsError(errorMessage);
139
- }, [loadingApp, remainingCredits, loggedInState]);
114
+ }, [loadingApp, remainingCredits]);
140
115
 
141
116
  const setPromptInputHandler = (value: string) => {
142
117
  if (
@@ -153,8 +128,6 @@ export const ContextProvider = ({
153
128
  };
154
129
 
155
130
  const value: AppContextType = {
156
- loggedInState,
157
- setLoggedInState,
158
131
  appError,
159
132
  setAppError,
160
133
  creditsError,
@@ -173,7 +146,6 @@ export const ContextProvider = ({
173
146
  setPromptInputError,
174
147
  generatedImages,
175
148
  setGeneratedImages,
176
- oauth,
177
149
  };
178
150
 
179
151
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
@@ -12,11 +12,6 @@ export const ContextMessages = defineMessages({
12
12
  description:
13
13
  "A message to indicate that there was a failure to get the number of credits the user has",
14
14
  },
15
- appErrorGetLoggedInStatusFailed: {
16
- defaultMessage: "Retrieving logged in status has failed.",
17
- description:
18
- "A message to indicate that due to an unexpected problem, the app was unable to determine if the user is logged in",
19
- },
20
15
 
21
16
  /** Messages related to prompts and user input validation. */
22
17
  promptMissingErrorMessage: {
@@ -26,16 +21,10 @@ export const ContextMessages = defineMessages({
26
21
  },
27
22
 
28
23
  /** Messages related to credits, including their availability and purchasing options. */
29
- alertNotEnoughCreditsLoggedIn: {
24
+ alertNotEnoughCredits: {
30
25
  defaultMessage:
31
26
  "You don’t have enough credits left to generate an image. Please purchase more.",
32
27
  description:
33
28
  "A message to indicate that the user doesn't have enough credits to generate an image, and will need to buy more to continue",
34
29
  },
35
- alertNotEnoughCreditsLoggedOut: {
36
- defaultMessage:
37
- "You don’t have enough credits left to generate an image. Please sign up or log in to purchase more.",
38
- description:
39
- "A message to indicate that the user doesn't have enough credits to generate an image, and will need to sign up or log in to buy more",
40
- },
41
30
  });
@@ -0,0 +1,13 @@
1
+ import { Outlet } from "react-router-dom";
2
+ import { Rows } from "@canva/app-ui-kit";
3
+ import { Footer } from "./components";
4
+ import * as styles from "styles/components.css";
5
+
6
+ export const Home = () => (
7
+ <div className={styles.scrollContainer}>
8
+ <Rows spacing="3u">
9
+ <Outlet />
10
+ <Footer />
11
+ </Rows>
12
+ </div>
13
+ );
@@ -1,27 +1,11 @@
1
1
  import { createRoot } from "react-dom/client";
2
- import { createHashRouter, RouterProvider } from "react-router-dom";
3
- import { ErrorBoundary } from "react-error-boundary";
4
- import { AppUiProvider } from "@canva/app-ui-kit";
5
- import { routes } from "./routes";
6
- import { ErrorPage } from "./pages";
7
- import { ContextProvider } from "./context";
8
2
  import "@canva/app-ui-kit/styles.css";
9
- import { AppI18nProvider } from "@canva/app-i18n-kit";
3
+ import { App } from "./app";
10
4
 
11
5
  const root = createRoot(document.getElementById("root") as Element);
12
6
 
13
7
  function render() {
14
- root.render(
15
- <AppI18nProvider>
16
- <AppUiProvider>
17
- <ErrorBoundary fallback={<ErrorPage />}>
18
- <ContextProvider>
19
- <RouterProvider router={createHashRouter(routes)} />
20
- </ContextProvider>
21
- </ErrorBoundary>
22
- </AppUiProvider>
23
- </AppI18nProvider>,
24
- );
8
+ root.render(<App />);
25
9
  }
26
10
 
27
11
  render();
@@ -1,4 +1,4 @@
1
- import { App } from "src/app";
1
+ import { Home } from "src/home";
2
2
  import { ErrorPage, GeneratePage, ResultsPage } from "src/pages";
3
3
 
4
4
  export enum Paths {
@@ -9,7 +9,7 @@ export enum Paths {
9
9
  export const routes = [
10
10
  {
11
11
  path: Paths.HOME,
12
- element: <App />,
12
+ element: <Home />,
13
13
  errorElement: <ErrorPage />,
14
14
  children: [
15
15
  {