@atlaskit/smart-card 44.7.0 → 44.7.1

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 (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-cold-cache--default.png +3 -0
  3. package/dist/cjs/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-message-low-tier--default.png +3 -0
  4. package/dist/cjs/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-message-not-low-tier--default.png +3 -0
  5. package/dist/cjs/messages.js +12 -1
  6. package/dist/cjs/state/hooks/use-current-site-cloud-id/index.js +0 -30
  7. package/dist/cjs/state/hooks/use-incoming-outgoing-links/index.js +3 -3
  8. package/dist/cjs/state/hooks/use-social-proof/index.js +44 -28
  9. package/dist/cjs/state/hooks/use-social-proof-experiment/index.js +48 -0
  10. package/dist/cjs/state/services/current-site-cloud-id/index.js +61 -74
  11. package/dist/cjs/state/services/personalization/index.js +59 -60
  12. package/dist/cjs/utils/analytics/analytics.js +1 -1
  13. package/dist/cjs/view/BlockCard/views/SocialProofMessage.js +36 -0
  14. package/dist/cjs/view/BlockCard/views/UnauthorisedView.js +85 -20
  15. package/dist/cjs/view/LinkUrl/index.js +1 -1
  16. package/dist/es2019/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-cold-cache--default.png +3 -0
  17. package/dist/es2019/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-message-low-tier--default.png +3 -0
  18. package/dist/es2019/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-message-not-low-tier--default.png +3 -0
  19. package/dist/es2019/messages.js +12 -1
  20. package/dist/es2019/state/hooks/use-current-site-cloud-id/index.js +0 -1
  21. package/dist/es2019/state/hooks/use-incoming-outgoing-links/index.js +2 -2
  22. package/dist/es2019/state/hooks/use-social-proof/index.js +41 -22
  23. package/dist/es2019/state/hooks/use-social-proof-experiment/index.js +41 -0
  24. package/dist/es2019/state/services/current-site-cloud-id/index.js +43 -67
  25. package/dist/es2019/state/services/personalization/index.js +40 -39
  26. package/dist/es2019/utils/analytics/analytics.js +1 -1
  27. package/dist/es2019/view/BlockCard/views/SocialProofMessage.js +27 -0
  28. package/dist/es2019/view/BlockCard/views/UnauthorisedView.js +83 -15
  29. package/dist/es2019/view/LinkUrl/index.js +1 -1
  30. package/dist/esm/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-cold-cache--default.png +3 -0
  31. package/dist/esm/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-message-low-tier--default.png +3 -0
  32. package/dist/esm/__tests__/vr-tests/__snapshots__/block-card/block-card-social-proof-message-not-low-tier--default.png +3 -0
  33. package/dist/esm/messages.js +12 -1
  34. package/dist/esm/state/hooks/use-current-site-cloud-id/index.js +0 -1
  35. package/dist/esm/state/hooks/use-incoming-outgoing-links/index.js +2 -2
  36. package/dist/esm/state/hooks/use-social-proof/index.js +45 -29
  37. package/dist/esm/state/hooks/use-social-proof-experiment/index.js +41 -0
  38. package/dist/esm/state/services/current-site-cloud-id/index.js +59 -72
  39. package/dist/esm/state/services/personalization/index.js +56 -59
  40. package/dist/esm/utils/analytics/analytics.js +1 -1
  41. package/dist/esm/view/BlockCard/views/SocialProofMessage.js +29 -0
  42. package/dist/esm/view/BlockCard/views/UnauthorisedView.js +85 -20
  43. package/dist/esm/view/LinkUrl/index.js +1 -1
  44. package/dist/types/messages.d.ts +1 -1
  45. package/dist/types/state/hooks/use-current-site-cloud-id/index.d.ts +0 -1
  46. package/dist/types/state/hooks/use-social-proof/index.d.ts +17 -4
  47. package/dist/types/state/hooks/use-social-proof-experiment/index.d.ts +39 -0
  48. package/dist/types/state/services/current-site-cloud-id/index.d.ts +9 -33
  49. package/dist/types/state/services/personalization/index.d.ts +14 -24
  50. package/dist/types/view/BlockCard/views/SocialProofMessage.d.ts +14 -0
  51. package/dist/types-ts4.5/messages.d.ts +1 -1
  52. package/dist/types-ts4.5/state/hooks/use-current-site-cloud-id/index.d.ts +0 -1
  53. package/dist/types-ts4.5/state/hooks/use-social-proof/index.d.ts +17 -4
  54. package/dist/types-ts4.5/state/hooks/use-social-proof-experiment/index.d.ts +39 -0
  55. package/dist/types-ts4.5/state/services/current-site-cloud-id/index.d.ts +9 -33
  56. package/dist/types-ts4.5/state/services/personalization/index.d.ts +14 -24
  57. package/dist/types-ts4.5/view/BlockCard/views/SocialProofMessage.d.ts +14 -0
  58. package/package.json +8 -2
  59. package/smart-card.docs.tsx +2 -2
@@ -1,29 +1,29 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { StorageClient } from '@atlaskit/frontend-utilities/storage-client';
3
3
  import { request } from '@atlaskit/linking-common';
4
-
5
- /**
6
- * Logical key shape matches smart-card storage conventions (see personalization-service). {@link StorageClient}
7
- * stores rows as `<clientKey>_<itemKey>` with `clientKey === '@atlaskit/smart-card'` and
8
- * `itemKey === 'site-cloud-id:v1'` (no further scope segments; unlike `pct-map:v1:`, this is a single fixed row).
9
- */
10
- const SMART_CARD_STORAGE_SCOPE = '@atlaskit/smart-card';
11
- export const CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY = 'site-cloud-id:v1';
4
+ const SMART_CARD_STORAGE_SCOPE = 'smart-card-social-proof';
5
+ export const CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY_PREFIX = 'site-cloud-id:v1:';
6
+ export const CURRENT_SITE_CLOUD_ID_TTL_MS = 24 * 60 * 60 * 1000;
12
7
  const smartCardStorage = new StorageClient(SMART_CARD_STORAGE_SCOPE);
8
+ function normalizeBaseUri(baseUriWithNoTrailingSlash = '') {
9
+ return baseUriWithNoTrailingSlash.replace(/\/$/, '');
10
+ }
11
+ function cloudIdStorageItemKey(baseUriWithNoTrailingSlash = '') {
12
+ return `${CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY_PREFIX}${encodeURIComponent(normalizeBaseUri(baseUriWithNoTrailingSlash))}`;
13
+ }
13
14
 
14
15
  /** Keys written by this service in localStorage when using {@link smartCardStorage}. */
15
- export const CURRENT_SITE_CLOUD_ID_LOCAL_STORAGE_KEY = `${SMART_CARD_STORAGE_SCOPE}_${CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY}`;
16
+ export const getCurrentSiteCloudIdLocalStorageKey = (baseUriWithNoTrailingSlash = '') => `${SMART_CARD_STORAGE_SCOPE}_${cloudIdStorageItemKey(baseUriWithNoTrailingSlash)}`;
17
+
18
+ /** Backwards-compatible default-scope key for existing tests and external assertions. */
19
+ export const CURRENT_SITE_CLOUD_ID_LOCAL_STORAGE_KEY = getCurrentSiteCloudIdLocalStorageKey();
16
20
  export class CurrentSiteCloudIdService {
17
21
  constructor() {
18
- /**
19
- * Holds the shared tenant_info work: one in-flight fetch, then (on success) a settled promise for the session cloud
20
- * id so later callers never trigger another `tenant_info` in the same page lifetime (until {@link clearCache}).
21
- */
22
- _defineProperty(this, "tenantInfoInflightPromise", null);
22
+ _defineProperty(this, "tenantInfoInflightPromises", new Map());
23
23
  }
24
- readStoredCloudId() {
24
+ readStoredCloudId(baseUriWithNoTrailingSlash = '') {
25
25
  try {
26
- const cloudId = smartCardStorage.getItem(CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY);
26
+ const cloudId = smartCardStorage.getItem(cloudIdStorageItemKey(baseUriWithNoTrailingSlash));
27
27
  if (typeof cloudId !== 'string' || !cloudId || cloudId === 'undefined') {
28
28
  return undefined;
29
29
  }
@@ -32,87 +32,63 @@ export class CurrentSiteCloudIdService {
32
32
  return undefined;
33
33
  }
34
34
  }
35
- writeStoredCloudId(cloudId) {
35
+ writeStoredCloudId(baseUriWithNoTrailingSlash, cloudId) {
36
36
  if (!cloudId) {
37
37
  return;
38
38
  }
39
39
  try {
40
- smartCardStorage.setItemWithExpiry(CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY, cloudId);
40
+ smartCardStorage.setItemWithExpiry(cloudIdStorageItemKey(baseUriWithNoTrailingSlash), cloudId, CURRENT_SITE_CLOUD_ID_TTL_MS);
41
41
  } catch {
42
42
  // Quota, private-mode, SSR, etc. — same intent as personalization-service.
43
43
  }
44
44
  }
45
45
  ensureTenantInfoInflightStarted(baseUriWithNoTrailingSlash) {
46
- if (this.tenantInfoInflightPromise !== null) {
47
- return;
46
+ const baseUri = normalizeBaseUri(baseUriWithNoTrailingSlash);
47
+ const existing = this.tenantInfoInflightPromises.get(baseUri);
48
+ if (existing) {
49
+ return existing;
48
50
  }
49
- this.tenantInfoInflightPromise = (async () => {
51
+ const promise = (async () => {
50
52
  try {
51
- const response = await request('get', baseUriWithNoTrailingSlash + '/_edge/tenant_info');
53
+ const response = await request('get', `${baseUri}/_edge/tenant_info`);
52
54
  const cloudId = response === null || response === void 0 ? void 0 : response.cloudId;
53
55
  if (cloudId) {
54
- this.writeStoredCloudId(cloudId);
56
+ this.writeStoredCloudId(baseUri, cloudId);
55
57
  }
56
- return cloudId ? cloudId : this.readStoredCloudId();
58
+ return cloudId ? cloudId : this.readStoredCloudId(baseUri);
57
59
  } catch {
58
- this.tenantInfoInflightPromise = null;
59
- return this.readStoredCloudId();
60
+ return this.readStoredCloudId(baseUri);
61
+ } finally {
62
+ this.tenantInfoInflightPromises.delete(baseUri);
60
63
  }
61
64
  })();
65
+ this.tenantInfoInflightPromises.set(baseUri, promise);
66
+ return promise;
62
67
  }
63
68
 
64
- /**
65
- * Returns the currently cached cloud id synchronously and starts a background refresh.
66
- * The refresh result is persisted for future calls but is not awaited by this call.
67
- */
68
- getCachedCloudIdAndRefresh() {
69
- this.ensureTenantInfoInflightStarted('');
70
- return this.readStoredCloudId();
69
+ /** Pure synchronous read scoped to the given base URI. */
70
+ getStoredCloudId(baseUriWithNoTrailingSlash = '') {
71
+ return this.readStoredCloudId(baseUriWithNoTrailingSlash);
71
72
  }
72
73
 
73
74
  /** Writes tenant cloud id for tests or callers that intentionally warm storage before edge resolves. */
74
- persistStoredCloudId(cloudId) {
75
- this.writeStoredCloudId(cloudId);
75
+ persistStoredCloudId(cloudId, baseUriWithNoTrailingSlash = '') {
76
+ this.writeStoredCloudId(baseUriWithNoTrailingSlash, cloudId);
76
77
  }
77
-
78
- /**
79
- * When local storage already has a tenant cloud id, it is returned immediately; a background tenant_info refresh
80
- * is still kicked off unless one is already in flight.
81
- *
82
- * Without storage, this awaits the deduped in-flight tenant_info (first concurrent caller chooses the URL;
83
- * all share one promise regardless of subsequent `baseUriWithNoTrailingSlash`).
84
- *
85
- * On network success with no cloud id, or on failure: falls back via {@link readStoredCloudId}.
86
- */
87
78
  async get(baseUriWithNoTrailingSlash = '') {
88
- const fromStorage = this.readStoredCloudId();
89
- this.ensureTenantInfoInflightStarted(baseUriWithNoTrailingSlash);
79
+ const fromStorage = this.readStoredCloudId(baseUriWithNoTrailingSlash);
90
80
  if (fromStorage) {
91
81
  return fromStorage;
92
82
  }
93
- return this.tenantInfoInflightPromise;
83
+ return this.ensureTenantInfoInflightStarted(baseUriWithNoTrailingSlash);
94
84
  }
95
85
  /** Clears session pin and persisted storage so the next {@link get} is a fresh tenant_info fetch. */
96
86
  clearCache() {
97
- this.tenantInfoInflightPromise = null;
98
- smartCardStorage.removeItem(CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY);
87
+ this.tenantInfoInflightPromises.clear();
99
88
  }
100
89
  }
101
90
  export const currentSiteCloudIdService = new CurrentSiteCloudIdService();
102
-
103
- /**
104
- * Resolves the current site cloud id through the module-level {@link currentSiteCloudIdService}.
105
- * Returns a stored cloud id immediately when one exists; otherwise waits for the shared
106
- * `tenant_info` request and persists the result for subsequent cached reads.
107
- */
108
- export const getCurrentSiteCloudId = (baseUriWithNoTrailingSlash = '') => currentSiteCloudIdService.get(baseUriWithNoTrailingSlash);
109
-
110
- /**
111
- * Reads the current site cloud id from browser storage (the `site-cloud-id:v1` row) via the
112
- * module-level {@link currentSiteCloudIdService} singleton, without awaiting network work.
113
- * Calling this also starts the shared `tenant_info` refresh in the background when one is not
114
- * already running, so a later call can observe a refreshed value when available.
115
- */
116
- export function getCachedCurrentSiteCloudIdAndRefresh() {
117
- return currentSiteCloudIdService.getCachedCloudIdAndRefresh();
118
- }
91
+ export function getCurrentSiteCloudIdSync(baseUriWithNoTrailingSlash = '') {
92
+ return currentSiteCloudIdService.getStoredCloudId(baseUriWithNoTrailingSlash);
93
+ }
94
+ export const getCurrentSiteCloudId = (baseUriWithNoTrailingSlash = '') => currentSiteCloudIdService.get(baseUriWithNoTrailingSlash);
@@ -1,20 +1,18 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { StorageClient } from '@atlaskit/frontend-utilities/storage-client';
3
- import { getCurrentSiteCloudId, getCachedCurrentSiteCloudIdAndRefresh } from '../current-site-cloud-id';
3
+ import { getCurrentSiteCloudId, getCurrentSiteCloudIdSync } from '../current-site-cloud-id';
4
4
  const BASE_URL = '/gateway/api/tap-delivery/api/v3/personalization';
5
-
6
- /**
7
- * Logical key shape: `@atlaskit/smart-card:<feature>:<schema-version>:<scope>` (see smart-card
8
- * storage conventions). {@link StorageClient} narrows the localStorage key to
9
- * `<clientKey>_<itemKey>` with `clientKey === '@atlaskit/smart-card'` and
10
- * `itemKey === 'pct-map:v1:<cloudId>:<traitName>'` (scope segments URI-encoded).
11
- */
12
- export const PERSONALIZATION_STORAGE_SCOPE = '@atlaskit/smart-card';
5
+ export const PERSONALIZATION_STORAGE_SCOPE = 'smart-card-social-proof';
13
6
  export const PERSONALIZATION_STORAGE_ITEM_KEY_PREFIX = 'pct-map:v1:';
7
+ export const PERSONALIZATION_PROVIDER_PCT_TTL_MS = 24 * 60 * 60 * 1000;
8
+ export const SOCIAL_PROOF_TRAIT_NAME = 'sl_3p_connected_providers_site_pct';
14
9
  const smartCardStorage = new StorageClient(PERSONALIZATION_STORAGE_SCOPE);
15
10
 
16
11
  /** Keys written by this service in localStorage when using {@link smartCardStorage}. */
17
12
  const LOCAL_STORAGE_ROW_KEY_PREFIX = `${PERSONALIZATION_STORAGE_SCOPE}_${PERSONALIZATION_STORAGE_ITEM_KEY_PREFIX}`;
13
+ function scopedCacheKey(cloudId, traitName) {
14
+ return `${cloudId}:${traitName}`;
15
+ }
18
16
  function pctMapStorageItemKey(cloudId, traitName) {
19
17
  return `${PERSONALIZATION_STORAGE_ITEM_KEY_PREFIX}${encodeURIComponent(cloudId)}:${encodeURIComponent(traitName)}`;
20
18
  }
@@ -41,27 +39,24 @@ export class PersonalizationService {
41
39
  constructor() {
42
40
  _defineProperty(this, "cache", new Map());
43
41
  }
44
- /**
45
- * Returns the currently cached provider percentage map synchronously and starts a background refresh.
46
- * The refresh result is persisted for future calls but is not awaited by this call.
47
- */
48
- getCachedProviderPctMapAndRefresh(traitName) {
49
- const cloudId = getCachedCurrentSiteCloudIdAndRefresh();
50
- const fromStorage = cloudId && this.readStoredProviderPctMap(cloudId, traitName) || null;
51
- void this.getProviderPctMap(traitName);
52
- return fromStorage;
42
+ /** Pure synchronous read for an explicit cloud id / trait pair. */
43
+ getProviderPctMapSync(cloudId, traitName) {
44
+ if (!cloudId) {
45
+ return null;
46
+ }
47
+ return this.readStoredProviderPctMap(cloudId, traitName);
53
48
  }
54
- async getProviderPctMap(traitName) {
55
- const cachedPromise = this.cache.get(traitName);
49
+ async getProviderPctMap(cloudId, traitName) {
50
+ if (!cloudId) {
51
+ return undefined;
52
+ }
53
+ const cacheKey = scopedCacheKey(cloudId, traitName);
54
+ const cachedPromise = this.cache.get(cacheKey);
56
55
  if (cachedPromise) {
57
56
  return cachedPromise;
58
57
  }
59
58
  const promise = (async () => {
60
59
  try {
61
- const cloudId = await getCurrentSiteCloudId();
62
- if (!cloudId) {
63
- return undefined;
64
- }
65
60
  const traits = await fetchSiteTraits(cloudId);
66
61
  const trait = traits.find(t => t.name === traitName);
67
62
  const mapped = this.parseTraitValue(trait === null || trait === void 0 ? void 0 : trait.value);
@@ -73,8 +68,11 @@ export class PersonalizationService {
73
68
  return undefined;
74
69
  }
75
70
  })();
76
- this.cache.set(traitName, promise);
77
- return promise;
71
+ const retryablePromise = promise.finally(() => {
72
+ this.cache.delete(cacheKey);
73
+ });
74
+ this.cache.set(cacheKey, retryablePromise);
75
+ return retryablePromise;
78
76
  }
79
77
  readStoredProviderPctMap(cloudId, traitName) {
80
78
  try {
@@ -89,7 +87,7 @@ export class PersonalizationService {
89
87
  }
90
88
  writeStoredProviderPctMap(cloudId, traitName, map) {
91
89
  try {
92
- smartCardStorage.setItemWithExpiry(pctMapStorageItemKey(cloudId, traitName), map);
90
+ smartCardStorage.setItemWithExpiry(pctMapStorageItemKey(cloudId, traitName), map, PERSONALIZATION_PROVIDER_PCT_TTL_MS);
93
91
  } catch {
94
92
  // Quota, private-mode, etc.
95
93
  }
@@ -145,20 +143,23 @@ export class PersonalizationService {
145
143
  }
146
144
  }
147
145
  export const personalizationService = new PersonalizationService();
146
+ export const getProviderPctMap = (cloudId, traitName) => personalizationService.getProviderPctMap(cloudId, traitName);
147
+ export function getProviderPctMapSync(cloudId, traitName) {
148
+ return personalizationService.getProviderPctMapSync(cloudId, traitName);
149
+ }
148
150
 
149
151
  /**
150
- * Resolves the provider percentage map for a TAP Delivery trait through the module-level
151
- * {@link personalizationService}. Work is deduped per trait name for the page lifetime, and a
152
- * successful response is persisted by cloud id and trait name for later cached reads.
153
- */
154
- export const getProviderPctMap = traitName => personalizationService.getProviderPctMap(traitName);
155
-
156
- /**
157
- * Reads the provider percentage map for a trait from browser storage via the module-level
158
- * {@link personalizationService} singleton, without awaiting network work.
159
- * Calling this also starts the trait-scoped shared refresh in the background, so a later call can
160
- * use a refreshed value when it becomes available.
152
+ * Backwards-compatible cache-first helper for inline-card social proof callers.
153
+ *
154
+ * Reads the persisted provider percentage map synchronously using the current site cloud id, then
155
+ * starts a background refresh for subsequent mounts. The async result intentionally does not affect
156
+ * the current call site, matching the warm-cache-only rendering contract.
161
157
  */
162
158
  export function getCachedProviderPctMapAndRefresh(traitName) {
163
- return personalizationService.getCachedProviderPctMapAndRefresh(traitName);
159
+ const cloudId = getCurrentSiteCloudIdSync();
160
+ const providerPctMap = getProviderPctMapSync(cloudId, traitName);
161
+ void getCurrentSiteCloudId().then(resolvedCloudId => {
162
+ void getProviderPctMap(resolvedCloudId, traitName);
163
+ });
164
+ return providerPctMap;
164
165
  }
@@ -2,7 +2,7 @@ export const ANALYTICS_CHANNEL = 'media';
2
2
  export const context = {
3
3
  componentName: 'smart-cards',
4
4
  packageName: "@atlaskit/smart-card" || '',
5
- packageVersion: "44.6.1" || ''
5
+ packageVersion: "44.7.0" || ''
6
6
  };
7
7
  export let TrackQuickActionType = /*#__PURE__*/function (TrackQuickActionType) {
8
8
  TrackQuickActionType["StatusUpdate"] = "StatusUpdate";
@@ -0,0 +1,27 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import React from 'react';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { Box, Text } from '@atlaskit/primitives/compiled';
5
+ import { messages } from '../../../messages';
6
+ // TODO: remove when social-proof-3p-unauth-block-fg is cleaned up
7
+ const SocialProofMessage = ({
8
+ tier,
9
+ connectedPct,
10
+ providerName,
11
+ testId = 'smart-block-social-proof-message'
12
+ }) => {
13
+ const message = tier === 'not-low' ? messages.pre_auth_block_social_proof_not_low : messages.pre_auth_block_social_proof_low;
14
+ return /*#__PURE__*/React.createElement(Box, {
15
+ testId: testId
16
+ }, /*#__PURE__*/React.createElement(FormattedMessage, _extends({}, message, {
17
+ values: {
18
+ percentage: connectedPct !== null && connectedPct !== void 0 ? connectedPct : 0,
19
+ provider: providerName,
20
+ b: chunks => /*#__PURE__*/React.createElement(Text, {
21
+ as: "strong",
22
+ weight: "bold"
23
+ }, chunks)
24
+ }
25
+ })));
26
+ };
27
+ export default SocialProofMessage;
@@ -5,13 +5,18 @@ import * as React from 'react';
5
5
  import { ax, ix } from "@compiled/react/runtime";
6
6
  import { useCallback, useMemo } from 'react';
7
7
  import { FormattedMessage } from 'react-intl';
8
+ import { di } from 'react-magnetic-di';
9
+ import { AnalyticsContext } from '@atlaskit/analytics-next';
8
10
  import { extractSmartLinkProvider } from '@atlaskit/link-extractors';
11
+ import { useSmartLinkContext } from '@atlaskit/link-provider';
12
+ import { componentWithFG } from '@atlaskit/platform-feature-flags-react';
9
13
  import { Box } from '@atlaskit/primitives/compiled';
10
14
  import { useAnalyticsEvents } from '../../../common/analytics/generated/use-analytics-events';
11
15
  import { ElementName, SmartLinkDirection, SmartLinkSize, SmartLinkWidth } from '../../../constants';
12
16
  import { messages } from '../../../messages';
13
17
  import { useFlexibleCardContext } from '../../../state/flexible-ui-context';
14
18
  import { hasAuthScopeOverrides } from '../../../state/helpers';
19
+ import useSocialProofExperiment from '../../../state/hooks/use-social-proof-experiment';
15
20
  import UnauthorisedViewContent from '../../common/UnauthorisedViewContent';
16
21
  import FlexibleCard from '../../FlexibleCard';
17
22
  import ActionGroup from '../../FlexibleCard/components/blocks/action-group';
@@ -21,6 +26,7 @@ import { renderElementItems } from '../../FlexibleCard/components/blocks/utils';
21
26
  import { LinkIcon, Title } from '../../FlexibleCard/components/elements';
22
27
  import { AuthorizeAction } from '../actions/AuthorizeAction';
23
28
  import unauthIllustrationGeneral from './assets/general@2x.png';
29
+ import SocialProofMessage from './SocialProofMessage';
24
30
  import { FlexibleCardUiOptions, titleBlockOptions } from './utils';
25
31
  import { withFlexibleUIBlockCardStyle } from './utils/withFlexibleUIBlockCardStyle';
26
32
  const contentStyles = null;
@@ -163,17 +169,15 @@ const UnauthorisedBlock = ({
163
169
  * @see SmartLinkStatus
164
170
  * @see FlexibleCardProps
165
171
  */
166
- const UnauthorisedView = ({
167
- testId = 'smart-block-unauthorized-view',
172
+ const UnauthorisedViewFrame = ({
173
+ content,
174
+ providerName,
175
+ testId,
168
176
  ...props
169
177
  }) => {
170
- var _extractSmartLinkProv;
171
178
  const {
172
- cardState,
173
179
  onAuthorize
174
180
  } = props;
175
- const providerName = (_extractSmartLinkProv = extractSmartLinkProvider(cardState.details)) === null || _extractSmartLinkProv === void 0 ? void 0 : _extractSmartLinkProv.text;
176
- const isProductIntegrationSupported = hasAuthScopeOverrides(cardState.details);
177
181
  const {
178
182
  fireEvent
179
183
  } = useAnalyticsEvents();
@@ -183,15 +187,6 @@ const UnauthorisedView = ({
183
187
  onAuthorize();
184
188
  }
185
189
  }, [onAuthorize, fireEvent]);
186
- const content = useMemo(() => onAuthorize ? /*#__PURE__*/React.createElement(UnauthorisedViewContent, {
187
- providerName: providerName,
188
- isProductIntegrationSupported: isProductIntegrationSupported,
189
- testId: testId
190
- }) : /*#__PURE__*/React.createElement(FormattedMessage, _extends({}, messages[providerName ? 'unauthorised_account_description' : 'unauthorised_account_description_no_provider'], {
191
- values: {
192
- context: providerName
193
- }
194
- })), [isProductIntegrationSupported, onAuthorize, providerName, testId]);
195
190
  const actions = useMemo(() => onAuthorize ? [AuthorizeAction(handleAuthorize, providerName)] : [], [handleAuthorize, onAuthorize, providerName]);
196
191
  return /*#__PURE__*/React.createElement(FlexibleCard, _extends({
197
192
  appearance: "block",
@@ -210,5 +205,78 @@ const UnauthorisedView = ({
210
205
  className: ax(["_11c8fhey _syazi7uo _19pku2gc", actions.length > 0 && "_1reo15vq _18m915vq _otyrpxbi _p12f1osq _o5721q9c _1bto1l2s"])
211
206
  }, content)));
212
207
  };
208
+ const UnauthorisedViewBase = ({
209
+ testId = 'smart-block-unauthorized-view',
210
+ ...props
211
+ }) => {
212
+ var _extractSmartLinkProv;
213
+ const {
214
+ cardState,
215
+ onAuthorize
216
+ } = props;
217
+ const providerName = (_extractSmartLinkProv = extractSmartLinkProvider(cardState.details)) === null || _extractSmartLinkProv === void 0 ? void 0 : _extractSmartLinkProv.text;
218
+ const isProductIntegrationSupported = hasAuthScopeOverrides(cardState.details);
219
+ const content = useMemo(() => onAuthorize ? /*#__PURE__*/React.createElement(UnauthorisedViewContent, {
220
+ providerName: providerName,
221
+ isProductIntegrationSupported: isProductIntegrationSupported,
222
+ testId: testId
223
+ }) : /*#__PURE__*/React.createElement(FormattedMessage, _extends({}, messages[providerName ? 'unauthorised_account_description' : 'unauthorised_account_description_no_provider'], {
224
+ values: {
225
+ context: providerName
226
+ }
227
+ })), [isProductIntegrationSupported, onAuthorize, providerName, testId]);
228
+ return /*#__PURE__*/React.createElement(UnauthorisedViewFrame, _extends({}, props, {
229
+ content: content,
230
+ providerName: providerName,
231
+ testId: testId
232
+ }));
233
+ };
234
+
235
+ /**
236
+ * Experiment wrapper: fires social proof exposure and renders social proof UI when FG is on.
237
+ * TODO: remove when social-proof-3p-unauth-block-fg is cleaned up
238
+ */
239
+ const UnauthorisedViewWithExperiment = ({
240
+ testId = 'smart-block-unauthorized-view',
241
+ ...props
242
+ }) => {
243
+ var _props$cardState, _props$cardState$deta, _props$cardState$deta2, _extractSmartLinkProv2;
244
+ const extensionKey = (_props$cardState = props.cardState) === null || _props$cardState === void 0 ? void 0 : (_props$cardState$deta = _props$cardState.details) === null || _props$cardState$deta === void 0 ? void 0 : (_props$cardState$deta2 = _props$cardState$deta.meta) === null || _props$cardState$deta2 === void 0 ? void 0 : _props$cardState$deta2.key;
245
+ const providerName = (_extractSmartLinkProv2 = extractSmartLinkProvider(props.cardState.details)) === null || _extractSmartLinkProv2 === void 0 ? void 0 : _extractSmartLinkProv2.text;
246
+ const {
247
+ connections
248
+ } = useSmartLinkContext();
249
+ const {
250
+ isTreatment,
251
+ tier,
252
+ connectedPct
253
+ } = useSocialProofExperiment(providerName ? extensionKey : undefined, connections.client.baseUrlOverride);
254
+ if (!isTreatment || !providerName) {
255
+ return /*#__PURE__*/React.createElement(UnauthorisedViewBase, _extends({}, props, {
256
+ testId: testId
257
+ }));
258
+ }
259
+ return /*#__PURE__*/React.createElement(AnalyticsContext, {
260
+ data: {
261
+ attributes: {
262
+ experiment: 'social_proof_3p_unauth_block_exp',
263
+ cohort: 'treatment',
264
+ tier
265
+ }
266
+ }
267
+ }, /*#__PURE__*/React.createElement(UnauthorisedViewFrame, _extends({}, props, {
268
+ content: /*#__PURE__*/React.createElement(SocialProofMessage, {
269
+ tier: tier,
270
+ connectedPct: connectedPct,
271
+ providerName: providerName
272
+ }),
273
+ providerName: providerName,
274
+ testId: testId
275
+ })));
276
+ };
277
+ const UnauthorisedViewWithoutExperiment = props => {
278
+ return /*#__PURE__*/React.createElement(UnauthorisedViewBase, props);
279
+ };
280
+ const UnauthorisedView = componentWithFG('social-proof-3p-unauth-block-fg', UnauthorisedViewWithExperiment, UnauthorisedViewWithoutExperiment);
213
281
  const _default_1 = withFlexibleUIBlockCardStyle(UnauthorisedView);
214
282
  export default _default_1;
@@ -12,7 +12,7 @@ import LinkWarningModal from './LinkWarningModal';
12
12
  import { useLinkWarningModal } from './LinkWarningModal/hooks/use-link-warning-modal';
13
13
  const PACKAGE_DATA = {
14
14
  packageName: "@atlaskit/smart-card",
15
- packageVersion: "44.6.1",
15
+ packageVersion: "44.7.0",
16
16
  componentName: 'linkUrl'
17
17
  };
18
18
  const Anchor = withLinkClickedEvent('a');
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b072be1d67bf9de1dce752032500f85882f4e3106047ba57d4944e0e9e123c34
3
+ size 17460
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bc4ec248eb4dfc15dc531155a386a12571398ad5c249de38df839c94cc03c4ab
3
+ size 16074
@@ -0,0 +1,3 @@
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:164d340225092b0d1ac5bd3daf47b305aeeadd0544dc8f76b0fbda83e200572a
3
+ size 16863
@@ -988,7 +988,7 @@ export var messages = defineMessages({
988
988
  },
989
989
  rovo_prompt_message_highlight_relevant_content: {
990
990
  id: 'fabric.linking.rovo_prompt_message_highlight_relevant_content.non-final',
991
- defaultMessage: '<p>Based on this linked item (<a>{url}</a>) and the {context} Im currently viewing, highlight the parts of the linked content that are most relevant to this work. Explain briefly why each part is relevant.</p>',
991
+ defaultMessage: "<p>Based on this linked item (<a>{url}</a>) and the {context} I'm currently viewing, highlight the parts of the linked content that are most relevant to this work. Explain briefly why each part is relevant.</p>",
992
992
  description: 'The prompt message to send to Rovo Chat. {context} refers to the content the user triggered from, e.g. Confluence page or Jira work item. {url} refers to Smart Link that the user triggers this action from. (Please make sure all html tags remain the same.)'
993
993
  },
994
994
  rovo_prompt_button_identify_key_trends: {
@@ -1050,5 +1050,16 @@ export var messages = defineMessages({
1050
1050
  id: 'fabric.linking.rovo_prompt_button_show_me_whats_relevant.non-final',
1051
1051
  defaultMessage: "Show me what's relevant",
1052
1052
  description: 'The name of the action to send prompt message to Rovo Chat in relation to current Smart Link'
1053
+ },
1054
+ // TODO: remove when social-proof-3p-unauth-block-fg is cleaned up
1055
+ pre_auth_block_social_proof_not_low: {
1056
+ id: 'fabric.linking.pre_auth_block_social_proof_not_low',
1057
+ defaultMessage: '<b>{percentage}%</b> of your team is previewing <b>{provider}</b>.',
1058
+ description: 'Social proof message shown on unauthorized 3P block cards when 30% or more of the tenant has connected the provider. {percentage} is a number, {provider} is the 3P app name (e.g. OneDrive).'
1059
+ },
1060
+ pre_auth_block_social_proof_low: {
1061
+ id: 'fabric.linking.pre_auth_block_social_proof_low',
1062
+ defaultMessage: 'Your team is previewing <b>{provider}</b>.',
1063
+ description: 'Social proof message shown on unauthorized 3P block cards when less than 30% of the tenant has connected the provider. {provider} is the 3P app name (e.g. OneDrive).'
1053
1064
  }
1054
1065
  });
@@ -1,7 +1,6 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { getCurrentSiteCloudId } from '../../services/current-site-cloud-id';
4
- export { CURRENT_SITE_CLOUD_ID_LOCAL_STORAGE_KEY, CURRENT_SITE_CLOUD_ID_STORAGE_ITEM_KEY, currentSiteCloudIdService, getCurrentSiteCloudId, getCachedCurrentSiteCloudIdAndRefresh } from '../../services/current-site-cloud-id';
5
4
  var useCurrentSiteCloudId = function useCurrentSiteCloudId() {
6
5
  var _useState = useState(undefined),
7
6
  _useState2 = _slicedToArray(_useState, 2),
@@ -3,7 +3,7 @@ import _regeneratorRuntime from "@babel/runtime/regenerator";
3
3
  import { useCallback, useMemo } from 'react';
4
4
  import { request } from '@atlaskit/linking-common';
5
5
  import { fg } from '@atlaskit/platform-feature-flags';
6
- import { getCurrentSiteCloudId } from '../use-current-site-cloud-id';
6
+ import { getCurrentSiteCloudId } from '../../services/current-site-cloud-id';
7
7
  import { queryIncomingOutgoingLinks as queryIncomingOutgoingAris } from './query';
8
8
  /**
9
9
  * @param baseUriWithNoTrailingSlash base url which will then be appended with /gateway/api/graphql to make requests to AGG
@@ -72,7 +72,7 @@ var useIncomingOutgoingAri = function useIncomingOutgoingAri() {
72
72
  }
73
73
  return _context3.abrupt("return", match[2]);
74
74
  case 4:
75
- if (!fg('platform_sl_3p_preauth_soc_proof_inline_killswitch')) {
75
+ if (!fg('platform_sl_incoming_outgoing_tenant_info_killswitch')) {
76
76
  _context3.next = 10;
77
77
  break;
78
78
  }
@@ -1,6 +1,7 @@
1
1
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
2
  import { useEffect, useMemo, useState } from 'react';
3
- import { getProviderPctMap } from '../../services/personalization';
3
+ import { getCurrentSiteCloudId, getCurrentSiteCloudIdSync } from '../../services/current-site-cloud-id';
4
+ import { getProviderPctMap, getProviderPctMapSync, SOCIAL_PROOF_TRAIT_NAME } from '../../services/personalization';
4
5
  var NOT_ENABLED_RESULT = {
5
6
  connectedPct: undefined,
6
7
  isEnabled: false,
@@ -8,46 +9,61 @@ var NOT_ENABLED_RESULT = {
8
9
  };
9
10
 
10
11
  /**
11
- * Fetches provider usage percentage from the TAP Delivery personalization service when the
12
- * killswitch allows it. Callers decide separately (e.g. via Statsig experiment) whether to
13
- * surface that data in the UI.
12
+ * Cache-first social proof hook.
13
+ *
14
+ * On mount:
15
+ * 1. Reads localStorage synchronously via `getProviderPctMapSync`.
16
+ * - If data exists (warm cache): sets `providerPctMap` immediately, `isLoading = false`.
17
+ * - If no data (cold cache): leaves `providerPctMap` undefined, `isLoading = false`.
18
+ * 2. Always kicks off an async fetch (fire-and-forget) via `getProviderPctMap` to populate
19
+ * localStorage for next page load. Does NOT update state with the async result.
20
+ *
21
+ * This means:
22
+ * - First page visit (cold): no social proof rendered, no experiment exposure fired.
23
+ * Background fetch populates localStorage for next time.
24
+ * - Second page visit (warm): social proof renders immediately from localStorage.
25
+ * Background refresh keeps localStorage fresh.
26
+ *
27
+ * Callers decide separately (e.g. via Statsig experiment) whether to surface the data.
14
28
  */
15
- var useSocialProof = function useSocialProof(traitName, extensionKey) {
16
- var isKillswitchOn = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
29
+ var useSocialProof = function useSocialProof(extensionKey) {
30
+ var isKillswitchOn = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
31
+ var baseUriWithNoTrailingSlash = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '';
17
32
  var isEnabled = isKillswitchOn;
18
- var _useState = useState(undefined),
19
- _useState2 = _slicedToArray(_useState, 2),
20
- providerPctMap = _useState2[0],
21
- setProviderPctMap = _useState2[1];
22
- var _useState3 = useState(false),
23
- _useState4 = _slicedToArray(_useState3, 2),
24
- isPersonalizationLoading = _useState4[0],
25
- setIsPersonalizationLoading = _useState4[1];
33
+ var _useState = useState(function () {
34
+ if (!isEnabled) {
35
+ return {
36
+ cloudId: undefined,
37
+ providerPctMap: null
38
+ };
39
+ }
40
+ var cloudId = getCurrentSiteCloudIdSync(baseUriWithNoTrailingSlash);
41
+ return {
42
+ cloudId: cloudId,
43
+ providerPctMap: getProviderPctMapSync(cloudId, SOCIAL_PROOF_TRAIT_NAME)
44
+ };
45
+ }),
46
+ _useState2 = _slicedToArray(_useState, 1),
47
+ snapshot = _useState2[0];
48
+
49
+ // Fire-and-forget: warm caches for future mounts only. Never update this mount's treatment UI.
26
50
  useEffect(function () {
27
51
  if (!isEnabled) {
28
52
  return;
29
53
  }
30
- var cancelled = false;
31
- setIsPersonalizationLoading(true);
32
- getProviderPctMap(traitName).then(function (pctMap) {
33
- if (!cancelled) {
34
- setProviderPctMap(pctMap);
35
- setIsPersonalizationLoading(false);
36
- }
54
+ void getCurrentSiteCloudId(baseUriWithNoTrailingSlash).then(function (cloudId) {
55
+ void getProviderPctMap(cloudId, SOCIAL_PROOF_TRAIT_NAME);
37
56
  });
38
- return function () {
39
- cancelled = true;
40
- };
41
- }, [isEnabled, traitName]);
57
+ }, [baseUriWithNoTrailingSlash, isEnabled]);
42
58
  return useMemo(function () {
43
59
  if (!isEnabled) {
44
60
  return NOT_ENABLED_RESULT;
45
61
  }
46
62
  return {
47
- connectedPct: extensionKey && providerPctMap ? providerPctMap[extensionKey] : undefined,
48
- isEnabled: isEnabled,
49
- isLoading: isPersonalizationLoading
63
+ connectedPct: extensionKey && snapshot.providerPctMap ? snapshot.providerPctMap[extensionKey] : undefined,
64
+ isEnabled: Boolean(snapshot.cloudId && snapshot.providerPctMap),
65
+ isLoading: false // sync read is instant; never in "loading" state
50
66
  };
51
- }, [extensionKey, isEnabled, isPersonalizationLoading, providerPctMap]);
67
+ }, [extensionKey, isEnabled, snapshot]);
52
68
  };
53
69
  export default useSocialProof;