Consenti l'accesso all'utente

Questa è la seconda procedura dettagliata per i componenti aggiuntivi di Classroom di una serie di procedure dettagliate.

In questa procedura dettagliata aggiungerai Accedi con Google all'applicazione web. Si tratta di un comportamento richiesto per i componenti aggiuntivi di Classroom. Utilizza le credenziali di di questo flusso di autorizzazione per tutte le chiamate future all'API.

Nel corso di questa procedura dettagliata, completerai quanto segue:

  • Configura l'app web in modo da conservare i dati della sessione all'interno di un iframe.
  • Implementa il flusso di accesso server-server per Google OAuth 2.0.
  • Effettua una chiamata all'API OAuth 2.0.
  • Crea route aggiuntive per supportare l'autorizzazione, la disconnessione e i test Chiamate API.

Al termine, puoi autorizzare completamente gli utenti nella tua applicazione web ed emettere chiamate a API di Google.

Informazioni sul flusso di autorizzazione

Le API di Google utilizzano il protocollo OAuth 2.0 per l'autenticazione e l'autorizzazione. La descrizione completa dell'implementazione di OAuth di Google è disponibile nella Guida OAuth per Google Identity.

Le credenziali della tua applicazione sono gestite in Google Cloud. Una volta che questi sono stati creati, implementare un processo in quattro fasi per autenticare e autorizzare utente:

  1. Richiedi l'autorizzazione. Fornisci un URL di callback come parte della richiesta. Al termine, riceverai un URL di autorizzazione.
  2. Reindirizza l'utente all'URL di autorizzazione. La pagina visualizzata fornisce informazioni delle autorizzazioni richieste dalla tua app e gli chiede di consentire l'accesso. Al termine, l'utente viene indirizzato all'URL di callback.
  3. Ricevi un codice di autorizzazione al tuo percorso di callback. Scambia per un token di accesso e un token di aggiornamento.
  4. Effettuare chiamate a un'API di Google utilizzando i token.
di Gemini Advanced.

Ottenere le credenziali OAuth 2.0

Assicurati di aver creato e scaricato le credenziali OAuth come descritto in pagina Panoramica. Il progetto deve utilizzare queste credenziali per eseguire l'accesso dell'utente.

Implementare il flusso di autorizzazione

Aggiungi logica e route alla nostra app web per realizzare il flusso descritto, includendo queste funzionalità:

  • Avvia il flusso di autorizzazione una volta raggiunta la pagina di destinazione.
  • Richiedi l'autorizzazione e gestisci la risposta del server di autorizzazione.
  • Cancella le credenziali memorizzate.
  • Revocare le autorizzazioni dell'app.
  • Testa una chiamata API.
di Gemini Advanced.

Avvia autorizzazione

Modifica la pagina di destinazione per avviare il flusso di autorizzazione, se necessario. La può essere in due stati: o se sono presenti token salvati sessione corrente o devi ottenere token dal server OAuth 2.0. Esegui una chiamata all'API di prova, se nella sessione sono presenti token, oppure chiedere all'utente di per eseguire l'accesso.

Python

Apri il file routes.py. Innanzitutto, imposta un paio di costanti e il nostro cookie in base ai consigli sulla sicurezza dell'iframe.

# The file that contains the OAuth 2.0 client_id and client_secret.
CLIENT_SECRETS_FILE = "client_secret.json"

# The OAuth 2.0 access scopes to request.
# These scopes must match the scopes in your Google Cloud project's OAuth Consent
# Screen: https://console.cloud.google.com/apis/credentials/consent
SCOPES = [
    "openid",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/classroom.addons.teacher",
    "https://www.googleapis.com/auth/classroom.addons.student"
]

# Flask cookie configurations.
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="None",
)

