@chipmobilesdk/rn-ads 0.4.0 → 0.5.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.
- package/CHANGELOG.md +224 -196
- package/README.md +6 -1
- package/package.json +2 -5
- package/skills/sdk-rn-ads/SKILL.md +1 -2
- package/src/config/resolveAdUnit.ts +85 -85
- package/src/config/types.ts +318 -321
- package/src/config/validate.ts +1 -4
- package/src/core/adOriginatedReturn.ts +134 -134
- package/src/core/adsInstance.ts +76 -37
- package/src/core/consentFlow.ts +182 -180
- package/src/core/enablement.ts +76 -76
- package/src/core/fullScreenLock.ts +54 -54
- package/src/core/gates.ts +94 -91
- package/src/core/initMachine.ts +187 -187
- package/src/core/placementMachine.ts +124 -114
- package/src/core/privacyGate.ts +93 -85
- package/src/core/retryPolicy.ts +127 -127
- package/src/core/rewardedSession.ts +84 -84
- package/src/core/timers.ts +216 -208
- package/src/diagnostics/correlation.ts +31 -31
- package/src/diagnostics/errors.ts +230 -234
- package/src/diagnostics/events.ts +238 -232
- package/src/engine/admobEngine.ts +399 -399
- package/src/engine/port.ts +144 -125
- package/src/engine/testAdUnits.ts +61 -61
- package/src/react/AdsBanner.tsx +17 -17
- package/src/react/AdsProvider.tsx +57 -57
- package/src/react/hooks.ts +75 -75
- package/src/testing/index.ts +463 -460
package/src/core/retryPolicy.ts
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bounded, opt-in retry for failed loads (FR-134 – FR-139).
|
|
3
|
-
*
|
|
4
|
-
* 0.1.0 shipped no retry on purpose, and the reason was sound: every retry is a
|
|
5
|
-
* billable request, and only the app knows what that costs. But the practical
|
|
6
|
-
* result was that every app rewrote the same backoff loop, and the cost of
|
|
7
|
-
* writing it wrong landed on a bill. This ships the loop once, keeps it **off by
|
|
8
|
-
* default**, and makes every attempt visible.
|
|
9
|
-
*
|
|
10
|
-
* Three properties do the work:
|
|
11
|
-
*
|
|
12
|
-
* - Off unless configured, so upgrading from 0.1.0 issues not one extra
|
|
13
|
-
* request (FR-134, and the compat test proves it rather than asserting it).
|
|
14
|
-
* - Only `retryable` errors. Configuration and consent errors, an unsupported
|
|
15
|
-
* platform, and a disabled or undecided instance are states another attempt
|
|
16
|
-
* cannot change, so retrying them would spend money on a certainty (FR-135).
|
|
17
|
-
* - Every attempt is an event carrying the load cycle's correlation id, so a
|
|
18
|
-
* bill can be reconciled against the event stream alone (FR-138).
|
|
19
|
-
*
|
|
20
|
-
* Pure — the schedule is computed here, the waiting happens in the instance's
|
|
21
|
-
* TimerSet so `dispose()` can prove it released everything.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { AdsConfigError } from '../diagnostics/errors';
|
|
25
|
-
import { checkBand, DEFAULT_RETRY_POLICY, RETRY_BOUNDS } from './timers';
|
|
26
|
-
import type { RetryPolicy } from '../config/types';
|
|
27
|
-
|
|
28
|
-
export interface ResolvedRetryPolicy {
|
|
29
|
-
readonly maxRetries: number;
|
|
30
|
-
readonly initialDelayMs: number;
|
|
31
|
-
readonly maxDelayMs: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Validates one policy against the bounds.
|
|
36
|
-
*
|
|
37
|
-
* Out-of-range values **throw** rather than being clamped, matching the rule
|
|
38
|
-
* `timers.ts` already follows: silently clamping a request for ten retries down
|
|
39
|
-
* to five tells the app it configured something it did not, and the discrepancy
|
|
40
|
-
* only surfaces later as a bill that does not match the code.
|
|
41
|
-
*
|
|
42
|
-
* `label` names the offending field precisely — an error that says only
|
|
43
|
-
* "invalid retry policy" makes an integrator hunt through a catalog by hand.
|
|
44
|
-
*/
|
|
45
|
-
export function resolveRetryPolicy(policy: RetryPolicy, label: string): ResolvedRetryPolicy {
|
|
46
|
-
const maxRetries = checkBand(
|
|
47
|
-
`${label}.maxRetries`,
|
|
48
|
-
policy.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
49
|
-
RETRY_BOUNDS.maxRetries,
|
|
50
|
-
);
|
|
51
|
-
const initialDelayMs = checkBand(
|
|
52
|
-
`${label}.initialDelayMs`,
|
|
53
|
-
policy.initialDelayMs ?? DEFAULT_RETRY_POLICY.initialDelayMs,
|
|
54
|
-
RETRY_BOUNDS.initialDelayMs,
|
|
55
|
-
);
|
|
56
|
-
const maxDelayMs = checkBand(
|
|
57
|
-
`${label}.maxDelayMs`,
|
|
58
|
-
policy.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs,
|
|
59
|
-
RETRY_BOUNDS.maxDelayMs,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
if (!Number.isInteger(maxRetries)) {
|
|
63
|
-
throw new AdsConfigError('retry_out_of_bounds', {
|
|
64
|
-
message: `${label}.maxRetries must be a whole number; received ${maxRetries}`,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (initialDelayMs > maxDelayMs) {
|
|
68
|
-
throw new AdsConfigError('retry_out_of_bounds', {
|
|
69
|
-
message: `${label}.initialDelayMs (${initialDelayMs}ms) exceeds ${label}.maxDelayMs (${maxDelayMs}ms); the first retry would already be past the ceiling`,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { maxRetries, initialDelayMs, maxDelayMs };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Resolves the effective policy for one placement.
|
|
78
|
-
*
|
|
79
|
-
* An override **replaces** the instance-wide policy rather than merging into it.
|
|
80
|
-
* Merging would mean the effective policy for a placement could not be read off
|
|
81
|
-
* the configuration — you would have to simulate the merge to know what it was —
|
|
82
|
-
* and a per-field merge makes "this placement retries twice" quietly depend on a
|
|
83
|
-
* value written somewhere else.
|
|
84
|
-
*
|
|
85
|
-
* Both are validated against the same bounds, so an override narrows behavior
|
|
86
|
-
* but can never exceed a ceiling (FR-136a).
|
|
87
|
-
*/
|
|
88
|
-
export function resolveEffectiveRetry(
|
|
89
|
-
instancePolicy: RetryPolicy | undefined,
|
|
90
|
-
placementPolicy: RetryPolicy | false | undefined,
|
|
91
|
-
placementKey: string,
|
|
92
|
-
): ResolvedRetryPolicy | null {
|
|
93
|
-
if (placementPolicy === false) {
|
|
94
|
-
// Explicit opt-out for this placement alone. A banner retry is cheap and
|
|
95
|
-
// invisible; a rewarded retry happens with a user waiting on the other
|
|
96
|
-
// side of it. An app may reasonably want one and not the other.
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
if (placementPolicy) {
|
|
100
|
-
return resolveRetryPolicy(placementPolicy, `placements.${placementKey}.retry`);
|
|
101
|
-
}
|
|
102
|
-
if (instancePolicy) {
|
|
103
|
-
return resolveRetryPolicy(instancePolicy, 'retry');
|
|
104
|
-
}
|
|
105
|
-
// Nothing configured anywhere: retry is entirely off, which is the default
|
|
106
|
-
// and the only behavior a 0.1.0 consumer can observe (FR-134).
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Delay before the given attempt. `attempt` is 1-based — the first retry is 1.
|
|
112
|
-
*
|
|
113
|
-
* Exponential with a fixed x2 multiplier and **no jitter**. Jitter exists to
|
|
114
|
-
* desynchronize a fleet of clients hitting one server; a single device retrying
|
|
115
|
-
* its own load has no herd to avoid. Leaving it out keeps "two retries, 2s then
|
|
116
|
-
* 4s" an exact assertion instead of a statistical one, which matters more here
|
|
117
|
-
* than a property that does not apply.
|
|
118
|
-
*/
|
|
119
|
-
export function delayForAttempt(policy: ResolvedRetryPolicy, attempt: number): number {
|
|
120
|
-
const raw = policy.initialDelayMs * Math.pow(2, Math.max(0, attempt - 1));
|
|
121
|
-
return Math.min(raw, policy.maxDelayMs);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Whether another attempt is permitted. `attemptsMade` counts retries only. */
|
|
125
|
-
export function canRetry(policy: ResolvedRetryPolicy | null, attemptsMade: number): boolean {
|
|
126
|
-
return policy !== null && attemptsMade < policy.maxRetries;
|
|
127
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, opt-in retry for failed loads (FR-134 – FR-139).
|
|
3
|
+
*
|
|
4
|
+
* 0.1.0 shipped no retry on purpose, and the reason was sound: every retry is a
|
|
5
|
+
* billable request, and only the app knows what that costs. But the practical
|
|
6
|
+
* result was that every app rewrote the same backoff loop, and the cost of
|
|
7
|
+
* writing it wrong landed on a bill. This ships the loop once, keeps it **off by
|
|
8
|
+
* default**, and makes every attempt visible.
|
|
9
|
+
*
|
|
10
|
+
* Three properties do the work:
|
|
11
|
+
*
|
|
12
|
+
* - Off unless configured, so upgrading from 0.1.0 issues not one extra
|
|
13
|
+
* request (FR-134, and the compat test proves it rather than asserting it).
|
|
14
|
+
* - Only `retryable` errors. Configuration and consent errors, an unsupported
|
|
15
|
+
* platform, and a disabled or undecided instance are states another attempt
|
|
16
|
+
* cannot change, so retrying them would spend money on a certainty (FR-135).
|
|
17
|
+
* - Every attempt is an event carrying the load cycle's correlation id, so a
|
|
18
|
+
* bill can be reconciled against the event stream alone (FR-138).
|
|
19
|
+
*
|
|
20
|
+
* Pure — the schedule is computed here, the waiting happens in the instance's
|
|
21
|
+
* TimerSet so `dispose()` can prove it released everything.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { AdsConfigError } from '../diagnostics/errors';
|
|
25
|
+
import { checkBand, DEFAULT_RETRY_POLICY, RETRY_BOUNDS } from './timers';
|
|
26
|
+
import type { RetryPolicy } from '../config/types';
|
|
27
|
+
|
|
28
|
+
export interface ResolvedRetryPolicy {
|
|
29
|
+
readonly maxRetries: number;
|
|
30
|
+
readonly initialDelayMs: number;
|
|
31
|
+
readonly maxDelayMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validates one policy against the bounds.
|
|
36
|
+
*
|
|
37
|
+
* Out-of-range values **throw** rather than being clamped, matching the rule
|
|
38
|
+
* `timers.ts` already follows: silently clamping a request for ten retries down
|
|
39
|
+
* to five tells the app it configured something it did not, and the discrepancy
|
|
40
|
+
* only surfaces later as a bill that does not match the code.
|
|
41
|
+
*
|
|
42
|
+
* `label` names the offending field precisely — an error that says only
|
|
43
|
+
* "invalid retry policy" makes an integrator hunt through a catalog by hand.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveRetryPolicy(policy: RetryPolicy, label: string): ResolvedRetryPolicy {
|
|
46
|
+
const maxRetries = checkBand(
|
|
47
|
+
`${label}.maxRetries`,
|
|
48
|
+
policy.maxRetries ?? DEFAULT_RETRY_POLICY.maxRetries,
|
|
49
|
+
RETRY_BOUNDS.maxRetries,
|
|
50
|
+
);
|
|
51
|
+
const initialDelayMs = checkBand(
|
|
52
|
+
`${label}.initialDelayMs`,
|
|
53
|
+
policy.initialDelayMs ?? DEFAULT_RETRY_POLICY.initialDelayMs,
|
|
54
|
+
RETRY_BOUNDS.initialDelayMs,
|
|
55
|
+
);
|
|
56
|
+
const maxDelayMs = checkBand(
|
|
57
|
+
`${label}.maxDelayMs`,
|
|
58
|
+
policy.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs,
|
|
59
|
+
RETRY_BOUNDS.maxDelayMs,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!Number.isInteger(maxRetries)) {
|
|
63
|
+
throw new AdsConfigError('retry_out_of_bounds', {
|
|
64
|
+
message: `${label}.maxRetries must be a whole number; received ${maxRetries}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (initialDelayMs > maxDelayMs) {
|
|
68
|
+
throw new AdsConfigError('retry_out_of_bounds', {
|
|
69
|
+
message: `${label}.initialDelayMs (${initialDelayMs}ms) exceeds ${label}.maxDelayMs (${maxDelayMs}ms); the first retry would already be past the ceiling`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { maxRetries, initialDelayMs, maxDelayMs };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolves the effective policy for one placement.
|
|
78
|
+
*
|
|
79
|
+
* An override **replaces** the instance-wide policy rather than merging into it.
|
|
80
|
+
* Merging would mean the effective policy for a placement could not be read off
|
|
81
|
+
* the configuration — you would have to simulate the merge to know what it was —
|
|
82
|
+
* and a per-field merge makes "this placement retries twice" quietly depend on a
|
|
83
|
+
* value written somewhere else.
|
|
84
|
+
*
|
|
85
|
+
* Both are validated against the same bounds, so an override narrows behavior
|
|
86
|
+
* but can never exceed a ceiling (FR-136a).
|
|
87
|
+
*/
|
|
88
|
+
export function resolveEffectiveRetry(
|
|
89
|
+
instancePolicy: RetryPolicy | undefined,
|
|
90
|
+
placementPolicy: RetryPolicy | false | undefined,
|
|
91
|
+
placementKey: string,
|
|
92
|
+
): ResolvedRetryPolicy | null {
|
|
93
|
+
if (placementPolicy === false) {
|
|
94
|
+
// Explicit opt-out for this placement alone. A banner retry is cheap and
|
|
95
|
+
// invisible; a rewarded retry happens with a user waiting on the other
|
|
96
|
+
// side of it. An app may reasonably want one and not the other.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (placementPolicy) {
|
|
100
|
+
return resolveRetryPolicy(placementPolicy, `placements.${placementKey}.retry`);
|
|
101
|
+
}
|
|
102
|
+
if (instancePolicy) {
|
|
103
|
+
return resolveRetryPolicy(instancePolicy, 'retry');
|
|
104
|
+
}
|
|
105
|
+
// Nothing configured anywhere: retry is entirely off, which is the default
|
|
106
|
+
// and the only behavior a 0.1.0 consumer can observe (FR-134).
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delay before the given attempt. `attempt` is 1-based — the first retry is 1.
|
|
112
|
+
*
|
|
113
|
+
* Exponential with a fixed x2 multiplier and **no jitter**. Jitter exists to
|
|
114
|
+
* desynchronize a fleet of clients hitting one server; a single device retrying
|
|
115
|
+
* its own load has no herd to avoid. Leaving it out keeps "two retries, 2s then
|
|
116
|
+
* 4s" an exact assertion instead of a statistical one, which matters more here
|
|
117
|
+
* than a property that does not apply.
|
|
118
|
+
*/
|
|
119
|
+
export function delayForAttempt(policy: ResolvedRetryPolicy, attempt: number): number {
|
|
120
|
+
const raw = policy.initialDelayMs * Math.pow(2, Math.max(0, attempt - 1));
|
|
121
|
+
return Math.min(raw, policy.maxDelayMs);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whether another attempt is permitted. `attemptsMade` counts retries only. */
|
|
125
|
+
export function canRetry(policy: ResolvedRetryPolicy | null, attemptsMade: number): boolean {
|
|
126
|
+
return policy !== null && attemptsMade < policy.maxRetries;
|
|
127
|
+
}
|
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rewarded session bookkeeping (FR-046 – FR-049).
|
|
3
|
-
*
|
|
4
|
-
* Two independent booleans, never derived from one another:
|
|
5
|
-
*
|
|
6
|
-
* rewardConfirmed=false rewardConfirmed=true
|
|
7
|
-
* dismissed=false in progress reward emitted, awaiting dismissal
|
|
8
|
-
* dismissed=true dismissed_no_reward earned
|
|
9
|
-
*
|
|
10
|
-
* The defect this exists to prevent is inferring "earned" from dismissal. It is
|
|
11
|
-
* the most common way rewarded integrations go wrong, and it is a direct revenue
|
|
12
|
-
* and fairness fault: a user who closed the ad after two seconds gets the prize,
|
|
13
|
-
* and the publisher is billed for nothing.
|
|
14
|
-
*
|
|
15
|
-
* Tracking two flags and deriving the outcome makes both callback orderings
|
|
16
|
-
* produce the same result *by construction* rather than by the provider
|
|
17
|
-
* happening to fire in a convenient order (SC-007).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { Reward } from '../config/types';
|
|
21
|
-
|
|
22
|
-
export type RewardedOutcome = 'in_progress' | 'earned' | 'dismissed_without_reward';
|
|
23
|
-
|
|
24
|
-
export interface RewardedSession {
|
|
25
|
-
/**
|
|
26
|
-
* Records a provider reward confirmation. Returns the reward the first time
|
|
27
|
-
* and `null` on every repeat, which is what collapses a duplicate provider
|
|
28
|
-
* emission into exactly one public event (FR-054).
|
|
29
|
-
*/
|
|
30
|
-
confirmReward(reward: Reward): Reward | null;
|
|
31
|
-
/** Records dismissal. Returns true the first time only. */
|
|
32
|
-
dismiss(): boolean;
|
|
33
|
-
outcome(): RewardedOutcome;
|
|
34
|
-
/** True only once the provider actually confirmed. Never inferred (FR-046). */
|
|
35
|
-
get rewardEarned(): boolean;
|
|
36
|
-
get dismissed(): boolean;
|
|
37
|
-
reward(): Reward | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function createRewardedSession(): RewardedSession {
|
|
41
|
-
let confirmed = false;
|
|
42
|
-
let dismissed = false;
|
|
43
|
-
let earnedReward: Reward | null = null;
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
confirmReward(reward) {
|
|
47
|
-
if (confirmed) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
confirmed = true;
|
|
51
|
-
earnedReward = reward;
|
|
52
|
-
return reward;
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
dismiss() {
|
|
56
|
-
if (dismissed) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
dismissed = true;
|
|
60
|
-
return true;
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
outcome() {
|
|
64
|
-
if (!dismissed) {
|
|
65
|
-
return 'in_progress';
|
|
66
|
-
}
|
|
67
|
-
// Read at dismissal time, so a reward that arrived first is honored and
|
|
68
|
-
// one that never arrives is not invented.
|
|
69
|
-
return confirmed ? 'earned' : 'dismissed_without_reward';
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
get rewardEarned() {
|
|
73
|
-
return confirmed;
|
|
74
|
-
},
|
|
75
|
-
|
|
76
|
-
get dismissed() {
|
|
77
|
-
return dismissed;
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
reward() {
|
|
81
|
-
return earnedReward;
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Rewarded session bookkeeping (FR-046 – FR-049).
|
|
3
|
+
*
|
|
4
|
+
* Two independent booleans, never derived from one another:
|
|
5
|
+
*
|
|
6
|
+
* rewardConfirmed=false rewardConfirmed=true
|
|
7
|
+
* dismissed=false in progress reward emitted, awaiting dismissal
|
|
8
|
+
* dismissed=true dismissed_no_reward earned
|
|
9
|
+
*
|
|
10
|
+
* The defect this exists to prevent is inferring "earned" from dismissal. It is
|
|
11
|
+
* the most common way rewarded integrations go wrong, and it is a direct revenue
|
|
12
|
+
* and fairness fault: a user who closed the ad after two seconds gets the prize,
|
|
13
|
+
* and the publisher is billed for nothing.
|
|
14
|
+
*
|
|
15
|
+
* Tracking two flags and deriving the outcome makes both callback orderings
|
|
16
|
+
* produce the same result *by construction* rather than by the provider
|
|
17
|
+
* happening to fire in a convenient order (SC-007).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Reward } from '../config/types';
|
|
21
|
+
|
|
22
|
+
export type RewardedOutcome = 'in_progress' | 'earned' | 'dismissed_without_reward';
|
|
23
|
+
|
|
24
|
+
export interface RewardedSession {
|
|
25
|
+
/**
|
|
26
|
+
* Records a provider reward confirmation. Returns the reward the first time
|
|
27
|
+
* and `null` on every repeat, which is what collapses a duplicate provider
|
|
28
|
+
* emission into exactly one public event (FR-054).
|
|
29
|
+
*/
|
|
30
|
+
confirmReward(reward: Reward): Reward | null;
|
|
31
|
+
/** Records dismissal. Returns true the first time only. */
|
|
32
|
+
dismiss(): boolean;
|
|
33
|
+
outcome(): RewardedOutcome;
|
|
34
|
+
/** True only once the provider actually confirmed. Never inferred (FR-046). */
|
|
35
|
+
get rewardEarned(): boolean;
|
|
36
|
+
get dismissed(): boolean;
|
|
37
|
+
reward(): Reward | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createRewardedSession(): RewardedSession {
|
|
41
|
+
let confirmed = false;
|
|
42
|
+
let dismissed = false;
|
|
43
|
+
let earnedReward: Reward | null = null;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
confirmReward(reward) {
|
|
47
|
+
if (confirmed) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
confirmed = true;
|
|
51
|
+
earnedReward = reward;
|
|
52
|
+
return reward;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
dismiss() {
|
|
56
|
+
if (dismissed) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
dismissed = true;
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
outcome() {
|
|
64
|
+
if (!dismissed) {
|
|
65
|
+
return 'in_progress';
|
|
66
|
+
}
|
|
67
|
+
// Read at dismissal time, so a reward that arrived first is honored and
|
|
68
|
+
// one that never arrives is not invented.
|
|
69
|
+
return confirmed ? 'earned' : 'dismissed_without_reward';
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
get rewardEarned() {
|
|
73
|
+
return confirmed;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
get dismissed() {
|
|
77
|
+
return dismissed;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
reward() {
|
|
81
|
+
return earnedReward;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|