diff --git a/cli/cli.ts b/cli/cli.ts index 3dc5ee9256c3..3bc6f224bfa3 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -2805,7 +2805,6 @@ function renderDocs(builtPackaged: string, localDir: string) { } pxt.log(`All docs written.`); } - export function serveAsync(parsed: commandParser.ParsedCommand) { // always use a cloud build // in most cases, the user machine is not properly setup to @@ -2864,6 +2863,7 @@ export function serveAsync(parsed: commandParser.ParsedCommand) { serial: !parsed.flags["noSerial"] && !globalConfig.noSerial, noauth: parsed.flags["noauth"] as boolean || false, backport: parsed.flags["backport"] as number || 0, + https: parsed.flags["https"] as boolean || false, })) } @@ -7129,7 +7129,8 @@ ${pxt.crowdin.KEY_VARIABLE} - crowdin key description: "port where the locally running backend is listening.", argument: "backport", type: "number", - } + }, + https: { description: "use https protocol instead of http"} } }, serveAsync); diff --git a/cli/server.ts b/cli/server.ts index 061083f414bb..b976d02314bc 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'; @@ -13,6 +14,7 @@ import { promisify } from "util"; import U = pxt.Util; import Cloud = pxt.Cloud; +import { SecureContextOptions } from 'tls'; const userProjectsDirName = "projects"; @@ -804,6 +806,7 @@ export interface ServeOptions { serial?: boolean; noauth?: boolean; backport?: number; + https?: boolean; } // can use http://localhost:3232/streams/nnngzlzxslfu for testing @@ -949,8 +952,7 @@ export function serveAsync(options: ServeOptions) { const wsServerPromise = initSocketServer(serveOptions.wsPort, serveOptions.hostname); if (serveOptions.serial) initSerialMonitor(); - - const server = http.createServer(async (req, res) => { + const reqListener: http.RequestListener = async (req, res) => { const error = (code: number, msg: string = null) => { res.writeHead(code, { "Content-Type": "text/plain" }) res.end(msg || "Error " + code) @@ -1351,7 +1353,10 @@ export function serveAsync(options: ServeOptions) { }); } return - }); + }; + const canUseHttps = serveOptions.https && process.env["HTTPS_KEY"] && process.env["HTTPS_CERT"]; + const httpsServerOptions: SecureContextOptions = {cert: process.env["HTTPS_CERT"], key: process.env["HTTPS_KEY"]}; + const server = canUseHttps ? https.createServer(httpsServerOptions, reqListener) : http.createServer(reqListener); // if user has a server.js file, require it const serverjs = path.resolve(path.join(root, 'built', 'server.js')) @@ -1362,12 +1367,16 @@ 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]) .then(() => { - const start = `http://${serveOptions.hostname}:${serveOptions.port}/#local_token=${options.localToken}&wsport=${serveOptions.wsPort}`; + const protocol = canUseHttps ? "https" : "http"; + const start = `${protocol}://${serveOptions.hostname}:${serveOptions.port}/#local_token=${options.localToken}&wsport=${serveOptions.wsPort}`; console.log(`---------------------------------------------`); console.log(``); console.log(`To launch the editor, open this URL:`); diff --git a/localtypings/ocv.d.ts b/localtypings/ocv.d.ts new file mode 100644 index 000000000000..01e69eaa8f89 --- /dev/null +++ b/localtypings/ocv.d.ts @@ -0,0 +1,274 @@ +declare namespace ocv { + + type FeedbackKind = "generic" | "rating"; + + const enum FeedbackAgeGroup { + Undefined = "Undefined", + MinorWithoutParentalConsent = "MinorWithoutParentalConsent", + MinorWithParentalConsent = "MinorWithParentalConsent", + NotAdult = "NotAdult", + Adult = "Adult", + MinorNoParentalConsentRequired = "MinorNoParentalConsentRequired" + } + + interface IFeedbackCallbackFunctions { + attachDiagnosticsLogs?: (diagnosticsUploadId: string, diagnosticsEndpoint: string) => void; + onDismiss?: (isFeedbackSent?: boolean) => void; + onSuccess?: (clientFeedbackId: string) => void; + onError?: (errorMessage?: string) => void; + supportCallback?: () => void; + initializationComplete?: (initializationCompleteResult: InitializationResult) => void; + setSubmitButtonState?: (isEnabled: boolean) => void; + } + + const enum FeedbackAuthenticationType { + MSA = "MSA", + AAD = "AAD", + Unauthenticated = "Unauthenticated" + } + + const enum FeedbackType { + Smile = "Smile", + Frown = "Frown", + Idea = "Idea", + Unclassified = "Unclassified", + Survey = "Survey" + } + + const enum FeedbackPolicyValue { + Enabled = "Enabled", + Disabled = "Disabled", + NotConfigured = "Not Configured", + NotApplicable = "Not Applicable" + } + + interface IThemeOptions { + isFluentV9?: boolean; + /** + * v9Theme must be Theme object from @fluentui/react-components@9.* + */ + v9Theme?: any; + /** + * brandVariants must be BrandVariants object from @fluentui/react-components@9.* + */ + brandVariants?: any; + baseTheme?: any; + colorScheme?: any; + } + + interface IFeedbackInitOptions { + ageGroup?: FeedbackAgeGroup; + appId?: number; + authenticationType?: FeedbackAuthenticationType; + callbackFunctions?: IFeedbackCallbackFunctions; + clientName?: string; + feedbackConfig?: IFeedbackConfig; + isProduction?: boolean; + telemetry?: IFeedbackTelemetry; + themeOptions?: IThemeOptions; + } + + const enum FeedbackUiType { + SidePane = "SidePane",// Default: Used for side pane/detail view + Modal = "Modal",// Used for modal view + CallOut = "CallOut",// Used for inscreen pop up dialogue + IFrameWithinSidePane = "IFrameWithinSidePane",// Same as side pane but used inside an iframe + IFrameWithinModal = "IFrameWithinModal",// Same as modal but used inside an iframe + IFrameWithinCallOut = "IFrameWithinCallOut",// Same as callout but used inside an iframe + NoSurface = "NoSurface",// Used when the surface is provided by the host app + NoSurfaceWithoutTitle = "NoSurfaceWithoutTitle" + } + + const enum FeedbackHostPlatformType { + Windows = "Windows", + IOS = "iOS", + Android = "Android", + WacTaskPane = "WacTaskPane", + MacOS = "MacOS", + Web = "Web", + IFrame = "IFrame" + } + + const enum FeedbackHostEventName { + SubmitClicked = "InAppFeedback_HostEvent_SubmitClicked", + BackClicked = "InAppFeedback_HostEvent_BackClicked" + } + + const enum InitializationStatus { + Success = "Success", + Error = "Error", + Warning = "Warning" + } + + const enum InAppFeedbackQuestionUiType { + DropDown = "DropDown", + MultiSelect = "MultiSelect", + Rating = "Rating", + SingleSelect = "SingleSelect", + SingleSelectHorizontal = "SingleSelectHorizontal" + } + + const enum InAppFeedbackScenarioType { + FeatureArea = "FeatureArea", + ResponsibleAI = "ResponsibleAI", + Experience = "Experience", + ProductSatisfaction = "ProductSatisfaction", + CrashImpact = "CrashImpact",// CrashImpact is of type Survey + Custom = "Custom", + AIThumbsDown = "AIThumbsDown", + AIThumbsUp = "AIThumbsUp", + AIError = "AIError", + PromptSuggestion = "PromptSuggestion" + } + + const enum InAppFeedbackQuestionUiBehaviour { + QuestionNotRequired = "QuestionNotRequired", + CommentNotRequired = "CommentNotRequired", + CommentRequiredWithLastOption = "CommentRequiredWithLastOption" + } + + const enum FeedbackAttachmentOrigin { + Application = "Application", + User = "User" + } + + const enum FeedbackEntryPoint { + Header = "Header", + Footer = "Footer", + Backstage = "Backstage", + HelpMenu = "Help Menu", + Canvas = "Canvas", + Chat = "Chat" + } + + interface InitializationResult { + status: InitializationStatus; + /** + * in UTC timestamp milliseconds + */ + timestamp?: number; + /** + * Duration to load package and validations (centro performance) in milliseconds + */ + loadTime?: number; + errorMessages?: string[]; + warningMessages?: string[]; + } + + interface IFeedbackConfig { + appData?: string; + canDisplayFeedbackCalled?: boolean; + feedbackUiType?: FeedbackUiType; + hideFooterActionButtons?: boolean; + initialFeedbackType?: FeedbackType; + hostPlatform?: FeedbackHostPlatformType; + /** + * Invokes onDismiss callback on Esc button press + * Useful for host apps like Win32 Pane or iFrames + */ + invokeOnDismissOnEsc?: boolean; + isDisplayed?: boolean; + isEmailCollectionEnabled?: boolean; + isFeedbackForumEnabled?: boolean; + isFileUploadEnabled?: boolean; + isMyFeedbackEnabled?: boolean; + isScreenRecordingEnabled?: boolean; + isScreenshotEnabled?: boolean; + isShareContextDataEnabled?: boolean; + isThankYouPageDisabled?: boolean; + isSupportEnabled?: boolean; + maxHeight?: number; + maxWidth?: number; + minHeight?: number; + minWidth?: number; + myFeedbackUrl?: string; + privacyUrl?: string; + retentionDurationDays?: number; + scenarioConfig?: InAppFeedbackScenarioConfig; + supportUrl?: string; + /** + * Enable submit offline feedback + * This will only work if submitOffline callback is provided + */ + isOfflineSubmitEnabled?: boolean; + /** + * For platforms that host other products or sites, this parameter is used to disambiguate the recipient of the data. + * Its effect is to alter the form title for internal users only, replacing 'Microsoft' with the string provided. + * The string length is capped at 30 characters. + * Please keep the name as short as possible to optimize the user experience, preferably including only the product name. + * */ + msInternalTitleTarget?: string; + } + + type IFeedbackTelemetry = { + accountCountryCode?: string; + affectedProcessSessionId?: string; + appVersion?: string; + audience?: string; + audienceGroup?: string; + browser?: string; + browserVersion?: string; + channel?: string; + clientCountryCode?: string; + cpuModel?: string; + dataCenter?: string; + deviceId?: string; + deviceType?: string; + entryPoint?: FeedbackEntryPoint; + errorClassification?: string; + errorCode?: string; + errorName?: string; + featureArea?: string; + featureName?: string; + feedbackOrigin?: string; + flights?: string; + flightSource?: string; + fundamentalArea?: string; + installationType?: string; + isLogIncluded?: boolean; + isUserSubscriber?: boolean; + officeArchitecture?: string; + officeBuild?: string; + officeEditingLang?: number; + officeUILang?: number; + osBitness?: number; + osBuild?: string; + osUserLang?: number; + platform?: string; + processorArchitecture?: string; + processorPhysicalCores?: number; + processSessionId?: string; + ringId?: number; + sku?: string; + sourceContext?: string; + sqmMachineId?: string; + subFeatureName?: string; + sourcePageName?: string; + sourcePageURI?: string; + systemManufacturer?: string; + systemProductName?: string; + uiHost?: string; + }; + + interface InAppFeedbackScenarioConfig { + isScenarioEnabled?: boolean; + scenarioType?: InAppFeedbackScenarioType; + questionDetails?: InAppFeedbackQuestion; + } + + interface InAppFeedbackQuestion { + questionUiType?: InAppFeedbackQuestionUiType; + questionInstruction?: InAppFeedbackCompositeString; + questionOptions?: InAppFeedbackCompositeString[]; + questionUiBehaviour?: InAppFeedbackQuestionUiBehaviour[]; + } + + interface InAppFeedbackCompositeString { + displayedString: string; + displayedStringInEnglish: string; + } +} + + + + diff --git a/localtypings/pxtarget.d.ts b/localtypings/pxtarget.d.ts index d0f1f29e7f15..999da49118ce 100644 --- a/localtypings/pxtarget.d.ts +++ b/localtypings/pxtarget.d.ts @@ -524,6 +524,9 @@ declare namespace pxt { timeMachineDiffInterval?: number; // An interval in milliseconds at which to take diffs to store in project history. Defaults to 5 minutes timeMachineSnapshotInterval?: number; // An interval in milliseconds at which to take full project snapshots in project history. Defaults to 15 minutes adjustBlockContrast?: boolean; // If set to true, all block colors will automatically be adjusted to have a contrast ratio of 4.5 with text + feedbackEnabled?: boolean; // allow feedback to be shown on a target + ocvAppId?: number; // the app id needed to attach to the OCV service + ocvFrameUrl?: string; // the base url for the OCV service } interface DownloadDialogTheme { diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index c90b9b2a7857..7aee16957980 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -1,6 +1,7 @@ /// /// /// +/// declare namespace pxt.editor { export interface EditorMessage { @@ -751,6 +752,11 @@ declare namespace pxt.editor { cmd?: string; } + export interface FeedbackState { + showing: boolean; + kind?: ocv.FeedbackKind; + } + export interface IAppProps { } export interface IAppState { active?: boolean; // is this tab visible at all @@ -805,6 +811,7 @@ declare namespace pxt.editor { extensionsVisible?: boolean; isMultiplayerGame?: boolean; // Arcade: Does the current project contain multiplayer blocks? onboarding?: pxt.tour.BubbleStep[]; + feedback?: FeedbackState; } export interface EditorState { @@ -1047,6 +1054,7 @@ declare namespace pxt.editor { showLanguagePicker(): void; showShareDialog(title?: string, kind?: "multiplayer" | "vscode" | "share"): void; showAboutDialog(): void; + showFeedbackDialog(kind: ocv.FeedbackKind): void; showTurnBackTimeDialogAsync(): Promise; showLoginDialog(continuationHash?: string): void; diff --git a/react-common/components/controls/Feedback/Feedback.tsx b/react-common/components/controls/Feedback/Feedback.tsx new file mode 100644 index 000000000000..7ba3fb8dbc3c --- /dev/null +++ b/react-common/components/controls/Feedback/Feedback.tsx @@ -0,0 +1,70 @@ +/// +import { useEffect } from "react" +import { initFeedbackEventListener, removeFeedbackEventListener } from "./FeedbackEventListener"; +import { baseConfig, ratingFeedbackConfig } from "./configs"; +import { Modal } from "../Modal"; + +// both components require onClose because the feedback modal should close when the user clicks the "finish" button +// this would not happen if the EventListener did not have a callback to close the modal +interface IFeedbackModalProps { + feedbackConfig: ocv.IFeedbackConfig; + frameId: string; + title: string; + onClose: () => void; +} + +// right now, there are two kinds of feedback that I think could be valuable for our targets +// generic and rating feedback, but we will likely want to expand this +interface IFeedbackProps { + kind: ocv.FeedbackKind; + onClose: () => void; +} + +// Wrapper component of the feedback modal so kind can determine what feedback actually shows in the modal +export const Feedback = (props: IFeedbackProps) => { + const { kind, onClose } = props; + return ( + <> + {kind === "generic" && + } + {kind === "rating" && + + } + + ) +} + +export const FeedbackModal = (props: IFeedbackModalProps) => { + const { feedbackConfig, frameId, title, onClose } = props; + + const onDismiss = () => { + onClose(); + } + + let callbacks = { onDismiss }; + + useEffect(() => { + initFeedbackEventListener(feedbackConfig, frameId, callbacks); + return () => { + removeFeedbackEventListener(); + } + }, []) + return ( + +