Spostati sul percorso di destinazione del componente aggiuntivo (/classroom-addon nell'esempio) ). Aggiungi logica per visualizzare una pagina di accesso se la sessione non contiene le "credenziali" chiave.

@app.route("/classroom-addon")
def classroom_addon():
    if "credentials" not in flask.session:
        return flask.render_template("authorization.html")

    return flask.render_template(
        "addon-discovery.html",
        message="You've reached the addon discovery page.")

Java

Il codice per questa procedura dettagliata è disponibile nel modulo step_02_sign_in.

Apri il file application.properties e aggiungi una configurazione di sessione che segue i consigli per la sicurezza dell'iframe.

# iFrame security recommendations call for cookies to have the HttpOnly and
# secure attribute set
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

# Ensures that the session is maintained across the iframe and sign-in pop-up.
server.servlet.session.cookie.same-site=none

Crea una classe di servizio (AuthService.java nel modulo step_02_sign_in) per gestire la logica alla base degli endpoint nel file del controller e configurare l'URI di reindirizzamento, la posizione del file dei client secret e gli ambiti che il tuo componente aggiuntivo richiede. L'URI di reindirizzamento viene utilizzato per reindirizzare gli utenti a un URI specifico dopo aver autorizzato la tua app. Consulta la sezione sulla configurazione del progetto del README.md nel codice sorgente per informazioni su dove posizionare il client_secret.json file.

@Service
public class AuthService {
    private static final String REDIRECT_URI = "https://localhost:5000/callback";
    private static final String CLIENT_SECRET_FILE = "client_secret.json";
    private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport();
    private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();

    private static final String[] REQUIRED_SCOPES = {
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/classroom.addons.teacher",
        "https://www.googleapis.com/auth/classroom.addons.student"
    };

    /** Creates and returns a Collection object with all requested scopes.
    *   @return Collection of scopes requested by the application.
    */
    public static Collection<String> getScopes() {
        return new ArrayList<>(Arrays.asList(REQUIRED_SCOPES));
    }
}

Apri il file del controller (AuthController.java nel file step_02_sign_in ) e aggiungere logica al percorso di destinazione per eseguire il rendering della pagina di accesso se La sessione non contiene la chiave credentials.

@GetMapping(value = {"/start-auth-flow"})
public String startAuthFlow(Model model) {
    try {
        return "authorization";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

@GetMapping(value = {"/addon-discovery"})
public String addon_discovery(HttpSession session, Model model) {
    try {
        if (session == null || session.getAttribute("credentials") == null) {
            return startAuthFlow(model);
        }
        return "addon-discovery";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

La pagina di autorizzazione deve contenere un link o un pulsante che l'utente deve "firmare" ". Se fai clic su questo pulsante, l'utente dovrebbe essere reindirizzato al percorso authorize.

Richiesta autorizzazione

Per richiedere l'autorizzazione, crea e reindirizza l'utente a un modello URL. Questo URL include varie informazioni, ad esempio gli ambiti richiesta, la route di destinazione per dopo l'autorizzazione e il l'ID client. Puoi verificarli in questo esempio di URL di autorizzazione.

Python

Aggiungi la seguente importazione al tuo file routes.py.

import google_auth_oauthlib.flow

Crea un nuovo percorso (/authorize). Crea un'istanza di google_auth_oauthlib.flow.Flow; consigliamo vivamente di utilizzare from_client_secrets_file per farlo.

@app.route("/authorize")
def authorize():
    # Create flow instance to manage the OAuth 2.0 Authorization Grant Flow
    # steps.
    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES)

Imposta il redirect_uri di flow; questo è il percorso a cui vuoi che gli utenti per tornare dopo aver autorizzato l'app. È /callback di esempio.

# The URI created here must exactly match one of the authorized redirect
# URIs for the OAuth 2.0 client, which you configured in the API Console. If
# this value doesn't match an authorized URI, you will get a
# "redirect_uri_mismatch" error.
flow.redirect_uri = flask.url_for("callback", _external=True)

Utilizza l'oggetto flow per creare authorization_url e state. Negozio state nella sessione; viene utilizzato per verificare l'autenticità del la risposta del server in un secondo momento. Infine, reindirizza l'utente al authorization_url.

authorization_url, state = flow.authorization_url(
    # Enable offline access so that you can refresh an access token without
    # re-prompting the user for permission. Recommended for web server apps.
    access_type="offline",
    # Enable incremental authorization. Recommended as a best practice.
    include_granted_scopes="true")

# Store the state so the callback can verify the auth server response.
flask.session["state"] = state

# Redirect the user to the OAuth authorization URL.
return flask.redirect(authorization_url)

Java

Aggiungi i seguenti metodi al file AuthService.java per creare un'istanza del Flow e utilizzarlo per recuperare l'URL di autorizzazione:

  • Il metodo getClientSecrets() legge il file del client secret e crea un oggetto GoogleClientSecrets.
  • Il metodo getFlow() crea un'istanza di GoogleAuthorizationCodeFlow.
  • Il metodo authorize() utilizza l'oggetto GoogleAuthorizationCodeFlow, state e l'URI di reindirizzamento per recuperare l'URL di autorizzazione. Il parametro state viene utilizzato per verificare l'autenticità della risposta dal server di autorizzazione. Il metodo restituisce quindi una mappa con l'URL di autorizzazione e il parametro state.
/** Reads the client secret file downloaded from Google Cloud.
 *   @return GoogleClientSecrets read in from client secret file. */
public GoogleClientSecrets getClientSecrets() throws Exception {
    try {
        InputStream in = SignInApplication.class.getClassLoader()
            .getResourceAsStream(CLIENT_SECRET_FILE);
        if (in == null) {
            throw new FileNotFoundException("Client secret file not found: "
                +   CLIENT_SECRET_FILE);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets
            .load(JSON_FACTORY, new InputStreamReader(in));
        return clientSecrets;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns authorization code flow.
*   @return GoogleAuthorizationCodeFlow object used to retrieve an access
*   token and refresh token for the application.
*   @throws Exception if reading client secrets or building code flow object
*   is unsuccessful.
*/
public GoogleAuthorizationCodeFlow getFlow() throws Exception {
    try {
        GoogleAuthorizationCodeFlow authorizationCodeFlow =
            new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT,
                JSON_FACTORY,
                getClientSecrets(),
                getScopes())
                .setAccessType("offline")
                .build();
        return authorizationCodeFlow;
    } catch (Exception e) {
        throw e;
    }
}

/** Builds and returns a map with the authorization URL, which allows the
*   user to give the app permission to their account, and the state parameter,
*   which is used to prevent cross site request forgery.
*   @return map with authorization URL and state parameter.
*   @throws Exception if building the authorization URL is unsuccessful.
*/
public HashMap authorize() throws Exception {
    HashMap<String, String> authDataMap = new HashMap<>();
    try {
        String state = new BigInteger(130, new SecureRandom()).toString(32);
        authDataMap.put("state", state);

        GoogleAuthorizationCodeFlow flow = getFlow();
        String authUrl = flow
            .newAuthorizationUrl()
            .setState(state)
            .setRedirectUri(REDIRECT_URI)
            .build();
        String url = authUrl;
        authDataMap.put("url", url);

        return authDataMap;
    } catch (Exception e) {
        throw e;
    }
}

Usa l'inserimento del costruttore per creare un'istanza della classe di servizio nella una classe controller.

/** Declare AuthService to be used in the Controller class constructor. */
private final AuthService authService;

/** AuthController constructor. Uses constructor injection to instantiate
*   the AuthService and UserRepository classes.
*   @param authService the service class that handles the implementation logic
*   of requests.
*/
public AuthController(AuthService authService) {
    this.authService = authService;
}

Aggiungi l'endpoint /authorize alla classe controller. Questo endpoint chiama il metodo authorize() AuthService per recuperare il parametro state e l'URL di autorizzazione. Quindi, l'endpoint archivia state nella sessione e reindirizza gli utenti all'URL di autorizzazione.

/** Redirects the sign-in pop-up to the authorization URL.
*   @param response the current response to pass information to.
*   @param session the current session.
*   @throws Exception if redirection to the authorization URL is unsuccessful.
*/
@GetMapping(value = {"/authorize"})
public void authorize(HttpServletResponse response, HttpSession session)
    throws Exception {
    try {
        HashMap authDataMap = authService.authorize();
        String authUrl = authDataMap.get("url").toString();
        String state = authDataMap.get("state").toString();
        session.setAttribute("state", state);
        response.sendRedirect(authUrl);
    } catch (Exception e) {
        throw e;
    }
}

Gestire la risposta del server

Dopo l'autorizzazione, l'utente torna alla route redirect_uri dal passaggio precedente. Nell'esempio precedente, questa route è /callback.

Ricevi un code nella risposta quando l'utente torna dal pagina delle autorizzazioni. Quindi scambia il codice con i token di accesso e di aggiornamento:

Python

Aggiungi le seguenti importazioni al file del server Flask.

import google.oauth2.credentials
import googleapiclient.discovery

Aggiungi il percorso al server. Costruire un'altra istanza di google_auth_oauthlib.flow.Flow, ma questa volta riutilizza lo stato salvato in passaggio precedente.

@app.route("/callback")
def callback():
    state = flask.session["state"]

    flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
        CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
    flow.redirect_uri = flask.url_for("callback", _external=True)

Quindi, richiedi i token di accesso e di aggiornamento. Fortunatamente, l'oggetto flow contiene il metodo fetch_token per eseguire questa operazione. Il metodo prevede gli argomenti code o authorization_response. Utilizza la authorization_response, perché è l'URL completo della richiesta.

authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)

Ora hai le credenziali complete. Archiviarli nella sessione in modo che possono essere recuperati in altri metodi o percorsi, quindi reindirizzano a un componente aggiuntivo pagina di destinazione.

credentials = flow.credentials
flask.session["credentials"] = {
    "token": credentials.token,
    "refresh_token": credentials.refresh_token,
    "token_uri": credentials.token_uri,
    "client_id": credentials.client_id,
    "client_secret": credentials.client_secret,
    "scopes": credentials.scopes
}

# Close the pop-up by rendering an HTML page with a script that redirects
# the owner and closes itself. This can be done with a bit of JavaScript:
# <script>
#     window.opener.location.href = "{{ url_for('classroom_addon') }}";
#     window.close();
# </script>
return flask.render_template("close-me.html")

Java

Aggiungi alla classe di servizio un metodo che restituisca l'oggetto Credentials tramite il passaggio del codice di autorizzazione recuperato dal reindirizzamento eseguito l'URL di autorizzazione. Questo oggetto Credentials viene utilizzato in un secondo momento per recuperare il token di accesso e il token di aggiornamento.

/** Returns the required credentials to access Google APIs.
*   @param authorizationCode the authorization code provided by the
*   authorization URL that's used to obtain credentials.
*   @return the credentials that were retrieved from the authorization flow.
*   @throws Exception if retrieving credentials is unsuccessful.
*/
public Credential getAndSaveCredentials(String authorizationCode) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        GoogleClientSecrets googleClientSecrets = getClientSecrets();
        TokenResponse tokenResponse = flow.newTokenRequest(authorizationCode)
            .setClientAuthentication(new ClientParametersAuthentication(
                googleClientSecrets.getWeb().getClientId(),
                googleClientSecrets.getWeb().getClientSecret()))
            .setRedirectUri(REDIRECT_URI)
            .execute();
        Credential credential = flow.createAndStoreCredential(tokenResponse, null);
        return credential;
    } catch (Exception e) {
        throw e;
    }
}

Aggiungi al controller un endpoint per l'URI di reindirizzamento. Recupera il e il parametro state della richiesta. Confronta state all'attributo state memorizzato nella sessione. Se corrispondono, quindi continua con il flusso di autorizzazione. Se non corrispondono, restituiscono un errore.

Quindi, chiama il metodo getAndSaveCredentials AuthService e passa il come parametro. Dopo aver recuperato Credentials , archivialo nella sessione. Quindi, chiudi la finestra di dialogo e reindirizza il l'utente alla pagina di destinazione del componente aggiuntivo.

/** Handles the redirect URL to grant the application access to the user's
*   account.
*   @param request the current request used to obtain the authorization code
*   and state parameter from.
*   @param session the current session.
*   @param response the current response to pass information to.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the close-pop-up template if authorization is successful, or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/callback"})
public String callback(HttpServletRequest request, HttpSession session,
    HttpServletResponse response, Model model) {
    try {
        String authCode = request.getParameter("code");
        String requestState = request.getParameter("state");
        String sessionState = session.getAttribute("state").toString();
        if (!requestState.equals(sessionState)) {
            response.setStatus(401);
            return onError("Invalid state parameter.", model);
        }
        Credential credentials = authService.getAndSaveCredentials(authCode);
        session.setAttribute("credentials", credentials);
        return "close-pop-up";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Testa una chiamata API

Completata la procedura, puoi effettuare chiamate alle API di Google.

Ad esempio, puoi richiedere le informazioni del profilo dell'utente. Puoi richiedere il dall'API OAuth 2.0.

Python

Leggi la documentazione relativa a API OAuth 2.0 discovery Utilizzala per ottenere un oggetto UserInfo compilato.

# Retrieve the credentials from the session data and construct a
# Credentials instance.
credentials = google.oauth2.credentials.Credentials(
    **flask.session["credentials"])

# Construct the OAuth 2.0 v2 discovery API library.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

# Request and store the username in the session.
# This allows it to be used in other methods or in an HTML template.
flask.session["username"] = (
    user_info_service.userinfo().get().execute().get("name"))

Java

Crea un metodo nella classe di servizio per creare un oggetto UserInfo utilizzando Credentials come parametro.

/** Obtains the Userinfo object by passing in the required credentials.
*   @param credentials retrieved from the authorization flow.
*   @return the Userinfo object for the currently signed-in user.
*   @throws IOException if creating UserInfo service or obtaining the
*   Userinfo object is unsuccessful.
*/
public Userinfo getUserInfo(Credential credentials) throws IOException {
    try {
        Oauth2 userInfoService = new Oauth2.Builder(
            new NetHttpTransport(),
            new GsonFactory(),
            credentials).build();
        Userinfo userinfo = userInfoService.userinfo().get().execute();
        return userinfo;
    } catch (Exception e) {
        throw e;
    }
}

Aggiungi l'endpoint /test al controller che mostra l'email dell'utente.

/** Returns the test request page with the user's email.
*   @param session the current session.
*   @param model the Model interface to pass error information that's
*   displayed on the error page.
*   @return the test page that displays the current user's email or the
*   onError method to handle and display the error message.
*/
@GetMapping(value = {"/test"})
public String test(HttpSession session, Model model) {
    try {
        Credential credentials = (Credential) session.getAttribute("credentials");
        Userinfo userInfo = authService.getUserInfo(credentials);
        String userInfoEmail = userInfo.getEmail();
        if (userInfoEmail != null) {
            model.addAttribute("userEmail", userInfoEmail);
        } else {
            return onError("Could not get user email.", model);
        }
        return "test";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Cancella credenziali

Puoi cancellare le credenziali di un utente rimuovendole dalla sessione corrente. In questo modo puoi testare il routing sulla pagina di destinazione del componente aggiuntivo.

Ti consigliamo di mostrare un'indicazione che indica che l'utente si è disconnesso in precedenza reindirizzandoli alla pagina di destinazione del componente aggiuntivo. La tua app deve eseguire la flusso di autorizzazione per ottenere nuove credenziali, ma agli utenti non viene richiesto di autorizzare di nuovo l'app.

Python

@app.route("/clear")
def clear_credentials():
    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    return flask.render_template("signed-out.html")

In alternativa, usa flask.session.clear(), ma questa operazione potrebbe avere effetti se ci sono altri valori memorizzati nella sessione.

Java

Nel controller, aggiungi un endpoint /clear.

/** Clears the credentials in the session and returns the sign-out
*   confirmation page.
*   @param session the current session.
*   @return the sign-out confirmation page.
*/
@GetMapping(value = {"/clear"})
public String clear(HttpSession session) {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            session.removeAttribute("credentials");
        }
        return "sign-out";
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Revoca l'autorizzazione dell'app

Un utente può revocare l'autorizzazione della tua app inviando una richiesta POST a https://oauth2.googleapis.com/revoke. La richiesta deve contenere il codice sorgente dell'utente token di accesso.

Python

import requests

@app.route("/revoke")
def revoke():
    if "credentials" not in flask.session:
        return flask.render_template("addon-discovery.html",
                            message="You need to authorize before " +
                            "attempting to revoke credentials.")

    credentials = google.oauth2.credentials.Credentials(
        **flask.session["credentials"])

    revoke = requests.post(
        "https://oauth2.googleapis.com/revoke",
        params={"token": credentials.token},
        headers={"content-type": "application/x-www-form-urlencoded"})

    if "credentials" in flask.session:
        del flask.session["credentials"]
        del flask.session["username"]

    status_code = getattr(revoke, "status_code")
    if status_code == 200:
        return flask.render_template("authorization.html")
    else:
        return flask.render_template(
            "index.html", message="An error occurred during revocation!")

Java

Aggiungi alla classe di servizio un metodo che effettui una chiamata all'endpoint di revoca.

/** Revokes the app's permissions to the user's account.
*   @param credentials retrieved from the authorization flow.
*   @return response entity returned from the HTTP call to obtain response
*   information.
*   @throws RestClientException if the POST request to the revoke endpoint is
*   unsuccessful.
*/
public ResponseEntity<String> revokeCredentials(Credential credentials) throws RestClientException {
    try {
        String accessToken = credentials.getAccessToken();
        String url = "https://oauth2.googleapis.com/revoke?token=" + accessToken;

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
        HttpEntity<Object> httpEntity = new HttpEntity<Object>(httpHeaders);
        ResponseEntity<String> responseEntity = new RestTemplate().exchange(
            url,
            HttpMethod.POST,
            httpEntity,
            String.class);
        return responseEntity;
    } catch (RestClientException e) {
        throw e;
    }
}

Aggiungi un endpoint, /revoke, al controller che cancella la sessione e reindirizza l'utente alla pagina di autorizzazione se la revoca è stata riuscito.

/** Revokes the app's permissions and returns the authorization page.
*   @param session the current session.
*   @return the authorization page.
*   @throws Exception if revoking access is unsuccessful.
*/
@GetMapping(value = {"/revoke"})
public String revoke(HttpSession session) throws Exception {
    try {
        if (session != null && session.getAttribute("credentials") != null) {
            Credential credentials = (Credential) session.getAttribute("credentials");
            ResponseEntity responseEntity = authService.revokeCredentials(credentials);
            Integer httpStatusCode = responseEntity.getStatusCodeValue();

            if (httpStatusCode != 200) {
                return onError("There was an issue revoking access: " +
                    responseEntity.getStatusCode(), model);
            }
            session.removeAttribute("credentials");
        }
        return startAuthFlow(model);
    } catch (Exception e) {
        return onError(e.getMessage(), model);
    }
}

Testa il componente aggiuntivo

Accedi a Google Classroom. in qualità di uno dei tuoi utenti di test Insegnanti. Vai alla scheda Lavori del corso e Crea un nuovo compito. Fai clic sul pulsante Componenti aggiuntivi sotto l'area di testo. quindi seleziona il componente aggiuntivo. L'iframe si apre e il componente aggiuntivo carica URI di configurazione dell'allegato che hai specificato nell'app dell'SDK GWM Configurazione .

Complimenti! Sei pronto per andare al passaggio successivo: gestione della ripetizione visite al tuo componente aggiuntivo.