From d31eca89f0a8a2de1f01c2266c0edfc17936d20a Mon Sep 17 00:00:00 2001 From: Sarah Rietkerk Date: Thu, 12 Dec 2024 11:38:50 -0800 Subject: [PATCH 01/32] plumbing for showing the feedback iframe --- cli/server.ts | 8 +- localtypings/pxteditor.d.ts | 1 + react-common/components/controls/Feedback.tsx | 169 ++++++++++++++++++ webapp/src/app.tsx | 4 + webapp/src/dialogs.tsx | 9 + webapp/src/projects.tsx | 7 + 6 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 react-common/components/controls/Feedback.tsx diff --git a/cli/server.ts b/cli/server.ts index 061083f414bb..3599d78ab340 100644 --- a/cli/server.ts +++ b/cli/server.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as http from 'http'; +import * as https from 'https'; import * as url from 'url'; import * as querystring from 'querystring'; import * as nodeutil from './nodeutil'; @@ -949,7 +950,7 @@ export function serveAsync(options: ServeOptions) { const wsServerPromise = initSocketServer(serveOptions.wsPort, serveOptions.hostname); if (serveOptions.serial) initSerialMonitor(); - + // https.createServer const server = http.createServer(async (req, res) => { const error = (code: number, msg: string = null) => { res.writeHead(code, { "Content-Type": "text/plain" }) @@ -1362,7 +1363,10 @@ export function serveAsync(options: ServeOptions) { const serverPromise = new Promise((resolve, reject) => { server.on("error", reject); - server.listen(serveOptions.port, serveOptions.hostname, () => resolve()); + server.listen(serveOptions.port, serveOptions.hostname, () => { + console.log(`Server listening on port ${serveOptions.port}`); + return resolve() + }); }); return Promise.all([wsServerPromise, serverPromise]) diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index 26dd274455a3..679bb03d3746 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -1047,6 +1047,7 @@ declare namespace pxt.editor { showLanguagePicker(): void; showShareDialog(title?: string, kind?: "multiplayer" | "vscode" | "share"): void; showAboutDialog(): void; + showFeedbackDialog(): void; showTurnBackTimeDialogAsync(): Promise; showLoginDialog(continuationHash?: string): void; diff --git a/react-common/components/controls/Feedback.tsx b/react-common/components/controls/Feedback.tsx new file mode 100644 index 000000000000..cbde982bd0f6 --- /dev/null +++ b/react-common/components/controls/Feedback.tsx @@ -0,0 +1,169 @@ +export const feedbackConfig: any = { + feedbackUiType: "Modal", + isDisplayed: true, + isEmailCollectionEnabled: false, // to enable email collection + isFileUploadEnabled: false, // to enable file upload function + isScreenshotEnabled: true, // to enable screenshot + isScreenRecordingEnabled: false, // to enable screen recording + invokeOnDismissOnEsc: true, +} + +export const themeOptions = { + baseTheme: "PublisherLightTheme", +} + +export const initfeedbackOptions: any = { + appId: 50315, + ageGroup: "Undefined", + authenticationType: "Unauthenticated", + clientName: "MakeCode", + feedbackConfig: feedbackConfig, + isProduction: false, + themeOptions: themeOptions, + // telemetry - will likely want this + // userId + // userData + +} +export interface FeedbackRequestEventPayload { + Event: string + EventArgs: string +} +type FeedbackRequestPayloadType = FeedbackRequestEventPayload + +export interface FeedbackResponseEventPayload { + event: string + data: T + error?: any +} + +export const FEEDBACK_FRAME_ID = 'feedback-iframe' + +let feedbackData = initfeedbackOptions + +export const feedbackCallbackEventListener = (event: MessageEvent) => { + if (event.data) { + const payload: FeedbackRequestPayloadType = event.data + switch (payload.Event) { + case 'InAppFeedbackInitOptions': //This is required to initialise feedback + sendFeedbackInitOptions() + break + // case 'InAppFeedbackScreenshot': //This is only needed if you want to have screenshot dynamically provided by host + // sendInAppFeedbackScreenshot() + // break + case 'InAppFeedbackOnError': //Invoked when an error occurrs on feedback submission + console.log('Error Message: ', payload.EventArgs) + break + case 'InAppFeedbackAttachDiagnosticLogs': //Invoked when submit button is clicked and feedbackData.feedbackConfig.diagnosticsConfig is configured + console.log('Host App can send diagnostics to a third party app data using', payload.EventArgs) + break + // case 'InAppFeedbackGetContextData': //This is needed if you want to pass in content samples to be displayed on the form + // sendInAppFeedbackGetContextData() + // break + case 'InAppFeedbackSetCurrentPage': //To follow the feedback Page number + console.log('pageName: ', payload.EventArgs) + break + case 'InAppFeedbackSetSubmitButtonState': //To follow the feedback Submit Button State + console.log('submitState: ', payload.EventArgs) + break + case 'InAppFeedbackInitializationComplete': //Invoked when feedback form is fully initialised and displays error/warning if any + console.log('InAppFeedbackInitializationComplete: ', payload.EventArgs) + break + case 'InAppFeedbackOnSuccess': //Invoked when feedback submission is successful + console.log('InAppFeedbackOnSuccess: ', payload.EventArgs) + break + case 'InAppFeedbackDismissWithResult': //Invoked when feedback is dismissed + console.log('InAppFeedbackDismissWithResult: ', payload.EventArgs) + break + case 'InAppFeedbackExtractFeedbackDataForHost': //Invoked when feedback is dismissed + console.log('InAppFeedbackExtractFeedbackDataForHost: ', payload.EventArgs) + break + } + } +} + +let currentTheme = '' + +export const sendUpdateTheme = () => { + type FeedbackResponsePayloadType = FeedbackResponseEventPayload + if (currentTheme == 'WindowsDark') { + currentTheme = 'WindowsLight' + } else { + currentTheme = 'WindowsDark' + } + const response: FeedbackResponsePayloadType = { + event: 'OnFeedbackHostAppThemeChanged', + data: { + baseTheme: currentTheme, + }, + } + const iFrameEle = document.getElementById(FEEDBACK_FRAME_ID) as HTMLIFrameElement + iFrameEle!.contentWindow!.postMessage(response, 'https://admin-ignite.microsoft.com') +} + +//private functions +const sendFeedbackInitOptions = () => { + type FeedbackResponsePayloadType = FeedbackResponseEventPayload + feedbackData.callbackFunctions = undefined +// feedbackData.feedbackConfig!.diagnosticsConfig!.attachDiagnostics = undefined + console.log("got the message to init"); + let response: FeedbackResponsePayloadType = { + event: 'InAppFeedbackInitOptions', + data: feedbackData, + } + response = JSON.parse(JSON.stringify(response)) + const iFrameEle = document.getElementById(FEEDBACK_FRAME_ID) as HTMLIFrameElement + iFrameEle!.contentWindow!.postMessage(response, 'https://admin-ignite.microsoft.com') +} + +// const sendInAppFeedbackScreenshot = () => { +// type FeedbackResponsePayloadType = FeedbackResponseEventPayload +// const response: FeedbackResponsePayloadType = { +// event: 'InAppFeedbackScreenshot', +// data: { +// providedScreenshotType: 'DynamicallyProvided', +// screenshotImageFormat: 'jpeg', +// screenshotBase64: SampleImage, +// }, +// } +// const iFrameEle = document.getElementById(FEEDBACK_FRAME_ID) as HTMLIFrameElement +// iFrameEle!.contentWindow!.postMessage(response, 'https://admin-ignite.microsoft.com') +// } + +// const sendInAppFeedbackGetContextData = () => { +// type FeedbackResponsePayloadType = FeedbackResponseEventPayload +// const file1 = new File([SampleImage], 'sample file 1.txt', { +// type: 'text/plain', +// }) +// const file2 = new File([SampleImage], 'sample file 2.txt', { +// type: 'text/plain', +// }) + +// const response: FeedbackResponsePayloadType = { +// event: 'InAppFeedbackGetContextData', +// data: [ +// { +// fileName: 'sample file 1.png', +// fileType: 'png', +// fileDataBase64: SampleImage, +// }, +// ], +// } +// const iFrameEle = document.getElementById(FEEDBACK_FRAME_ID) as HTMLIFrameElement +// iFrameEle!.contentWindow!.postMessage(response, 'https://admin-ignite.microsoft.com') +// } + + +export const Feedback = () => { + const appId = 50315; + return ( +